diff --git a/contracts/base/GeneralMiddleware.sol b/contracts/base/GeneralMiddleware.sol index a4b7850f..3d00a7c4 100644 --- a/contracts/base/GeneralMiddleware.sol +++ b/contracts/base/GeneralMiddleware.sol @@ -61,11 +61,20 @@ contract GeneralMiddleware is IbcMwUser, IbcMiddleware, IbcMwEventsEmitter, IbcM bytes32 destPortAddr, bytes calldata appData, uint64 timeoutTimestamp - ) external override { + ) external override returns (uint64 sequence) { emit UCHPacketSent(msg.sender, destPortAddr); - _sendPacket(channelId, IbcUtils.toBytes32(msg.sender), destPortAddr, 0, appData, timeoutTimestamp); + return _sendPacket(channelId, IbcUtils.toBytes32(msg.sender), destPortAddr, 0, appData, timeoutTimestamp); } + function sendUniversalPacketWithFee( + bytes32 channelId, + bytes32 destPortAddr, + bytes calldata appData, + uint64 timeoutTimestamp, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) external payable override returns (uint64 sequence) {} + function sendMWPacket( bytes32 channelId, bytes32 srcPortAddr, @@ -73,8 +82,8 @@ contract GeneralMiddleware is IbcMwUser, IbcMiddleware, IbcMwEventsEmitter, IbcM uint256 srcMwIds, bytes calldata appData, uint64 timeoutTimestamp - ) external override { - _sendPacket(channelId, srcPortAddr, destPortAddr, srcMwIds, appData, timeoutTimestamp); + ) external override returns (uint64 sequence) { + return _sendPacket(channelId, srcPortAddr, destPortAddr, srcMwIds, appData, timeoutTimestamp); } function onRecvMWPacket( @@ -191,7 +200,7 @@ contract GeneralMiddleware is IbcMwUser, IbcMiddleware, IbcMwEventsEmitter, IbcM uint256 srcMwIds, bytes calldata appData, uint64 timeoutTimestamp - ) internal virtual { + ) internal virtual returns (uint64 sequence) { // extra MW custom logic here to process packet, eg. emit MW events, mutate state, etc. // implementer can emit custom data fields suitable for their use cases. // Here we use MW_ID as the custom MW data field. @@ -200,7 +209,7 @@ contract GeneralMiddleware is IbcMwUser, IbcMiddleware, IbcMwEventsEmitter, IbcM ); // send packet to next MW - IbcMwPacketSender(mw).sendMWPacket( + return IbcMwPacketSender(mw).sendMWPacket( channelId, srcPortAddr, destPortAddr, srcMwIds | MW_ID, appData, timeoutTimestamp ); } diff --git a/contracts/core/Dispatcher.sol b/contracts/core/Dispatcher.sol index cba4b7a0..3a5d86f8 100644 --- a/contracts/core/Dispatcher.sol +++ b/contracts/core/Dispatcher.sol @@ -28,6 +28,7 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard. import {Channel, ChannelEnd, ChannelOrder, IbcPacket, ChannelState, AckPacket, Ibc} from "../libs/Ibc.sol"; import {IBCErrors} from "../libs/IbcErrors.sol"; import {IbcUtils} from "../libs/IbcUtils.sol"; +import {IFeeVault} from "../interfaces/IFeeVault.sol"; /** * @title Dispatcher @@ -64,6 +65,7 @@ contract Dispatcher is OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuard, IDi ILightClient _UNUSED; // From previous dispatcher version mapping(bytes32 => string) private _channelIdToConnection; mapping(string => ILightClient) private _connectionToLightClient; + IFeeVault public feeVault; constructor() { _disableInitializers(); @@ -75,13 +77,17 @@ contract Dispatcher is OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuard, IDi * @dev This method should be called only once during contract deployment. * @dev For contract upgarades, which need to reinitialize the contract, use the reinitializer modifier. */ - function initialize(string memory initPortPrefix) public virtual initializer nonReentrant { + function initialize(string memory initPortPrefix, IFeeVault _feeVault) public virtual initializer nonReentrant { if (bytes(initPortPrefix).length == 0) { revert IBCErrors.invalidPortPrefix(); } + if (address(_feeVault) == address(0)) { + revert IBCErrors.invalidAddress(); + } __Ownable_init(); portPrefix = initPortPrefix; portPrefixLen = uint32(bytes(initPortPrefix).length); + feeVault = _feeVault; } /** diff --git a/contracts/core/FeeVault.sol b/contracts/core/FeeVault.sol new file mode 100644 index 00000000..c56d01a2 --- /dev/null +++ b/contracts/core/FeeVault.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright 2024, Polymer Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity 0.8.15; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IFeeVault} from "../interfaces/IFeeVault.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {ChannelOrder} from "../libs/Ibc.sol"; + +contract FeeVault is Ownable, ReentrancyGuard, IFeeVault { + /** + * @notice Deposits the send packet fee for a given channel and sequence that is used for relaying recieve and + * acknowledge steps of a packet handhsake after a dapp has called the sendPacket on dispatcher. + * @dev This function calculates the required fee based on provided gas limits and gas prices, + * and reverts if the sent value does not match the calculated fee. + * The first entry in `gasLimits` and `gasPrices` arrays corresponds to `recvPacket` fees, + * and the second entry corresponds to `ackPacket` fees. + * @param channelId The identifier of the channel. + * @param sequence The sequence number for the packet, returned from the dispatcher sendPacket call. + * @param gasLimits An array containing two gas limit values: + * - gasLimits[0] for `recvPacket` fees + * - gasLimits[1] for `ackPacket` fees. + * @param gasPrices An array containing two gas price values: + * - gasPrices[0] for `recvPacket` fees, for the dest chain + * - gasPrices[1] for `ackPacket` fees, for the src chain + */ + function depositSendPacketFee( + bytes32 channelId, + uint64 sequence, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) external payable nonReentrant { + uint256 fee = gasLimits[0] * gasPrices[0] + gasLimits[1] * gasPrices[1]; + if ((fee) != msg.value) { + revert IncorrectFeeSent(fee, msg.value); + } + emit SendPacketFeeDeposited(channelId, sequence, gasLimits, gasPrices); + } + + /** + * @notice Deposits the fee for a channel handshake, to pay a relayer for relaying the channelOpenTry, + * channelOpenConfirm, and channelOpenAck steps after a dapp has called channelOpenInit + * @dev The fee amount that needs to be sent for Polymer to relay the whole channel handshake can be queried on the + * web2 layer. + * @param src The address of the sender, should be the address in the localportId. + * @param version The version string of the channel, the same argument as that sent in the + * dispatcher.channelOpenInit call + * @param ordering The ordering of the channel, the same argument as that sent in the dispatcher.channelOpenInit + * call + * @param connectionHops An array of connection hops, the same argument as that sent in the + * dispatcher.channelOpenInit call + * @param counterpartyPortId The counterparty port identifier, the same argument as that sent in the + * dispatcher.channelOpenInit call + */ + function depositOpenChannelFee( + address src, + string memory version, + ChannelOrder ordering, + string[] calldata connectionHops, + string calldata counterpartyPortId + ) external payable nonReentrant { + if (msg.value == 0) { + revert NoFeeSent(); + } + emit OpenChannelFeeDeposited(src, version, ordering, connectionHops, counterpartyPortId, msg.value); + } + + /** + * @notice Withdraws all collected fees to the contract owner's address. + * @dev Transfers the entire balance of this contract to the owner. + * @dev Anyone can call this, but it will always only be sent to the owner. + */ + function withdrawFeesToOwner() external { + payable(owner()).transfer(address(this).balance); + } +} diff --git a/contracts/core/UniversalChannelHandler.sol b/contracts/core/UniversalChannelHandler.sol index e14736df..7de73e99 100644 --- a/contracts/core/UniversalChannelHandler.sol +++ b/contracts/core/UniversalChannelHandler.sol @@ -19,10 +19,11 @@ pragma solidity 0.8.15; import {IbcDispatcher} from "../interfaces/IbcDispatcher.sol"; import {IbcUniversalChannelMW, IbcUniversalPacketReceiver} from "../interfaces/IbcMiddleware.sol"; -import {IbcReceiverBaseUpgradeable} from "../interfaces/IbcReceiverUpgradeable.sol"; +import {IbcReceiverBaseUpgradeable} from "../implementation_templates/IbcReceiverUpgradeable.sol"; import {ChannelOrder, IbcPacket, AckPacket, UniversalPacket} from "../libs/Ibc.sol"; import {IbcUtils} from "../libs/IbcUtils.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {FeeSender} from "../implementation_templates/FeeSender.sol"; /** * @title Universal Channel Handler @@ -31,7 +32,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeab * channel handshake to establish a channel. * @dev This contract can integrate directly with dapps, or a middleware stack for packet routing. */ -contract UniversalChannelHandler is IbcReceiverBaseUpgradeable, UUPSUpgradeable, IbcUniversalChannelMW { +contract UniversalChannelHandler is IbcReceiverBaseUpgradeable, FeeSender, UUPSUpgradeable, IbcUniversalChannelMW { bytes32 private _UNUSED; // Storage placeholder to ensure upgrade from this version is backwards compatible string public constant VERSION = "1.0"; @@ -82,12 +83,38 @@ contract UniversalChannelHandler is IbcReceiverBaseUpgradeable, UUPSUpgradeable, bytes32 destPortAddr, bytes calldata appData, uint64 timeoutTimestamp - ) external { + ) external returns (uint64 sequence) { bytes memory packetData = IbcUtils.toUniversalPacketBytes( UniversalPacket(IbcUtils.toBytes32(msg.sender), MW_ID, destPortAddr, appData) ); emit UCHPacketSent(msg.sender, destPortAddr); - dispatcher.sendPacket(channelId, packetData, timeoutTimestamp); + sequence = dispatcher.sendPacket(channelId, packetData, timeoutTimestamp); + } + + /** + * @notice Sends a universal packet over an IBC channel + * @param channelId The channel ID through which the packet is sent on the dispatcher + * @param destPortAddr The destination port address + * @param appData The packet data to be sent + * @param timeoutTimestamp of when the packet can timeout + */ + function sendUniversalPacketWithFee( + bytes32 channelId, + bytes32 destPortAddr, + bytes calldata appData, + uint64 timeoutTimestamp, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) external payable returns (uint64 sequence) { + // Cache dispatcher for gas savings + IbcDispatcher _dispatcher = dispatcher; + + bytes memory packetData = IbcUtils.toUniversalPacketBytes( + UniversalPacket(IbcUtils.toBytes32(msg.sender), MW_ID, destPortAddr, appData) + ); + emit UCHPacketSent(msg.sender, destPortAddr); + sequence = _dispatcher.sendPacket(channelId, packetData, timeoutTimestamp); + _depositSendPacketFee(dispatcher, channelId, sequence, gasLimits, gasPrices); } /** diff --git a/contracts/examples/Earth.sol b/contracts/examples/Earth.sol index 2e28abd2..be53099a 100644 --- a/contracts/examples/Earth.sol +++ b/contracts/examples/Earth.sol @@ -20,6 +20,7 @@ pragma solidity ^0.8.9; import {UniversalPacket, AckPacket} from "../libs/Ibc.sol"; import {IbcUtils} from "../libs/IbcUtils.sol"; import {IbcUniversalPacketReceiverBase, IbcUniversalPacketSender} from "../interfaces/IbcMiddleware.sol"; +import {IUniversalChannelHandler} from "../interfaces/IUniversalChannelHandler.sol"; /** * @title Earth @@ -48,12 +49,33 @@ contract Earth is IbcUniversalPacketReceiverBase { constructor(address _middleware) IbcUniversalPacketReceiverBase(_middleware) {} + /** + * @notice Send a packet to a destination chain. without a fee + * @notice this is useful for self-relaying apckets which don't rely on polymer to fund. + * @param destPortAddr The destination chain's port address. + * @param channelId The channel id to send the packet on. + * @param message The message to send. + * @param timeoutTimestamp The timeout timestamp for the packet. + */ function greet(address destPortAddr, bytes32 channelId, bytes calldata message, uint64 timeoutTimestamp) external { IbcUniversalPacketSender(mw).sendUniversalPacket( channelId, IbcUtils.toBytes32(destPortAddr), message, timeoutTimestamp ); } + function greetWithFee( + address destPortAddr, + bytes32 channelId, + bytes calldata message, + uint64 timeoutTimestamp, + uint256[2] memory gasLimits, + uint256[2] memory gasPrices + ) external payable returns (uint64 sequence) { + return IUniversalChannelHandler(mw).sendUniversalPacketWithFee{value: msg.value}( + channelId, IbcUtils.toBytes32(destPortAddr), message, timeoutTimestamp, gasLimits, gasPrices + ); + } + function onRecvUniversalPacket(bytes32 channelId, UniversalPacket calldata packet) external onlyIbcMw diff --git a/contracts/examples/Mars.sol b/contracts/examples/Mars.sol index 5a9f4047..fea2ef52 100644 --- a/contracts/examples/Mars.sol +++ b/contracts/examples/Mars.sol @@ -20,6 +20,7 @@ pragma solidity ^0.8.9; import {AckPacket, ChannelOrder} from "../libs/Ibc.sol"; import {IbcReceiverBase, IbcReceiver, IbcPacket} from "../interfaces/IbcReceiver.sol"; import {IbcDispatcher} from "../interfaces/IbcDispatcher.sol"; +import {FeeSender} from "../implementation_templates/FeeSender.sol"; /** * @title Mars @@ -27,7 +28,7 @@ import {IbcDispatcher} from "../interfaces/IbcDispatcher.sol"; * @dev This contract is used for only testing IBC functionality and as an example for dapp developers on how to * integrate with the vibc protocol. */ -contract Mars is IbcReceiverBase, IbcReceiver { +contract Mars is IbcReceiverBase, IbcReceiver, FeeSender { // received packet as chain B IbcPacket[] public recvedPackets; // received ack packet as chain A @@ -50,6 +51,18 @@ contract Mars is IbcReceiverBase, IbcReceiver { dispatcher.channelOpenInit(version, ordering, feeEnabled, connectionHops, counterpartyPortId); } + function triggerChannelInitWithFee( + string calldata version, + ChannelOrder ordering, + bool feeEnabled, + string[] calldata connectionHops, + string calldata counterpartyPortId + ) external payable onlyOwner { + IbcDispatcher _dispatcher = dispatcher; // cache for gas savings to avoid 2 SLOADS + _dispatcher.channelOpenInit(version, ordering, feeEnabled, connectionHops, counterpartyPortId); + _depositOpenChannelFee(_dispatcher, version, ordering, connectionHops, counterpartyPortId); + } + function onRecvPacket(IbcPacket memory packet) external virtual @@ -92,13 +105,36 @@ contract Mars is IbcReceiverBase, IbcReceiver { } /** - * @dev Sends a packet with a greeting message over a specified channel. + * @dev Sends a packet with a greeting message over a specified channel, without depositing any sendPacket relaying + * fees. + * @notice Use greetWithFee for sending packets with fees. * @param message The greeting message to be sent. * @param channelId The ID of the channel to send the packet to. * @param timeoutTimestamp The timestamp at which the packet will expire if not received. + * @dev This method also returns sequence from the dispatcher for easy testing */ - function greet(string calldata message, bytes32 channelId, uint64 timeoutTimestamp) external { - dispatcher.sendPacket(channelId, bytes(message), timeoutTimestamp); + function greet(string calldata message, bytes32 channelId, uint64 timeoutTimestamp) + external + returns (uint64 sequence) + { + sequence = dispatcher.sendPacket(channelId, bytes(message), timeoutTimestamp); + } + + /** + * @dev Sends a packet with a greeting message over a specified channel, and deposits a fee for relaying the packet + * @param message The greeting message to be sent. + * @param channelId The ID of the channel to send the packet to. + * @param timeoutTimestamp The timestamp at which the packet will expire if not received. + */ + function greetWithFee( + string calldata message, + bytes32 channelId, + uint64 timeoutTimestamp, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) external payable returns (uint64 sequence) { + sequence = dispatcher.sendPacket(channelId, bytes(message), timeoutTimestamp); + _depositSendPacketFee(dispatcher, channelId, sequence, gasLimits, gasPrices); } function onChanOpenInit(ChannelOrder, string[] calldata, string calldata, string calldata version) diff --git a/contracts/implementation_templates/FeeSender.sol b/contracts/implementation_templates/FeeSender.sol new file mode 100644 index 00000000..06e28006 --- /dev/null +++ b/contracts/implementation_templates/FeeSender.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright 2024, Polymer Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity 0.8.15; + +import {IbcDispatcher} from "../interfaces/IbcDispatcher.sol"; +import {ChannelOrder} from "../libs/Ibc.sol"; + +/// Contract with +abstract contract FeeSender { + function _depositSendPacketFee( + IbcDispatcher dispatcher, + bytes32 channelId, + uint64 sequence, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) internal { + dispatcher.feeVault().depositSendPacketFee{value: msg.value}(channelId, sequence, gasLimits, gasPrices); + } + + function _depositOpenChannelFee( + IbcDispatcher dispatcher, + string memory version, + ChannelOrder ordering, + string[] calldata connectionHops, + string calldata counterpartyPortId + ) internal { + dispatcher.feeVault().depositOpenChannelFee{value: msg.value}( + address(this), version, ordering, connectionHops, counterpartyPortId + ); + } +} diff --git a/contracts/interfaces/IbcReceiverUpgradeable.sol b/contracts/implementation_templates/IbcReceiverUpgradeable.sol similarity index 96% rename from contracts/interfaces/IbcReceiverUpgradeable.sol rename to contracts/implementation_templates/IbcReceiverUpgradeable.sol index fc12d784..dfd01d7c 100644 --- a/contracts/interfaces/IbcReceiverUpgradeable.sol +++ b/contracts/implementation_templates/IbcReceiverUpgradeable.sol @@ -18,12 +18,13 @@ pragma solidity ^0.8.9; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {IbcDispatcher} from "./IbcDispatcher.sol"; +import {IbcDispatcher} from "../interfaces/IbcDispatcher.sol"; contract IbcReceiverBaseUpgradeable is OwnableUpgradeable { IbcDispatcher public dispatcher; error notIbcDispatcher(); + error invalidAddress(); error UnsupportedVersion(); error ChannelNotFound(); diff --git a/contracts/interfaces/IDispatcher.sol b/contracts/interfaces/IDispatcher.sol index 4f6d064d..43e052c9 100644 --- a/contracts/interfaces/IDispatcher.sol +++ b/contracts/interfaces/IDispatcher.sol @@ -21,6 +21,7 @@ import {IbcDispatcher, IbcEventsEmitter} from "./IbcDispatcher.sol"; import {L1Header, OpL2StateProof, Ics23Proof} from "./IProofVerifier.sol"; import {Channel, ChannelEnd, ChannelOrder, IbcPacket} from "../libs/Ibc.sol"; import {ILightClient} from "./ILightClient.sol"; +import {IFeeVault} from "./IFeeVault.sol"; interface IDispatcher is IbcDispatcher, IbcEventsEmitter { function setPortPrefix(string calldata _portPrefix) external; @@ -105,6 +106,7 @@ interface IDispatcher is IbcDispatcher, IbcEventsEmitter { function writeTimeoutPacket(IbcPacket calldata packet, Ics23Proof calldata proof) external; function recvPacket(IbcPacket calldata packet, Ics23Proof calldata proof) external; + function feeVault() external returns (IFeeVault feeVault); function getOptimisticConsensusState(uint256 height, string calldata connection) external diff --git a/contracts/interfaces/IFeeVault.sol b/contracts/interfaces/IFeeVault.sol new file mode 100644 index 00000000..2948c556 --- /dev/null +++ b/contracts/interfaces/IFeeVault.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright 2024, Polymer Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity 0.8.15; + +import {ChannelOrder} from "../libs/Ibc.sol"; + +struct GasFee { + uint256 gasLimit; + uint256 gasPrice; +} + +struct SendpacketFeeDeposited { + uint256[2] gasLimits; + uint256[2] gasPrices; +} + +interface IFeeVault { + event SendPacketFeeDeposited(bytes32 channelId, uint64 sequence, uint256[2] gasLimits, uint256[2] gasPrices); + event OpenChannelFeeDeposited( + address sourceAddress, + string version, + ChannelOrder ordering, + string[] connectionHops, + string counterpartyPortId, + uint256 feeAmount + ); + + error SenderNotDispatcher(); + error NoFeeSent(); + error IncorrectFeeSent(uint256 expected, uint256 sent); + + function depositSendPacketFee( + bytes32 channelId, + uint64 sequence, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) external payable; + + function depositOpenChannelFee( + address sender, + string memory version, + ChannelOrder ordering, + string[] calldata connectionHops, + string memory counterpartyPortId + ) external payable; + + function withdrawFeesToOwner() external; +} diff --git a/contracts/interfaces/IbcDispatcher.sol b/contracts/interfaces/IbcDispatcher.sol index 798019ed..6bff228a 100644 --- a/contracts/interfaces/IbcDispatcher.sol +++ b/contracts/interfaces/IbcDispatcher.sol @@ -19,6 +19,7 @@ pragma solidity ^0.8.9; import {Height, ChannelOrder, AckPacket} from "../libs/Ibc.sol"; import {Ics23Proof} from "./IProofVerifier.sol"; +import {IFeeVault} from "./IFeeVault.sol"; /** * @title IbcPacketSender @@ -49,7 +50,7 @@ interface IbcDispatcher is IbcPacketSender { function channelCloseConfirm(address portAddress, bytes32 channelId, Ics23Proof calldata proof) external; function channelCloseInit(bytes32 channelId) external; - + function feeVault() external returns (IFeeVault feeVault); function portPrefix() external view returns (string memory portPrefix); } diff --git a/contracts/interfaces/IbcMiddleware.sol b/contracts/interfaces/IbcMiddleware.sol index ef98768b..80a2df64 100644 --- a/contracts/interfaces/IbcMiddleware.sol +++ b/contracts/interfaces/IbcMiddleware.sol @@ -31,7 +31,16 @@ interface IbcUniversalPacketSender { bytes32 destPortAddr, bytes calldata appData, uint64 timeoutTimestamp - ) external; + ) external returns (uint64 sequence); + + function sendUniversalPacketWithFee( + bytes32 channelId, + bytes32 destPortAddr, + bytes calldata appData, + uint64 timeoutTimestamp, + uint256[2] calldata gasLimits, + uint256[2] calldata gasPrices + ) external payable returns (uint64 sequence); } interface IbcMwPacketSender { @@ -47,7 +56,7 @@ interface IbcMwPacketSender { uint256 srcMwIds, bytes calldata appData, uint64 timeoutTimestamp - ) external; + ) external returns (uint64 sequence); } // IBC middleware contracts must implement this interface to relay universal channel packets to other IBC middleware diff --git a/test/Dispatcher.gasGriefing.t.sol b/test/Dispatcher.gasGriefing.t.sol index 73c05d8e..9210203f 100644 --- a/test/Dispatcher.gasGriefing.t.sol +++ b/test/Dispatcher.gasGriefing.t.sol @@ -22,7 +22,7 @@ contract DispatcherGasGriefing is Base { ChannelEnd("polyibc.eth.71C95911E9a5D330f4D621842EC243EE1343292e", IbcUtils.toBytes32("channel-1"), "1.0"); function setUp() public override { - (dispatcherProxy, dispatcherImplementation) = TestUtilsTest.deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = TestUtilsTest.deployDispatcherProxyAndImpl(portPrefix, feeVault); gasUsingMars = new GasUsingMars(3_000_000, dispatcherProxy); // Set arbitrarily high gas useage in mars contract bytes32 connectionStr = bytes32(0x636f6e6e656374696f6e2d310000000000000000000000000000000000000018); // connection-1 // in hex diff --git a/test/Dispatcher/Dispatcher.client.t.sol b/test/Dispatcher/Dispatcher.client.t.sol index ee02dcc7..dd6bdc40 100644 --- a/test/Dispatcher/Dispatcher.client.t.sol +++ b/test/Dispatcher/Dispatcher.client.t.sol @@ -31,7 +31,7 @@ abstract contract DispatcherUpdateClientTestSuite is Base { contract DispatcherUpdateClientTest is DispatcherUpdateClientTestSuite { function setUp() public virtual override { - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix, feeVault); dispatcherProxy.setClientForConnection("connection-0", opLightClient); } } diff --git a/test/Dispatcher/Dispatcher.closeChannel.t.sol b/test/Dispatcher/Dispatcher.closeChannel.t.sol index af3ab54e..8647055c 100644 --- a/test/Dispatcher/Dispatcher.closeChannel.t.sol +++ b/test/Dispatcher/Dispatcher.closeChannel.t.sol @@ -21,11 +21,13 @@ contract DispatcherCloseChannelTest is PacketSenderTestBase { } function test_closeChannelInit_success() public { + vm.startPrank(mars.owner()); assertNotEq0(abi.encode(dispatcherProxy.getChannel(address(mars), channelId)), abi.encode(defaultChannel)); vm.expectEmit(true, true, true, true); emit ChannelCloseInit(address(mars), channelId); mars.triggerChannelClose(channelId); assertEq(abi.encode(dispatcherProxy.getChannel(address(mars), channelId)), abi.encode(defaultChannel)); + vm.stopPrank(); } function test_closeChannelInit_mustOwner() public { @@ -56,11 +58,20 @@ contract DispatcherCloseChannelTest is PacketSenderTestBase { } function test_sendPacket_afterChannelCloseInit() public { + vm.startPrank(mars.owner()); mars.triggerChannelClose(channelId); sentPacket = genPacket(nextSendSeq); ackPacket = genAckPacket(Ibc.toStr(nextSendSeq)); vm.expectRevert(IBCErrors.channelNotOwnedBySender.selector); mars.greet(payloadStr, channelId, maxTimeout); + + // Should also revert for fee enabled packets + vm.deal(address(this), totalSendPacketFees); + vm.expectRevert(IBCErrors.channelNotOwnedBySender.selector); + mars.greetWithFee{value: totalSendPacketFees}( + payloadStr, channelId, maxTimeout, sendPacketGasLimit, sendPacketGasPrice + ); + vm.stopPrank(); } function test_sendPacket_afterChannelCloseConfirm() public { @@ -69,6 +80,13 @@ contract DispatcherCloseChannelTest is PacketSenderTestBase { ackPacket = genAckPacket(Ibc.toStr(nextSendSeq)); vm.expectRevert(IBCErrors.channelNotOwnedBySender.selector); mars.greet(payloadStr, channelId, maxTimeout); + + // Should also revert for fee enabled packets + vm.deal(address(this), totalSendPacketFees); + vm.expectRevert(IBCErrors.channelNotOwnedBySender.selector); + mars.greetWithFee{value: totalSendPacketFees}( + payloadStr, channelId, maxTimeout, sendPacketGasLimit, sendPacketGasPrice + ); } } diff --git a/test/Dispatcher/Dispatcher.dappHandlerRevert.t.sol b/test/Dispatcher/Dispatcher.dappHandlerRevert.t.sol index c62f5d88..2ded0a7a 100644 --- a/test/Dispatcher/Dispatcher.dappHandlerRevert.t.sol +++ b/test/Dispatcher/Dispatcher.dappHandlerRevert.t.sol @@ -21,7 +21,7 @@ contract DappHandlerRevertTests is Base { ChannelEnd("polyibc.eth2.71C95911E9a5D330f4D621842EC243EE1343292e", IbcUtils.toBytes32("channel-1"), "1.0"); function setUp() public virtual override { - (dispatcherProxy, dispatcherImplementation) = TestUtilsTest.deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = TestUtilsTest.deployDispatcherProxyAndImpl(portPrefix, feeVault); dispatcherProxy.setClientForConnection(connectionHops0[0], dummyLightClient); dispatcherProxy.setClientForConnection(connectionHops1[0], dummyLightClient); dispatcherProxy.setClientForConnection(connectionHops[0], dummyLightClient); diff --git a/test/Dispatcher/Dispatcher.multiclient.sol b/test/Dispatcher/Dispatcher.multiclient.sol index 8008b544..e897728f 100644 --- a/test/Dispatcher/Dispatcher.multiclient.sol +++ b/test/Dispatcher/Dispatcher.multiclient.sol @@ -24,7 +24,7 @@ contract DispatcherRealProofMultiClient is Base { function setUp() public override { opLightClient = new OptimisticLightClient(1, opProofVerifier, l1BlockProvider); dummyLightClient = new DummyLightClient(); - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl("polyibc.eth1."); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl("polyibc.eth1.", feeVault); dispatcherProxy.setClientForConnection(connectionHops0[0], dummyLightClient); dispatcherProxy.setClientForConnection(connectionHops1[0], opLightClient); address targetMarsAddress = 0x71C95911E9a5D330f4D621842EC243EE1343292e; diff --git a/test/Dispatcher/Dispatcher.proof.t.sol b/test/Dispatcher/Dispatcher.proof.t.sol index 54a7aebc..6e4bbe2f 100644 --- a/test/Dispatcher/Dispatcher.proof.t.sol +++ b/test/Dispatcher/Dispatcher.proof.t.sol @@ -32,6 +32,26 @@ abstract contract DispatcherIbcWithRealProofsSuite is IbcEventsEmitter, Base { dispatcherProxy.channelOpenInit(ch1.version, ChannelOrder.NONE, false, connectionHops1, ch1.portId); } + function test_ibc_channel_open_init_WithFee() public { + vm.deal(address(mars), totalOpenChannelFees); + uint256 startingBal = address(feeVault).balance; + + vm.expectEmit(true, true, true, true, address(dispatcherProxy)); + emit ChannelOpenInit(address(mars), "1.0", ChannelOrder.NONE, false, connectionHops1, ch1.portId); + + vm.expectEmit(true, true, true, true, address(feeVault)); + emit OpenChannelFeeDeposited( + address(mars), "1.0", ChannelOrder.NONE, connectionHops1, ch1.portId, totalOpenChannelFees + ); + // since this is open chann init, the proof is not used. so use an invalid one + mars.triggerChannelInitWithFee{value: totalOpenChannelFees}( + "1.0", ChannelOrder.NONE, false, connectionHops1, ch1.portId + ); + + vm.stopPrank(); + assertEq(address(feeVault).balance, startingBal + totalOpenChannelFees); + } + function test_ibc_channel_open_try() public { Ics23Proof memory proof = load_proof("/test/payload/channel_try_pending_proof.hex"); @@ -139,7 +159,7 @@ contract DispatcherIbcWithRealProofs is DispatcherIbcWithRealProofsSuite { string memory portPrefix1 = "polyibc.eth1."; opLightClient = new OptimisticLightClient(1, opProofVerifier, l1BlockProvider); - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix1); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix1, feeVault); dispatcherProxy.setClientForConnection(connectionHops0[0], opLightClient); dispatcherProxy.setClientForConnection(connectionHops0[1], opLightClient); dispatcherProxy.setClientForConnection(connectionHops1[0], opLightClient); diff --git a/test/Dispatcher/Dispatcher.t.sol b/test/Dispatcher/Dispatcher.t.sol index 38d74b8b..aa128343 100644 --- a/test/Dispatcher/Dispatcher.t.sol +++ b/test/Dispatcher/Dispatcher.t.sol @@ -103,6 +103,21 @@ abstract contract ChannelHandshakeTestSuite is ChannelHandshakeUtils { channelOpenConfirm(le, re, settings[i], true); } } + + // Should also be able to do the same with fee-enabled channels + for (uint256 i = 0; i < settings.length; i++) { + for (uint256 j = 0; j < versions.length; j++) { + LocalEnd memory le = _local; + ChannelEnd memory re = _remote; + le.versionCall = versions[j]; + le.versionExpected = versions[j]; + re.version = versions[j]; + channelOpenInitWithFee(le, re, settings[i], true); + channelOpenTry(le, re, settings[i], true); + channelOpenAck(le, re, settings[i], true); + channelOpenConfirm(le, re, settings[i], true); + } + } } function test_openChannel_initiator_revert_unsupportedVersion() public { @@ -183,7 +198,7 @@ abstract contract ChannelHandshakeTestSuite is ChannelHandshakeUtils { contract ChannelHandshakeTest is ChannelHandshakeTestSuite { function setUp() public virtual override { - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix, feeVault); dispatcherProxy.setClientForConnection(connectionHops[0], dummyLightClient); mars = new Mars(dispatcherProxy); portId = IbcUtils.addressToPortId(portPrefix, address(mars)); @@ -208,7 +223,7 @@ contract ChannelOpenTestBaseSetup is Base { RevertingBytesMars revertingBytesMars; function setUp() public virtual override { - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix, feeVault); dispatcherProxy.setClientForConnection(connectionHops[0], dummyLightClient); ChannelHandshakeSetting memory setting = ChannelHandshakeSetting(ChannelOrder.ORDERED, feeEnabled, true, validProof); @@ -233,6 +248,7 @@ contract ChannelOpenTestBaseSetup is Base { channelOpenAck(_local, _remote, setting, true); channelOpenConfirm(_localRevertingMars, _remote, setting, true); + vm.stopPrank(); } } @@ -288,10 +304,32 @@ contract PacketSenderTestBase is ChannelOpenTestBaseSetup { function sendPacket() internal { sentPacket = genPacket(nextSendSeq); ackPacket = genAckPacket(Ibc.toStr(nextSendSeq)); - mars.greet(payloadStr, channelId, maxTimeout); + + uint256 currentSeqSend; + // Test both fee and non-fee incentivized packet sending by switching off every other nextSendSeq. + if (nextSendSeq % 2 == 0) { + currentSeqSend = _doFeeSendPacket(); + } else { + currentSeqSend = mars.greet(payloadStr, channelId, maxTimeout); + } + + assertEq(nextSendSeq, currentSeqSend); nextSendSeq += 1; } + function _doFeeSendPacket() internal returns (uint64 currentSeqSend) { + uint256 beforeBalance = address(feeVault).balance; + + vm.deal(address(this), totalSendPacketFees); + vm.expectEmit(true, true, true, true, address(dispatcherProxy)); + emit SendPacket(address(mars), channelId, payload, nextSendSeq, maxTimeout); + // Test both fee and non-fee incentivized packet sending by switching off every other nextSendSeq. + currentSeqSend = mars.greetWithFee{value: totalSendPacketFees}( + payloadStr, channelId, maxTimeout, sendPacketGasLimit, sendPacketGasPrice + ); + assertEq(address(feeVault).balance, beforeBalance + totalSendPacketFees); + } + // genPacket generates a packet for the given packet sequence function genPacket(uint64 packetSeq) internal view returns (IbcPacket memory) { return IbcPacket(src, dest, packetSeq, payload, ZERO_HEIGHT, maxTimeout); diff --git a/test/FeeVault.t.sol b/test/FeeVault.t.sol new file mode 100644 index 00000000..70fc5c18 --- /dev/null +++ b/test/FeeVault.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {ChannelOpenTestBaseSetup} from "./Dispatcher/Dispatcher.t.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import "forge-std/Test.sol"; + +contract FeeVaultTest is ChannelOpenTestBaseSetup { + address notOwner = vm.addr(2); + address owner; + Ownable feeVaultOwnable; // FeeVault address but from Ownable interface + uint256 feePerGreet = 600_000 * 60 gwei + 700_000 * 70 gwei; + + function setUp() public override { + super.setUp(); + + feeVaultOwnable = Ownable(address(feeVault)); + greetMarsWithFee(); + } + + // Fuzz test for always reverting if incorrect fee is sent. + function testFuzz_sendPacket_FeesAddUpToValue( + uint104 gasFee1, + uint104 gasFee2, + uint104 gasLimit1, + uint104 gasLimit2, + bool shouldRevert, + uint56 fuzz + ) public { + gasFee1 = uint104(bound(gasFee1, 1, 2 ** 104 - 1)); + gasLimit1 = uint104(bound(gasLimit1, 1, 2 ** 104 - 1)); + fuzz = uint56(bound(fuzz, 1, 2 ** 56 - 1)); + fuzz = uint56(bound(fuzz, 1, gasFee1)); + + uint256 feesToSend = uint256(gasFee1) * uint256(gasLimit1) + uint256(gasFee2) * uint256(gasLimit2); + vm.deal(address(this), feesToSend + fuzz); + + if (shouldRevert) { + // Fuzz the fees to make sure it reverts + if (fuzz % 2 == 0) { + feesToSend += uint256(fuzz); + } else { + feesToSend -= uint256(fuzz); + } + vm.expectRevert(); + } + feeVault.depositSendPacketFee{value: feesToSend}( + channelId, uint64(1), [uint256(gasFee1), uint256(gasFee2)], [uint256(gasLimit1), uint256(gasLimit2)] + ); + } + + function test_withdrawAlwaysGoesToOwner() public { + assertEq(address(feeVault).balance, feePerGreet); + uint256 startingBalance = address(this).balance; + feeVault.withdrawFeesToOwner(); + assertEq(address(this).balance, startingBalance + feePerGreet); + + greetMarsWithFee(); // send more fees to feeVault + // Non Owners can call but it should sill go to the owner (i.e. this address) + vm.prank(notOwner); + feeVault.withdrawFeesToOwner(); + assertEq(address(feeVault).balance, 0); + assertEq(address(this).balance, startingBalance + (feePerGreet * 2)); + } + + function greetMarsWithFee() internal { + vm.deal(address(mars), feePerGreet); + vm.prank(address(mars)); + vm.expectEmit(false, true, true, false, address(feeVault)); // Ignore the emitted seuqence since we don't know + // what it will be before we call sendpacket (vm.expect emit assumes the first event will always be + // chedcked, so to avoid checking second argument we pass false for the first argument) + emit SendPacketFeeDeposited( + channelId, 1, [uint256(600_000), uint256(700_000)], [uint256(60 gwei), uint256(70 gwei)] + ); + mars.greetWithFee{value: feePerGreet}( + "hello", channelId, maxTimeout, [uint256(600_000), uint256(700_000)], [uint256(60 gwei), uint256(70 gwei)] + ); + } + + // This test contract needs to have a receive function to accept funds sent from the FeeVault + // Note: our multisig should have a fallback to accept funds in general + receive() external payable {} +} diff --git a/test/VirtualChain.sol b/test/VirtualChain.sol index 04ed56cd..a5783dab 100644 --- a/test/VirtualChain.sol +++ b/test/VirtualChain.sol @@ -6,12 +6,14 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import {IbcDispatcher, IbcEventsEmitter} from "../contracts/interfaces/IbcDispatcher.sol"; import {IUniversalChannelHandler} from "../contracts/interfaces/IUniversalChannelHandler.sol"; import {IDispatcher} from "../contracts/interfaces/IDispatcher.sol"; +import {IFeeVault} from "../contracts/interfaces/IFeeVault.sol"; import "../contracts/libs/Ibc.sol"; import {IbcUtils} from "../contracts/libs/IbcUtils.sol"; import {Dispatcher} from "../contracts/core/Dispatcher.sol"; import {IbcChannelReceiver, IbcPacketReceiver} from "../contracts/interfaces/IbcReceiver.sol"; import "../contracts/interfaces/IProofVerifier.sol"; import {UniversalChannelHandler} from "../contracts/core/UniversalChannelHandler.sol"; +import {FeeVault} from "../contracts/core/FeeVault.sol"; import {Mars} from "../contracts/examples/Mars.sol"; import {Earth} from "../contracts/examples/Earth.sol"; import {GeneralMiddleware} from "../contracts/base/GeneralMiddleware.sol"; @@ -35,6 +37,7 @@ struct VirtualChainData { GeneralMiddleware mw1; GeneralMiddleware mw2; string[] connectionHops; + IFeeVault feeVault; } // A test contract that keeps two types of dApps, 1. regular IBC-enabled dApp, 2. universal channel dApp @@ -42,6 +45,7 @@ contract VirtualChain is Test, IbcEventsEmitter, TestUtilsTest { IDispatcher public dispatcherProxy; Dispatcher public dispatcherImplementation; IUniversalChannelHandler public ucHandlerProxy; + IFeeVault public feeVault; GeneralMiddleware public mw1; GeneralMiddleware public mw2; @@ -60,8 +64,9 @@ contract VirtualChain is Test, IbcEventsEmitter, TestUtilsTest { // ChannelIds are not initialized until channel handshake is started constructor(uint256 seed, string memory portPrefix) { _seed = seed; + feeVault = new FeeVault(); - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix, feeVault); (ucHandlerProxy,) = deployUCHProxyAndImpl(address(dispatcherProxy)); mars = new Mars(dispatcherProxy); @@ -82,7 +87,7 @@ contract VirtualChain is Test, IbcEventsEmitter, TestUtilsTest { // return virtualChainData function getVirtualChainData() external view returns (VirtualChainData memory) { - return VirtualChainData(dispatcherProxy, ucHandlerProxy, mars, earth, mw1, mw2, connectionHops); + return VirtualChainData(dispatcherProxy, ucHandlerProxy, mars, earth, mw1, mw2, connectionHops, feeVault); } // expectedChannel returns a Channel struct with expected values diff --git a/test/universal.channel.t.sol b/test/universal.channel.t.sol index ab4a0e9a..bd42ce0c 100644 --- a/test/universal.channel.t.sol +++ b/test/universal.channel.t.sol @@ -12,6 +12,7 @@ import "../contracts/interfaces/IbcMiddleware.sol"; import "../contracts/core/OptimisticLightClient.sol"; import "./utils/Dispatcher.base.t.sol"; import "./VirtualChain.sol"; +import {IFeeVault} from "../contracts/interfaces/IFeeVault.sol"; contract UniversalChannelTest is Base { function test_channel_settings_ok() public { @@ -243,7 +244,7 @@ contract UniversalChannelPacketTest is Base, IbcMwEventsEmitter { function test_uch_new_dispatcher_set_ok() public { IUniversalChannelHandler uch = eth1.ucHandlerProxy(); vm.startPrank(address(eth1)); // Prank eth1 since that address is the owner - (IDispatcher newDispatcher,) = deployDispatcherProxyAndImpl("polyibc.new."); + (IDispatcher newDispatcher,) = deployDispatcherProxyAndImpl("polyibc.new.", feeVault); assertFalse( address(uch.dispatcher()) == address(newDispatcher), "new dispatcher in uch test not setup correctly" ); @@ -256,7 +257,7 @@ contract UniversalChannelPacketTest is Base, IbcMwEventsEmitter { IUniversalChannelHandler uch = eth1.ucHandlerProxy(); address notOwner = vm.addr(1); vm.startPrank(notOwner); - (IDispatcher newDispatcher,) = deployDispatcherProxyAndImpl("polyibc.new."); + (IDispatcher newDispatcher,) = deployDispatcherProxyAndImpl("polyibc.new.", feeVault); vm.expectRevert("Ownable: caller is not the owner"); uch.setDispatcher(newDispatcher); @@ -460,9 +461,26 @@ contract UniversalChannelPacketTest is Base, IbcMwEventsEmitter { } // Verify event emitted by Dispatcher - vm.expectEmit(true, true, true, true); - emit SendPacket(address(v1.ucHandlerProxy), channelId1, packetData, packetSeq, timeout); - v1.earth.greet(address(v2.earth), channelId1, appData, timeout); + + // Alternate test non fee & fee + if (packetSeq % 2 == 1) { + vm.expectEmit(true, true, true, true); + emit SendPacket(address(v1.ucHandlerProxy), channelId1, packetData, packetSeq, timeout); + v1.earth.greet(address(v2.earth), channelId1, appData, timeout); + } else { + uint256 beforeBalance = address(v1.feeVault).balance; + vm.deal(address(this), totalSendPacketFees); + + vm.expectEmit(true, true, true, true, address(v1.dispatcherProxy)); + emit SendPacket(address(v1.ucHandlerProxy), channelId1, packetData, packetSeq, timeout); + vm.expectEmit(true, true, true, true, address(v1.feeVault)); + emit SendPacketFeeDeposited(channelId1, packetSeq, sendPacketGasLimit, sendPacketGasPrice); + uint64 sequence = v1.earth.greetWithFee{value: totalSendPacketFees}( + address(v2.earth), channelId1, appData, timeout, sendPacketGasLimit, sendPacketGasPrice + ); + assertEq(address(v1.feeVault).balance, beforeBalance + totalSendPacketFees); + assertEq(sequence, packetSeq); + } // simulate relayer calling dispatcherProxy.recvPacket on chain B // recvPacket is an IBC packet diff --git a/test/upgradeableProxy/Dispatcher.upgrade.t.sol b/test/upgradeableProxy/Dispatcher.upgrade.t.sol index 2afe77b2..937bc687 100644 --- a/test/upgradeableProxy/Dispatcher.upgrade.t.sol +++ b/test/upgradeableProxy/Dispatcher.upgrade.t.sol @@ -24,12 +24,15 @@ import {IbcReceiver, IbcChannelReceiver} from "../../contracts/interfaces/IbcRec import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {OptimisticLightClient} from "../../contracts/core/OptimisticLightClient.sol"; import {IProofVerifier} from "../../contracts/core/OptimisticProofVerifier.sol"; +import {IFeeVault} from "../../contracts/interfaces/IFeeVault.sol"; +import {FeeVault} from "../../contracts/core/FeeVault.sol"; import {DummyLightClient} from "../../contracts/utils/DummyLightClient.sol"; import {IDispatcher} from "../../contracts/interfaces/IDispatcher.sol"; import {UniversalChannelHandler} from "../../contracts/core/UniversalChannelHandler.sol"; import {IUniversalChannelHandler} from "../../contracts/interfaces/IUniversalChannelHandler.sol"; -import {DispatcherRc4} from "./upgrades/DispatcherRc4.sol"; +import {DispatcherRc4, IDispatcherRc4} from "./upgrades/DispatcherRc4.sol"; +import {Mars as MarsRc4, IbcDispatcher as IbcDispatcherRc4} from "./upgrades/MarsRc4.sol"; import {UniversalChannelHandlerV2} from "./upgrades/UCHV2.sol"; import {DispatcherV2Initializable} from "./upgrades/DispatcherV2Initializable.sol"; import {DispatcherV2} from "./upgrades/DispatcherV2.sol"; @@ -41,13 +44,13 @@ abstract contract UpgradeTestUtils { LocalEnd _localDummy; ChannelEnd _remoteDummy; - function upgradeDispatcher(string memory portPrefix, address dispatcherProxy) + function upgradeDispatcher(string memory portPrefix, IFeeVault feeVault, address dispatcherProxy) public returns (DispatcherV2Initializable newDispatcherImplementation) { // Upgrade dispatcherProxy for tests newDispatcherImplementation = new DispatcherV2Initializable(); - bytes memory initData = abi.encodeWithSignature("initialize(string)", portPrefix); + bytes memory initData = abi.encodeWithSignature("initialize(string,address)", portPrefix, feeVault); UUPSUpgradeable(dispatcherProxy).upgradeToAndCall(address(newDispatcherImplementation), initData); } @@ -58,12 +61,12 @@ abstract contract UpgradeTestUtils { function deployDispatcherRC4ProxyAndImpl(string memory initPortPrefix, ILightClient initLightClient) public - returns (IDispatcher proxy) + returns (IbcDispatcherRc4 proxy) { DispatcherRc4 dispatcherImplementation = new DispatcherRc4(); bytes memory initData = abi.encodeWithSelector(DispatcherRc4.initialize.selector, initPortPrefix, initLightClient); - proxy = IDispatcher(address(new ERC1967Proxy(address(dispatcherImplementation), initData))); + proxy = IbcDispatcherRc4(address(new ERC1967Proxy(address(dispatcherImplementation), initData))); } function deployUCHV2ProxyAndImpl(address dispatcherProxy) public returns (IUniversalChannelHandler proxy) { @@ -171,7 +174,7 @@ contract ChannelHandShakeUpgradeUtil is ChannelHandshakeUtils { contract DispatcherUpgradeTest is ChannelHandShakeUpgradeUtil, UpgradeTestUtils { function setUp() public override { address targetMarsAddress = 0x71C95911E9a5D330f4D621842EC243EE1343292e; - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix, feeVault); deployCodeTo("contracts/examples/Mars.sol:Mars", abi.encode(address(dispatcherProxy)), targetMarsAddress); dispatcherProxy.setClientForConnection(connectionHops[0], dummyLightClient); mars = new Mars(dispatcherProxy); @@ -184,8 +187,9 @@ contract DispatcherUpgradeTest is ChannelHandShakeUpgradeUtil, UpgradeTestUtils doChannelHandshake(_local, _remote); sendPacket(_local.channelId); + IFeeVault newFeeVault = new FeeVault(); // Upgrade dispatcherProxy for tests - upgradeDispatcher("adfsafsa", address(dispatcherProxy)); + upgradeDispatcher("adfsafsa", newFeeVault, address(dispatcherProxy)); } function test_SentPacketState_Conserved() public { diff --git a/test/upgradeableProxy/DispatcherRC4.upgrade.t.sol b/test/upgradeableProxy/DispatcherRC4.upgrade.t.sol index 91af2580..f33bc3b5 100644 --- a/test/upgradeableProxy/DispatcherRC4.upgrade.t.sol +++ b/test/upgradeableProxy/DispatcherRC4.upgrade.t.sol @@ -66,13 +66,16 @@ contract DispatcherRC4UpgradeTest is DispatcherRC4TestUtils, UpgradeTestUtils { function setUp() public override { // In Rc4 version, there can only be one dispatcher per light client so we deploy multiple clients // Deploy dummy old dispathcer - oldDummyDispatcherProxy = IbcDispatcherRc4(address(deployDispatcherRC4ProxyAndImpl(portPrefix, dummyLightClient))); // we have to manually cast here because solidity is confused by having interfaces coming from seperate files + oldDummyDispatcherProxy = + IbcDispatcherRc4(address(deployDispatcherRC4ProxyAndImpl(portPrefix, dummyLightClient))); // we have to manually + // cast here because solidity is confused by having interfaces coming from seperate files // Deploy op old dispatcher DummyLightClient dummyLightClient2 = new DummyLightClient(); // dummyLightClient2 models the op light client in // prod - it will be the light client that is chosen for the upgrade (and the oldDummyDispatcherProxy will // be deprecated) - oldDispatcherInterface = IbcDispatcherRc4(address(deployDispatcherRC4ProxyAndImpl(portPrefix, dummyLightClient2))); + oldDispatcherInterface = + IbcDispatcherRc4(address(deployDispatcherRC4ProxyAndImpl(portPrefix, dummyLightClient2))); dispatcherProxy = IDispatcher(address(oldDispatcherInterface)); uch = deployUCHV2ProxyAndImpl(address(dispatcherProxy)); earth = new EarthRc4(address(uch)); @@ -101,7 +104,7 @@ contract DispatcherRC4UpgradeTest is DispatcherRC4TestUtils, UpgradeTestUtils { earth.greet(address(sendingMars), _localUch.channelId, bytes("hello sendingMars"), UINT64_MAX); // Upgrade dispatcherProxy and uch for tests - upgradeDispatcher(portPrefix, address(dispatcherProxy)); + upgradeDispatcher(portPrefix, feeVault, address(dispatcherProxy)); upgradeUch(address(uch)); dispatcherProxy.setClientForConnection(connectionHops0[0], dummyLightClient2); dispatcherProxy.setClientForConnection(connectionHops1[0], dummyLightClient2); @@ -258,7 +261,7 @@ contract DispatcherRC4MidwayUpgradeTest is DispatcherRC4TestUtils, UpgradeTestUt // Test that channel handshake can be finished even if done during an upgrade function test_UpgradeBetween_ChannelOpen() public { - upgradeDispatcher(portPrefix, address(dispatcherProxy)); + upgradeDispatcher(portPrefix, feeVault, address(dispatcherProxy)); dispatcherProxy.setClientForConnection(connectionHops1[0], dummyLightClient2); ChannelHandshakeSetting memory setting = ChannelHandshakeSetting(ChannelOrder.ORDERED, false, true, validProof); channelOpenAck(_local, _remote, setting, true); @@ -282,7 +285,7 @@ contract DispatcherRC4MidwayUpgradeTest is DispatcherRC4TestUtils, UpgradeTestUt ); // Do upgrade before finishing packet handshake - upgradeDispatcher(portPrefix, address(dispatcherProxy)); + upgradeDispatcher(portPrefix, feeVault, address(dispatcherProxy)); upgradeUch(address(uch)); dispatcherProxy.setClientForConnection(connectionHops1[0], dummyLightClient2); // earth.authorizeChannel(_localUch.channelId); diff --git a/test/upgradeableProxy/DispatcherUUPS.accessControl.t.sol b/test/upgradeableProxy/DispatcherUUPS.accessControl.t.sol index ba7736ac..07c4d53e 100644 --- a/test/upgradeableProxy/DispatcherUUPS.accessControl.t.sol +++ b/test/upgradeableProxy/DispatcherUUPS.accessControl.t.sol @@ -5,6 +5,7 @@ import "../../contracts/libs/Ibc.sol"; import {Dispatcher} from "../../contracts/core/Dispatcher.sol"; import {IbcEventsEmitter} from "../../contracts/interfaces/IbcDispatcher.sol"; import {IbcReceiver} from "../../contracts/interfaces/IbcReceiver.sol"; +import {IFeeVault} from "../../contracts/interfaces/IFeeVault.sol"; import {Mars} from "../../contracts/examples/Mars.sol"; import "../../contracts/core/OptimisticLightClient.sol"; import "../utils/Dispatcher.base.t.sol"; @@ -21,7 +22,7 @@ contract DispatcherUUPSAccessControl is Base { DispatcherV2Initializable dispatcherImplementation3; function setUp() public override { - (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix); + (dispatcherProxy, dispatcherImplementation) = deployDispatcherProxyAndImpl(portPrefix, feeVault); dispatcherProxy.setClientForConnection(connectionHops[0], dummyLightClient); dispatcherImplementation2 = new DispatcherV2(); dispatcherImplementation3 = new DispatcherV2Initializable(); @@ -35,11 +36,13 @@ contract DispatcherUUPSAccessControl is Base { } function test_Dispatcher_Allows_Upgrade_To_And_Call() public { + IFeeVault newFeeVault = new FeeVault(); assertEq(address(dispatcherImplementation), getProxyImplementation(address(dispatcherProxy), vm)); - bytes memory initData = abi.encodeWithSignature("initialize(string)", portPrefix2); + bytes memory initData = abi.encodeWithSignature("initialize(string,address)", portPrefix2, newFeeVault); UUPSUpgradeable(address(dispatcherProxy)).upgradeToAndCall(address(dispatcherImplementation3), initData); assertEq(address(dispatcherImplementation3), getProxyImplementation(address(dispatcherProxy), vm)); assertEq(dispatcherProxy.portPrefix(), portPrefix2); + assertEq(address(dispatcherProxy.feeVault()), address(newFeeVault)); } function test_Dispatcher_Prevents_Non_Owner_Updgrade() public { @@ -55,6 +58,6 @@ contract DispatcherUUPSAccessControl is Base { function test_Dispatcher_Prevents_Reinit_Attacks() public { vm.expectRevert("Initializable: contract is already initialized"); - dispatcherImplementation.initialize("IIpolyibc.eth."); + dispatcherImplementation.initialize("IIpolyibc.eth.", IFeeVault(vm.addr(1))); } } diff --git a/test/upgradeableProxy/upgrades/DispatcherV2Initializable.sol b/test/upgradeableProxy/upgrades/DispatcherV2Initializable.sol index df60ed4d..a8bed48d 100644 --- a/test/upgradeableProxy/upgrades/DispatcherV2Initializable.sol +++ b/test/upgradeableProxy/upgrades/DispatcherV2Initializable.sol @@ -19,6 +19,7 @@ pragma solidity ^0.8.9; import {DispatcherV2} from "./DispatcherV2.sol"; import {ILightClient} from "../../../contracts/interfaces/ILightClient.sol"; +import {IFeeVault} from "../../../contracts/interfaces/IFeeVault.sol"; import {IBCErrors} from "../../../contracts/libs/IbcErrors.sol"; /** @@ -29,9 +30,10 @@ import {IBCErrors} from "../../../contracts/libs/IbcErrors.sol"; * which can be relayed to a rollup module on the Polymerase chain */ contract DispatcherV2Initializable is DispatcherV2 { - function initialize(string memory initPortPrefix) public override reinitializer(2) { + function initialize(string memory initPortPrefix, IFeeVault _feeVault) public override reinitializer(2) { __Ownable_init(); portPrefix = initPortPrefix; portPrefixLen = uint32(bytes(initPortPrefix).length); + feeVault = _feeVault; } } diff --git a/test/utils/Dispatcher.base.t.sol b/test/utils/Dispatcher.base.t.sol index 788b06c9..8e23285e 100644 --- a/test/utils/Dispatcher.base.t.sol +++ b/test/utils/Dispatcher.base.t.sol @@ -14,6 +14,8 @@ import "../../contracts/utils/DummyLightClient.sol"; import "../../contracts/core/OptimisticProofVerifier.sol"; import {TestUtilsTest} from "./TestUtils.t.sol"; import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {FeeVault} from "../../contracts/core/FeeVault.sol"; +import {IFeeVault} from "../../contracts/interfaces/IFeeVault.sol"; struct LocalEnd { IbcChannelReceiver receiver; @@ -36,6 +38,16 @@ struct ChannelHandshakeSetting { contract Base is IbcEventsEmitter, ProofBase, TestUtilsTest { using stdStorage for StdStorage; + event SendPacketFeeDeposited(bytes32 channelId, uint64 sequence, uint256[2] gasLimits, uint256[2] gasPrices); + event OpenChannelFeeDeposited( + address sourceAddress, + string version, + ChannelOrder ordering, + string[] connectionHops, + string counterpartyPortId, + uint256 fees + ); + uint32 CONNECTION_TO_CLIENT_ID_STARTING_SLOT = 161; uint32 SEND_PACKET_COMMITMENT_STARTING_SLOT = 156; uint64 UINT64_MAX = 18_446_744_073_709_551_615; @@ -45,12 +57,22 @@ contract Base is IbcEventsEmitter, ProofBase, TestUtilsTest { ILightClient opLightClient = new OptimisticLightClient(1800, opProofVerifier, l1BlockProvider); ILightClient dummyLightClient = new DummyLightClient(); + IFeeVault feeVault = new FeeVault(); IDispatcher public dispatcherProxy; Dispatcher public dispatcherImplementation; string portPrefix = "polyibc.eth."; string[] connectionHops = ["connection-1", "connection-2"]; + uint256 BASE_GAS_LIMIT = 1_000_000; + uint256 BASE_GAS_PRICE = 50 gwei; + uint256[2] public sendPacketGasLimit = [BASE_GAS_LIMIT, BASE_GAS_LIMIT]; + uint256[2] public sendPacketGasPrice = [BASE_GAS_PRICE, BASE_GAS_PRICE]; + uint256 public totalSendPacketFees = BASE_GAS_LIMIT * BASE_GAS_PRICE * 2; + + uint256 public totalOpenChannelFees = BASE_GAS_LIMIT * BASE_GAS_PRICE * 3; // The msg.value sent in a + // channelOpenInit call + // ⬇️ Utility functions for testing // getHexStr converts an address to a hex string without the leading 0x @@ -92,6 +114,46 @@ contract Base is IbcEventsEmitter, ProofBase, TestUtilsTest { vm.stopPrank(); } + /** + * @dev Step-1 of the 4-step handshake to open an IBC channel. + * @param le Local end settings for the channel. + * @param re Remote end settings for the channel. + * @param s Channel handshake settings. + * @param expPass Expected pass status of the operation. + * If expPass is false, `vm.expectRevert` should be called before this function. + */ + function channelOpenInitWithFee( + LocalEnd memory le, + ChannelEnd memory re, + ChannelHandshakeSetting memory s, + bool expPass + ) public { + uint256 beforeBalance = address(feeVault).balance; + vm.deal(address(le.receiver), totalOpenChannelFees); + + vm.startPrank(address(le.receiver)); + if (expPass) { + vm.expectEmit(true, true, true, true); + emit ChannelOpenInit( + address(le.receiver), le.versionExpected, s.ordering, s.feeEnabled, le.connectionHops, re.portId + ); + } + dispatcherProxy.channelOpenInit(le.versionCall, s.ordering, s.feeEnabled, le.connectionHops, re.portId); + if (expPass) { + vm.expectEmit(true, true, true, true, address(feeVault)); + emit OpenChannelFeeDeposited( + address(le.receiver), le.versionCall, s.ordering, connectionHops, re.portId, totalOpenChannelFees + ); + } + feeVault.depositOpenChannelFee{value: totalOpenChannelFees}( + address(le.receiver), le.versionCall, s.ordering, le.connectionHops, re.portId + ); + + assertEq(address(feeVault).balance, beforeBalance + totalOpenChannelFees); + + vm.stopPrank(); + } + /** * @dev Step-2 of the 4-step handshake to open an IBC channel. * @param le Local end settings for the channel. diff --git a/test/utils/TestUtils.t.sol b/test/utils/TestUtils.t.sol index e8fbd9e7..4a1fb04b 100644 --- a/test/utils/TestUtils.t.sol +++ b/test/utils/TestUtils.t.sol @@ -3,6 +3,7 @@ import {IDispatcher} from "../../contracts/interfaces/IDispatcher.sol"; import {IUniversalChannelHandler} from "../../contracts/interfaces/IUniversalChannelHandler.sol"; import {UniversalChannelHandler} from "../../contracts/core/UniversalChannelHandler.sol"; import {ILightClient} from "../../contracts/interfaces/ILightClient.sol"; +import {IFeeVault} from "../../contracts/interfaces/IFeeVault.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Dispatcher} from "../../contracts/core/Dispatcher.sol"; import {Test} from "forge-std/Test.sol"; @@ -15,7 +16,7 @@ abstract contract TestUtilsTest { bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - function deployDispatcherProxyAndImpl(string memory initPortPrefix) + function deployDispatcherProxyAndImpl(string memory initPortPrefix, IFeeVault feeVault) public returns (IDispatcher proxy, Dispatcher dispatcherImplementation) { @@ -24,7 +25,7 @@ abstract contract TestUtilsTest { address( new ERC1967Proxy( address(dispatcherImplementation), - abi.encodeWithSelector(Dispatcher.initialize.selector, initPortPrefix) + abi.encodeWithSelector(Dispatcher.initialize.selector, initPortPrefix, feeVault) ) ) );