From 13e66f1fefa8f2951bbf66522a04836ef11900b9 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Wed, 1 Nov 2023 11:03:23 +1100 Subject: [PATCH 01/49] WIP --- src/child/ChildAxelarBridgeAdaptor.sol | 21 ++++++--- src/child/ChildERC20Bridge.sol | 53 ++++++++++++++++++++++ src/interfaces/child/IChildERC20Bridge.sol | 6 +++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index fd0f983ac..4408bb77d 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -2,20 +2,29 @@ pragma solidity ^0.8.21; import {AxelarExecutable} from "@axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {IChildERC20Bridge} from "../interfaces/child/IChildERC20Bridge.sol"; import {IChildAxelarBridgeAdaptorErrors} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol"; -contract ChildAxelarBridgeAdaptor is AxelarExecutable, IChildAxelarBridgeAdaptorErrors { +contract ChildAxelarBridgeAdaptor is AxelarExecutable, Initializable, IChildAxelarBridgeAdaptorErrors { /// @notice Address of bridge to relay messages to. - IChildERC20Bridge public immutable CHILD_BRIDGE; + IChildERC20Bridge public childBridge; string public rootBridgeAdaptor; - constructor(address _gateway, address _childBridge) AxelarExecutable(_gateway) { + constructor(address _gateway) AxelarExecutable(_gateway) {} + + /** + * @notice Initializes the contract. + * @param _childBridge Address of the child bridge contract. + * @dev Always sets the rootBridgeAdaptor to whatever the rootERC20BridgeAdaptor of the bridge contract is. + */ + function initialize(address _childBridge) external initializer { if (_childBridge == address(0)) { revert ZeroAddress(); } - CHILD_BRIDGE = IChildERC20Bridge(_childBridge); + childBridge = IChildERC20Bridge(_childBridge); + rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); } // TODO tests for this @@ -25,7 +34,7 @@ contract ChildAxelarBridgeAdaptor is AxelarExecutable, IChildAxelarBridgeAdaptor * @dev Always sets it to whatever the rootERC20BridgeAdaptor of the bridge contract is. */ function setRootBridgeAdaptor() external { - rootBridgeAdaptor = CHILD_BRIDGE.rootERC20BridgeAdaptor(); + rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); } /** @@ -36,6 +45,6 @@ contract ChildAxelarBridgeAdaptor is AxelarExecutable, IChildAxelarBridgeAdaptor internal override { - CHILD_BRIDGE.onMessageReceive(sourceChain_, sourceAddress_, payload_); + childBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_); } } diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index dc40ed384..bc845ffdb 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -38,6 +38,7 @@ contract ChildERC20Bridge is bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address public constant NATIVE_ETH = address(0xeee); IChildERC20BridgeAdaptor public bridgeAdaptor; @@ -125,6 +126,58 @@ contract ChildERC20Bridge is } } + + function withdraw(IChildERC20 childToken, uint256 amount) external { + _withdraw(childToken, msg.sender, amount); + } + + function withdrawTo(IChildERC20 childToken, address receiver, uint256 amount) external { + _beforeTokenWithdraw(); + _withdraw(childToken, receiver, amount); + _afterTokenWithdraw(); + } + + function _withdraw(IChildERC20 childToken, address receiver, uint256 amount) private { + if (address(childToken).code.length == 0) { + revert EmptyTokenContract(); + } + + address rootToken = childToken.rootToken(); + + if (rootTokenToChildToken[rootToken] != address(childToken)) { + revert NotMapped(); + } + + // A mapped token should never a root token unset + if (rootToken == address(0)) { + revert ZeroAddressRootToken(); + } + + // A mapped token should never have the bridge unset + if (childToken.bridge() != address(this)) { + revert BrigeNotSet(); + } + + if (!childToken.burn(msg.sender, amount)) { + revert BurnFailed(); + } + + // Encode the message payload + bytes memory payload = abi.encode(WITHDRAW_SIG, rootToken, msg.sender, receiver, amount); + + // Send the message to the bridge adaptor and up to root chain + bridgeAdaptor.sendMessage(rootChain, rootERC20BridgeAdaptor, payload); + + if (address(childToken) == childETHToken) { + childToken.burn(msg.sender, amount); + Address.sendValue(payable(receiver), amount); + } else { + childToken.burn(msg.sender, amount); + IERC20Metadata rootToken = IERC20Metadata(childToken.rootToken()); + rootToken.safeTransfer(receiver, amount); + } + } + function _mapToken(bytes calldata data) private { (, address rootToken, string memory name, string memory symbol, uint8 decimals) = abi.decode(data, (bytes32, address, string, string, uint8)); diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 90bffc196..0608d1aa7 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -70,4 +70,10 @@ interface IChildERC20BridgeErrors { error InvalidSourceChain(); /// @notice Error when the source chain's message sender is not a recognised address. error InvalidSourceAddress(); + /// @notice Error when a given child token's root token is the zero address. + error ZeroAddressRootToken(); + /// @notice Error when a given child token's bridge address is not set. + error BrigeNotSet(); + /// @notice Error when a call to the given child token's `burn` function fails. + error BurnFailed(); } From 080dedc4c296ffe1a750b8852794a4a5b9923ef5 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Wed, 1 Nov 2023 11:44:03 +1100 Subject: [PATCH 02/49] ERC20 Bridging pre-tests --- lib/openzeppelin-contracts | 2 +- script/DeployChildContracts.s.sol | 1 - script/InitializeChildContracts.s.sol | 3 +- src/child/ChildAxelarBridgeAdaptor.sol | 38 +++++++++++++++++-- src/child/ChildERC20Bridge.sol | 15 ++------ .../child/IChildAxelarBridgeAdaptor.sol | 9 +++++ .../child/IChildERC20BridgeAdaptor.sol | 11 +++++- src/root/RootAxelarBridgeAdaptor.sol | 2 - .../integration/child/ChildAxelarBridge.t.sol | 5 ++- .../unit/child/ChildAxelarBridgeAdaptor.t.sol | 10 +++-- 10 files changed, 70 insertions(+), 26 deletions(-) diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 9329cfacd..fd81a96f0 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 9329cfacd4c7d20bcb43d772d947ff9e39b65df9 +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 diff --git a/script/DeployChildContracts.s.sol b/script/DeployChildContracts.s.sol index a8d1f7c0a..530eadb4e 100644 --- a/script/DeployChildContracts.s.sol +++ b/script/DeployChildContracts.s.sol @@ -17,7 +17,6 @@ contract DeployChildContracts is Script { function run() public { uint256 deployerPrivateKey = vm.envUint("CHILD_PRIVATE_KEY"); address childGateway = vm.envAddress("CHILD_GATEWAY_ADDRESS"); - address childGasService = vm.envAddress("CHILD_GAS_SERVICE_ADDRESS"); // Not yet used. string memory childRpcUrl = vm.envString("CHILD_RPC_URL"); vm.createSelectFork(childRpcUrl); diff --git a/script/InitializeChildContracts.s.sol b/script/InitializeChildContracts.s.sol index ec29a6cd9..314ade367 100644 --- a/script/InitializeChildContracts.s.sol +++ b/script/InitializeChildContracts.s.sol @@ -20,6 +20,7 @@ contract InitializeChildContracts is Script { string memory childRpcUrl = vm.envString("CHILD_RPC_URL"); string memory rootChainName = vm.envString("ROOT_CHAIN_NAME"); address rootIMXToken = vm.envAddress("ROOT_IMX_ADDRESS"); + address childGasService = vm.envAddress("CHILD_GAS_SERVICE_ADDRESS"); // Not yet used. /** * INITIALIZE CHILD CONTRACTS @@ -35,7 +36,7 @@ contract InitializeChildContracts is Script { address(childAxelarBridgeAdaptor), rootBridgeAdaptorString, childTokenTemplate, rootChainName, rootIMXToken ); - childAxelarBridgeAdaptor.initialize(address(childERC20Bridge)); + childAxelarBridgeAdaptor.initialize(rootChainName, address(childERC20Bridge), childGasService); vm.stopBroadcast(); } diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 4408bb77d..3c6d2a64b 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -2,14 +2,20 @@ pragma solidity ^0.8.21; import {AxelarExecutable} from "@axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; +import {IAxelarGasService} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGasService.sol"; +import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {IChildERC20Bridge} from "../interfaces/child/IChildERC20Bridge.sol"; -import {IChildAxelarBridgeAdaptorErrors} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import {IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import {IChildERC20BridgeAdaptor} from "../interfaces/child/IChildERC20BridgeAdaptor.sol"; -contract ChildAxelarBridgeAdaptor is AxelarExecutable, Initializable, IChildAxelarBridgeAdaptorErrors { +contract ChildAxelarBridgeAdaptor is AxelarExecutable, IChildERC20BridgeAdaptor, Initializable, IChildAxelarBridgeAdaptorErrors, + IChildAxelarBridgeAdaptorEvents { /// @notice Address of bridge to relay messages to. IChildERC20Bridge public childBridge; + IAxelarGasService public gasService; string public rootBridgeAdaptor; + string public rootChain; constructor(address _gateway) AxelarExecutable(_gateway) {} @@ -18,12 +24,14 @@ contract ChildAxelarBridgeAdaptor is AxelarExecutable, Initializable, IChildAxel * @param _childBridge Address of the child bridge contract. * @dev Always sets the rootBridgeAdaptor to whatever the rootERC20BridgeAdaptor of the bridge contract is. */ - function initialize(address _childBridge) external initializer { + function initialize(string memory _rootChain, address _childBridge, address _gasService) external initializer { if (_childBridge == address(0)) { revert ZeroAddress(); } childBridge = IChildERC20Bridge(_childBridge); + rootChain = _rootChain; + gasService = IAxelarGasService(_gasService); rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); } @@ -37,6 +45,30 @@ contract ChildAxelarBridgeAdaptor is AxelarExecutable, Initializable, IChildAxel rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); } + /** + * TODO + */ + function sendMessage(bytes calldata payload, address refundRecipient) external payable override { + if (msg.value == 0) { + revert NoGas(); + } + if (msg.sender != address(childBridge)) { + revert CallerNotBridge(); + } + + // Load from storage. + string memory _rootBridgeAdaptor = rootBridgeAdaptor; + string memory _rootChain = rootChain; + + // TODO For `sender` (first param), should likely be refundRecipient (and in which case refundRecipient should be renamed to sender and used as refund recipient) + gasService.payNativeGasForContractCall{value: msg.value}( + address(this), _rootChain, _rootBridgeAdaptor, payload, refundRecipient + ); + + gateway.callContract(_rootChain, _rootBridgeAdaptor, payload); + emit MapTokenAxelarMessage(_rootChain, _rootBridgeAdaptor, payload); + } + /** * @dev This function is called by the parent `AxelarExecutable` contract to execute the payload. * @custom:assumes `sourceAddress_` is a 20 byte address. diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index bc845ffdb..c77938009 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -132,9 +132,7 @@ contract ChildERC20Bridge is } function withdrawTo(IChildERC20 childToken, address receiver, uint256 amount) external { - _beforeTokenWithdraw(); _withdraw(childToken, receiver, amount); - _afterTokenWithdraw(); } function _withdraw(IChildERC20 childToken, address receiver, uint256 amount) private { @@ -166,16 +164,9 @@ contract ChildERC20Bridge is bytes memory payload = abi.encode(WITHDRAW_SIG, rootToken, msg.sender, receiver, amount); // Send the message to the bridge adaptor and up to root chain - bridgeAdaptor.sendMessage(rootChain, rootERC20BridgeAdaptor, payload); - - if (address(childToken) == childETHToken) { - childToken.burn(msg.sender, amount); - Address.sendValue(payable(receiver), amount); - } else { - childToken.burn(msg.sender, amount); - IERC20Metadata rootToken = IERC20Metadata(childToken.rootToken()); - rootToken.safeTransfer(receiver, amount); - } + bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); + + // TODO emit event } function _mapToken(bytes calldata data) private { diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index c35d82fc1..0e13f2ae1 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -4,4 +4,13 @@ pragma solidity ^0.8.21; interface IChildAxelarBridgeAdaptorErrors { /// @notice Error when a zero address is given when not valid. error ZeroAddress(); + /// @notice Error when a message is sent with no gas payment. + error NoGas(); + /// @notice Error when the caller is not the bridge. + error CallerNotBridge(); } + +interface IChildAxelarBridgeAdaptorEvents { + /// @notice Emitted when an Axelar message is sent to the root chain. + event MapTokenAxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); +} \ No newline at end of file diff --git a/src/interfaces/child/IChildERC20BridgeAdaptor.sol b/src/interfaces/child/IChildERC20BridgeAdaptor.sol index 620d3e46b..1407beb0b 100644 --- a/src/interfaces/child/IChildERC20BridgeAdaptor.sol +++ b/src/interfaces/child/IChildERC20BridgeAdaptor.sol @@ -1,5 +1,12 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; -// TODO to be used when sending messages L2 -> L1 -interface IChildERC20BridgeAdaptor {} +interface IChildERC20BridgeAdaptor { + /** + * @notice Send an arbitrary message to the root chain via the message passing protocol. + * @param payload The message to send, encoded in a `bytes` array. + * @param refundRecipient Used if the message passing protocol requires fees & pays back excess to a refund recipient. + * @dev `payable` because the message passing protocol may require a fee to be paid. + */ + function sendMessage(bytes calldata payload, address refundRecipient) external payable; +} diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 0f8816438..e2642245c 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -30,8 +30,6 @@ contract RootAxelarBridgeAdaptor is address public rootBridge; string public childBridgeAdaptor; - /// @dev childChain could be immutable, but as of writing this Solidity does not support immutable strings. - /// see: https://ethereum.stackexchange.com/questions/127622/typeerror-immutable-variables-cannot-have-a-non-value-type string public childChain; IAxelarGateway public axelarGateway; IAxelarGasService public gasService; diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index 945176926..282499b5d 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -13,6 +13,7 @@ import { } from "../../../src/child/ChildERC20Bridge.sol"; import {IChildERC20, ChildERC20} from "../../../src/child/ChildERC20.sol"; import {MockChildAxelarGateway} from "../../../src/test/child/MockChildAxelarGateway.sol"; +import {MockChildAxelarGasService} from "../../../src/test/child/MockChildAxelarGasService.sol"; import {Utils} from "../../utils.t.sol"; contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { @@ -25,6 +26,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil ChildERC20 public childERC20; ChildAxelarBridgeAdaptor public childAxelarBridgeAdaptor; MockChildAxelarGateway public mockChildAxelarGateway; + MockChildAxelarGasService public mockChildAxelarGasService; function setUp() public { childERC20 = new ChildERC20(); @@ -32,6 +34,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil childERC20Bridge = new ChildERC20Bridge(); mockChildAxelarGateway = new MockChildAxelarGateway(); + mockChildAxelarGasService = new MockChildAxelarGasService(); childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway)); childERC20Bridge.initialize( @@ -42,7 +45,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil IMX_TOKEN_ADDRESS ); - childAxelarBridgeAdaptor.initialize(address(childERC20Bridge)); + childAxelarBridgeAdaptor.initialize(ROOT_CHAIN_NAME, address(childERC20Bridge), address(mockChildAxelarGasService)); } function test_ChildTokenMap() public { diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 18e324a1f..5e80de4cf 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -7,6 +7,7 @@ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {ChildAxelarBridgeAdaptor} from "../../../src/child/ChildAxelarBridgeAdaptor.sol"; import {MockChildERC20Bridge} from "../../../src/test/child/MockChildERC20Bridge.sol"; import {MockChildAxelarGateway} from "../../../src/test/child/MockChildAxelarGateway.sol"; +import {MockChildAxelarGasService} from "../../../src/test/child/MockChildAxelarGasService.sol"; import {IChildAxelarBridgeAdaptorErrors} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErrors { @@ -15,12 +16,14 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro ChildAxelarBridgeAdaptor public childAxelarBridgeAdaptor; MockChildERC20Bridge public mockChildERC20Bridge; MockChildAxelarGateway public mockChildAxelarGateway; + MockChildAxelarGasService public mockChildAxelarGasService; function setUp() public { mockChildERC20Bridge = new MockChildERC20Bridge(); mockChildAxelarGateway = new MockChildAxelarGateway(); + mockChildAxelarGasService = new MockChildAxelarGasService(); childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway)); - childAxelarBridgeAdaptor.initialize(address(mockChildERC20Bridge)); + childAxelarBridgeAdaptor.initialize("root", address(mockChildERC20Bridge), address(mockChildAxelarGasService)); } function test_Constructor_SetsValues() public { @@ -28,10 +31,11 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro assertEq(address(childAxelarBridgeAdaptor.gateway()), address(mockChildAxelarGateway), "gateway not set"); } - function test_RevertIf_ConstructorGivenZeroAddress() public { + // TODO add more initialize tests + function test_RevertIf_InitializeGivenZeroAddress() public { ChildAxelarBridgeAdaptor newAdaptor = new ChildAxelarBridgeAdaptor(GATEWAY_ADDRESS); vm.expectRevert(ZeroAddress.selector); - newAdaptor.initialize(address(0)); + newAdaptor.initialize("root", address(0), address(mockChildAxelarGasService)); } function test_Execute() public { From 74932210b6fa7fedc9f193a220bf2c491e7a3f41 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Wed, 1 Nov 2023 11:53:08 +1100 Subject: [PATCH 03/49] WIP --- src/child/ChildERC20Bridge.sol | 3 ++- src/interfaces/child/IChildERC20Bridge.sol | 4 +++- src/interfaces/root/IRootERC20Bridge.sol | 2 +- src/root/RootERC20Bridge.sol | 2 +- test/integration/child/ChildAxelarBridge.t.sol | 4 ++-- test/integration/root/RootERC20Bridge.t.sol | 4 ++-- test/unit/child/ChildERC20Bridge.t.sol | 4 ++-- test/unit/root/RootERC20Bridge.t.sol | 8 ++++---- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index c77938009..7e8304c4a 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -167,6 +167,7 @@ contract ChildERC20Bridge is bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); // TODO emit event + emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount); } function _mapToken(bytes calldata data) private { @@ -229,7 +230,7 @@ contract ChildERC20Bridge is if (address(rootToken) == NATIVE_ETH) { emit NativeEthDeposit(address(rootToken), childToken, sender, receiver, amount); } else { - emit ERC20Deposit(address(rootToken), childToken, sender, receiver, amount); + emit ChildChainERC20Deposit(address(rootToken), childToken, sender, receiver, amount); } } else { Address.sendValue(payable(receiver), amount); diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 0608d1aa7..532e56efd 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -25,7 +25,9 @@ interface IChildERC20BridgeEvents { /// @notice Emitted when a map token message is received from the root chain and executed successfully. event L2TokenMapped(address rootToken, address childToken); - event ERC20Deposit( + event ChildChainERC20Withdraw(address indexed rootToken, address indexed childToken, address depositor, address indexed receiver, uint256 amount); + + event ChildChainERC20Deposit( address indexed rootToken, address indexed childToken, address depositor, diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 75fa06c46..edea801e9 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -38,7 +38,7 @@ interface IRootERC20BridgeEvents { /// @notice Emitted when a map token message is sent to the child chain. event L1TokenMapped(address indexed rootToken, address indexed childToken); /// @notice Emitted when an ERC20 deposit message is sent to the child chain. - event ERC20Deposit( + event ChildChainERC20Deposit( address indexed rootToken, address indexed childToken, address depositor, diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 23ae28359..263a1ea8f 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -267,7 +267,7 @@ contract RootERC20Bridge is } else if (address(rootToken) == rootIMXToken) { emit IMXDeposit(address(rootToken), msg.sender, receiver, amount); } else { - emit ERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount); + emit ChildChainERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount); } } } diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index 282499b5d..980477a2a 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -133,7 +133,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil /* * DEPOSIT */ - function test_deposit_EmitsERC20Deposit() public { + function test_deposit_EmitsChildChainERC20Deposit() public { address rootTokenAddress = address(456); address sender = address(0xff); address receiver = address(0xee); @@ -142,7 +142,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil bytes32 commandId = bytes32("testCommandId"); vm.expectEmit(address(childERC20Bridge)); - emit ERC20Deposit(rootTokenAddress, childToken, sender, receiver, amount); + emit ChildChainERC20Deposit(rootTokenAddress, childToken, sender, receiver, amount); childAxelarBridgeAdaptor.execute( commandId, diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 374ad9ff0..1a76f41fd 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -285,7 +285,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); - emit ERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); + emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); vm.expectCall( address(axelarAdaptor), @@ -341,7 +341,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx vm.expectEmit(address(axelarAdaptor)); emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); - emit ERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); + emit ChildChainERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); vm.expectCall( address(axelarAdaptor), diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index f8fb5505f..858720318 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -334,7 +334,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); } - function test_onMessageReceive_Deposit_EmitsERC20DepositEvent() public { + function test_onMessageReceive_Deposit_EmitsChildChainERC20DepositEvent() public { setupChildDeposit(rootToken, childBridge, ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR); address sender = address(100); @@ -346,7 +346,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B address childToken = childBridge.rootTokenToChildToken(address(rootToken)); vm.expectEmit(address(childBridge)); - emit ERC20Deposit(address(rootToken), childToken, sender, receiver, amount); + emit ChildChainERC20Deposit(address(rootToken), childToken, sender, receiver, amount); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, depositData); } diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 901a8f0bf..04743c2d2 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -420,12 +420,12 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.deposit{value: depositFee}(token, amount); } - function test_depositEmitsERC20DepositEvent() public { + function test_depositEmitsChildChainERC20DepositEvent() public { uint256 amount = 100; (address childToken,) = setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, amount, true); vm.expectEmit(); - emit ERC20Deposit(address(token), childToken, address(this), address(this), amount); + emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), amount); rootBridge.deposit{value: depositFee}(token, amount); } @@ -517,7 +517,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.depositTo{value: depositFee}(token, receiver, amount); } - function test_depositToEmitsERC20DepositEvent() public { + function test_depositToEmitsChildChainERC20DepositEvent() public { uint256 amount = 100; address receiver = address(12345); @@ -525,7 +525,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, amount, receiver, true); vm.expectEmit(); - emit ERC20Deposit(address(token), childToken, address(this), receiver, amount); + emit ChildChainERC20Deposit(address(token), childToken, address(this), receiver, amount); rootBridge.depositTo{value: depositFee}(token, receiver, amount); } From ec43e7d639ff92b7a9b01c3b9695abdc15aa64af Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Wed, 1 Nov 2023 11:58:32 +1100 Subject: [PATCH 04/49] lint --- src/child/ChildAxelarBridgeAdaptor.sol | 14 +++++++++++--- src/child/ChildERC20Bridge.sol | 3 +-- src/interfaces/child/IChildAxelarBridgeAdaptor.sol | 2 +- src/interfaces/child/IChildERC20Bridge.sol | 8 +++++++- src/test/child/MockChildAxelarGasService.sol | 12 ++++++++++++ test/integration/child/ChildAxelarBridge.t.sol | 4 +++- .../withdrawals/ChildERC20BridgeWithdraw.t.sol | 1 + 7 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/test/child/MockChildAxelarGasService.sol create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 3c6d2a64b..8cfa753af 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -6,11 +6,19 @@ import {IAxelarGasService} from "@axelar-cgp-solidity/contracts/interfaces/IAxel import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {IChildERC20Bridge} from "../interfaces/child/IChildERC20Bridge.sol"; -import {IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import { + IChildAxelarBridgeAdaptorErrors, + IChildAxelarBridgeAdaptorEvents +} from "../interfaces/child/IChildAxelarBridgeAdaptor.sol"; import {IChildERC20BridgeAdaptor} from "../interfaces/child/IChildERC20BridgeAdaptor.sol"; -contract ChildAxelarBridgeAdaptor is AxelarExecutable, IChildERC20BridgeAdaptor, Initializable, IChildAxelarBridgeAdaptorErrors, - IChildAxelarBridgeAdaptorEvents { +contract ChildAxelarBridgeAdaptor is + AxelarExecutable, + IChildERC20BridgeAdaptor, + Initializable, + IChildAxelarBridgeAdaptorErrors, + IChildAxelarBridgeAdaptorEvents +{ /// @notice Address of bridge to relay messages to. IChildERC20Bridge public childBridge; IAxelarGasService public gasService; diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 7e8304c4a..ecf25fbd7 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -126,7 +126,6 @@ contract ChildERC20Bridge is } } - function withdraw(IChildERC20 childToken, uint256 amount) external { _withdraw(childToken, msg.sender, amount); } @@ -165,7 +164,7 @@ contract ChildERC20Bridge is // Send the message to the bridge adaptor and up to root chain bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); - + // TODO emit event emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount); } diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index 0e13f2ae1..8dbb6309c 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -13,4 +13,4 @@ interface IChildAxelarBridgeAdaptorErrors { interface IChildAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the root chain. event MapTokenAxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); -} \ No newline at end of file +} diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 532e56efd..c24252e16 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -25,7 +25,13 @@ interface IChildERC20BridgeEvents { /// @notice Emitted when a map token message is received from the root chain and executed successfully. event L2TokenMapped(address rootToken, address childToken); - event ChildChainERC20Withdraw(address indexed rootToken, address indexed childToken, address depositor, address indexed receiver, uint256 amount); + event ChildChainERC20Withdraw( + address indexed rootToken, + address indexed childToken, + address depositor, + address indexed receiver, + uint256 amount + ); event ChildChainERC20Deposit( address indexed rootToken, diff --git a/src/test/child/MockChildAxelarGasService.sol b/src/test/child/MockChildAxelarGasService.sol new file mode 100644 index 000000000..1b0fc31eb --- /dev/null +++ b/src/test/child/MockChildAxelarGasService.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +contract MockChildAxelarGasService { + function payNativeGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address refundAddress + ) external payable {} +} diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index 980477a2a..724c676cf 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -45,7 +45,9 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil IMX_TOKEN_ADDRESS ); - childAxelarBridgeAdaptor.initialize(ROOT_CHAIN_NAME, address(childERC20Bridge), address(mockChildAxelarGasService)); + childAxelarBridgeAdaptor.initialize( + ROOT_CHAIN_NAME, address(childERC20Bridge), address(mockChildAxelarGasService) + ); } function test_ChildTokenMap() public { diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -0,0 +1 @@ + From 939b0884c40b32afb8b42077bdf233d9ba124517 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Thu, 2 Nov 2023 09:17:45 +1100 Subject: [PATCH 05/49] Adding initial tests for withdrawals --- src/child/ChildERC20Bridge.sol | 2 +- src/interfaces/child/IChildERC20Bridge.sol | 2 +- .../integration/child/ChildAxelarBridge.t.sol | 2 +- test/integration/root/RootERC20Bridge.t.sol | 2 +- .../unit/child/ChildAxelarBridgeAdaptor.t.sol | 2 +- test/unit/child/ChildERC20Bridge.t.sol | 3 +- .../ChildERC20BridgeWithdraw.t.sol | 80 +++++++++++++++++++ test/unit/root/RootAxelarBridgeAdaptor.t.sol | 2 +- test/unit/root/RootERC20Bridge.t.sol | 2 +- test/utils.t.sol | 8 +- 10 files changed, 92 insertions(+), 13 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index ecf25fbd7..547280d61 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -152,7 +152,7 @@ contract ChildERC20Bridge is // A mapped token should never have the bridge unset if (childToken.bridge() != address(this)) { - revert BrigeNotSet(); + revert BridgeNotSet(); } if (!childToken.burn(msg.sender, amount)) { diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index c24252e16..087daa893 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -81,7 +81,7 @@ interface IChildERC20BridgeErrors { /// @notice Error when a given child token's root token is the zero address. error ZeroAddressRootToken(); /// @notice Error when a given child token's bridge address is not set. - error BrigeNotSet(); + error BridgeNotSet(); /// @notice Error when a call to the given child token's `burn` function fails. error BurnFailed(); } diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index 724c676cf..6b4977c6d 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 1a76f41fd..ae57f29af 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 5e80de4cf..5d92f764d 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 858720318..7bb37a263 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; @@ -13,7 +13,6 @@ import { } from "../../../src/child/ChildERC20Bridge.sol"; import {IChildERC20} from "../../../src/interfaces/child/IChildERC20.sol"; import {ChildERC20} from "../../../src/child/ChildERC20.sol"; -import {MockAdaptor} from "../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../utils.t.sol"; contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index 8b1378917..94f0546d4 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -1 +1,81 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + ChildERC20 public childTokenTemplate; + ChildERC20 public rootToken; + ChildERC20 public childToken; + address public childETHToken; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + + function setUp() public { + rootToken = new ChildERC20(); + rootToken.initialize(address(456), "Test", "TST", 18); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + ); + + bytes memory mapTokenData = abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.prank(address(mockAdaptor)); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, mapTokenData); + + childToken = ChildERC20(childBridge.rootTokenToChildToken(address(rootToken))); + } + + function test_RevertsIf_WithdrawCalledWithEmptyChildToken() public { + vm.expectRevert(EmptyTokenContract.selector); + childBridge.withdraw(IChildERC20(address(2222222)), 100); + } + + function test_RevertsIf_WithdrawCalledWithUnmappedToken() public { + ChildERC20 newToken = new ChildERC20(); + newToken.initialize(address(123), "Test", "TST", 18); + vm.expectRevert(NotMapped.selector); + childBridge.withdraw(IChildERC20(address(newToken)), 100); + } + + function test_RevertsIf_WithdrawCalledWithAChildTokenThatHasWrongBridge() public { + // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "bridge"` + uint256 bridgeSlot = 108; + bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); + vm.store(address(childToken), bridgeSlotBytes32, bytes32(address(0x123))); + + vm.expectRevert(BridgeNotSet.selector); + childBridge.withdraw(IChildERC20(address(childToken)), 100); + } + + /** + * @dev TODO NOTE: we are currently not testing the case where: + * token is mapped AND child token's `rootToken` == address(0). + */ +} diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index c93c70d0c..0853ee69a 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 04743c2d2..ef8ff0353 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; diff --git a/test/utils.t.sol b/test/utils.t.sol index 7151afd95..e2ac1566a 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; @@ -123,9 +123,9 @@ contract Utils is Test { string memory sourceChain, string memory sourceAddress ) public { - string memory name = "TEST"; - string memory symbol = "TST"; - uint8 decimals = 18; + string memory name = token.name(); + string memory symbol = token.symbol(); + uint8 decimals = token.decimals(); bytes memory payload = abi.encode(childBridge.MAP_TOKEN_SIG(), address(token), name, symbol, decimals); From b98a961e7a94ac801711b43b60978ced687430aa Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Thu, 2 Nov 2023 10:14:04 +1100 Subject: [PATCH 06/49] Add test for unset root token --- src/child/ChildERC20Bridge.sol | 2 +- .../integration/child/ChildAxelarBridge.t.sol | 1 + .../ChildERC20BridgeWithdraw.t.sol | 27 +++++++++++++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 547280d61..f17607f4e 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -145,7 +145,7 @@ contract ChildERC20Bridge is revert NotMapped(); } - // A mapped token should never a root token unset + // A mapped token should never have root token unset if (rootToken == address(0)) { revert ZeroAddressRootToken(); } diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index 6b4977c6d..c73f41a68 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -299,6 +299,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil address rootAddress = address(0x123); { // Slot is 2 because of the Ownable, Initializable contracts coming first. + // Found by running `forge inspect src/child/ChildERC20Bridge.sol:ChildERC20Bridge storageLayout | grep -B3 -A5 -i "rootTokenToChildToken"` uint256 rootTokenToChildTokenMappingSlot = 2; address childAddress = address(444444); bytes32 slot = getMappingStorageSlotFor(rootAddress, rootTokenToChildTokenMappingSlot); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index 94f0546d4..86d5ae665 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -44,7 +44,8 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN ); - bytes memory mapTokenData = abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); + bytes memory mapTokenData = + abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); vm.prank(address(mockAdaptor)); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, mapTokenData); @@ -68,12 +69,34 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "bridge"` uint256 bridgeSlot = 108; bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); - vm.store(address(childToken), bridgeSlotBytes32, bytes32(address(0x123))); + vm.store(address(childToken), bridgeSlotBytes32, bytes32(uint256(uint160(address(0x123))))); vm.expectRevert(BridgeNotSet.selector); childBridge.withdraw(IChildERC20(address(childToken)), 100); } + function test_RevertsIf_WithdrawCalledWithAChildTokenWithUnsetRootToken() public { + /* First, set rootToken of mapped token to zero */ + + // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "rootToken"` + uint256 rootTokenSlot = 109; + bytes32 rootTokenSlotBytes32 = bytes32(rootTokenSlot); + vm.store(address(childToken), rootTokenSlotBytes32, bytes32(uint256(uint160(address(0))))); + + /* Then, set rootTokenToChildToken[address(0)] to the child token (to bypass the NotMapped check) */ + + // Slot is 2 because of the Ownable, Initializable contracts coming first. + // Found by running `forge inspect src/child/ChildERC20Bridge.sol:ChildERC20Bridge storageLayout | grep -B3 -A5 -i "rootTokenToChildToken"` + uint256 rootTokenToChildTokenMappingSlot = 2; + bytes32 slot = getMappingStorageSlotFor(address(0), rootTokenToChildTokenMappingSlot); + bytes32 data = bytes32(uint256(uint160(address(childToken)))); + + vm.store(address(childBridge), slot, data); + + vm.expectRevert(ZeroAddressRootToken.selector); + childBridge.withdraw(IChildERC20(address(childToken)), 100); + } + /** * @dev TODO NOTE: we are currently not testing the case where: * token is mapped AND child token's `rootToken` == address(0). From 8a01761546c3156e1d0e1522ba880c301d8edea3 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Thu, 2 Nov 2023 17:21:48 +1100 Subject: [PATCH 07/49] All tests except integration --- src/child/ChildAxelarBridgeAdaptor.sol | 2 +- src/child/ChildERC20Bridge.sol | 8 +- .../child/IChildAxelarBridgeAdaptor.sol | 2 +- .../root/IRootAxelarBridgeAdaptor.sol | 2 +- src/root/RootAxelarBridgeAdaptor.sol | 2 +- src/test/child/ChildERC20FailOnBurn.sol | 18 ++ src/test/child/MockChildAxelarGateway.sol | 2 + .../ChildAxelarBridgeWithdraw.t.sol | 0 .../ChildAxelarBridgeWithdrawTo.t.sol | 0 test/integration/root/RootERC20Bridge.t.sol | 12 +- .../unit/child/ChildAxelarBridgeAdaptor.t.sol | 134 +++++++++++- .../ChildERC20BridgeWithdraw.t.sol | 100 +++++++-- .../ChildERC20BridgeWithdrawTo.t.sol | 192 ++++++++++++++++++ test/unit/root/RootAxelarBridgeAdaptor.t.sol | 5 +- 14 files changed, 441 insertions(+), 38 deletions(-) create mode 100644 src/test/child/ChildERC20FailOnBurn.sol create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 8cfa753af..a8e0ddfe0 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -74,7 +74,7 @@ contract ChildAxelarBridgeAdaptor is ); gateway.callContract(_rootChain, _rootBridgeAdaptor, payload); - emit MapTokenAxelarMessage(_rootChain, _rootBridgeAdaptor, payload); + emit AxelarMessage(_rootChain, _rootBridgeAdaptor, payload); } /** diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index f17607f4e..a20269c2e 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -126,11 +126,11 @@ contract ChildERC20Bridge is } } - function withdraw(IChildERC20 childToken, uint256 amount) external { + function withdraw(IChildERC20 childToken, uint256 amount) external payable { _withdraw(childToken, msg.sender, amount); } - function withdrawTo(IChildERC20 childToken, address receiver, uint256 amount) external { + function withdrawTo(IChildERC20 childToken, address receiver, uint256 amount) external payable { _withdraw(childToken, receiver, amount); } @@ -159,13 +159,15 @@ contract ChildERC20Bridge is revert BurnFailed(); } + + // TODO Should we enforce receiver != 0? old poly contracts don't + // Encode the message payload bytes memory payload = abi.encode(WITHDRAW_SIG, rootToken, msg.sender, receiver, amount); // Send the message to the bridge adaptor and up to root chain bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); - // TODO emit event emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount); } diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index 8dbb6309c..f7cd63330 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -12,5 +12,5 @@ interface IChildAxelarBridgeAdaptorErrors { interface IChildAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the root chain. - event MapTokenAxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); + event AxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); } diff --git a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol index f5235bb99..d134c9410 100644 --- a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol +++ b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -14,5 +14,5 @@ interface IRootAxelarBridgeAdaptorErrors { interface IRootAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the child chain. - event MapTokenAxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); + event AxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index e2642245c..b68e811a0 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -86,6 +86,6 @@ contract RootAxelarBridgeAdaptor is ); axelarGateway.callContract(_childChain, _childBridgeAdaptor, payload); - emit MapTokenAxelarMessage(_childChain, _childBridgeAdaptor, payload); + emit AxelarMessage(_childChain, _childBridgeAdaptor, payload); } } diff --git a/src/test/child/ChildERC20FailOnBurn.sol b/src/test/child/ChildERC20FailOnBurn.sol new file mode 100644 index 000000000..f287319ca --- /dev/null +++ b/src/test/child/ChildERC20FailOnBurn.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2.0 +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) +pragma solidity ^0.8.21; + +import "../../child/ChildERC20.sol"; + +/** + * @title ChildERC20FailOnBurn + * @author Immutable (@Benjimmutable) + * @notice ChildERC20 contract, except burn always returns false. Used for testing. + * @dev USED FOR TESTING + */ +// solhint-disable reason-string +contract ChildERC20FailOnBurn is ChildERC20 { + function burn(address account, uint256 amount) public virtual override returns (bool) { + return false; + } +} diff --git a/src/test/child/MockChildAxelarGateway.sol b/src/test/child/MockChildAxelarGateway.sol index 51b9dffc7..d4c9a1b57 100644 --- a/src/test/child/MockChildAxelarGateway.sol +++ b/src/test/child/MockChildAxelarGateway.sol @@ -5,4 +5,6 @@ contract MockChildAxelarGateway { function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) { return true; } + + function callContract(string memory childChain, string memory childBridgeAdaptor, bytes memory payload) external {} } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol new file mode 100644 index 000000000..e69de29bb diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol new file mode 100644 index 000000000..e69de29bb diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index ae57f29af..183aa942c 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -50,7 +50,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); + emit AxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); vm.expectEmit(true, true, false, false, address(rootBridge)); emit L1TokenMapped(address(token), childToken); @@ -112,7 +112,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx console2.logBytes(predictedPayload); vm.expectEmit(address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit NativeEthDeposit( address(NATIVE_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount @@ -168,7 +168,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(IMX_TOKEN_ADDRESS, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit IMXDeposit(address(IMX_TOKEN_ADDRESS), address(this), address(this), tokenAmount); @@ -225,7 +225,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit WETHDeposit(address(WRAPPED_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount); vm.expectCall( @@ -283,7 +283,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, true); vm.expectEmit(address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); @@ -339,7 +339,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); vm.expectEmit(address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit ChildChainERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 5d92f764d..d582bead1 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -4,31 +4,37 @@ pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import {ChildAxelarBridgeAdaptor} from "../../../src/child/ChildAxelarBridgeAdaptor.sol"; import {MockChildERC20Bridge} from "../../../src/test/child/MockChildERC20Bridge.sol"; import {MockChildAxelarGateway} from "../../../src/test/child/MockChildAxelarGateway.sol"; import {MockChildAxelarGasService} from "../../../src/test/child/MockChildAxelarGasService.sol"; -import {IChildAxelarBridgeAdaptorErrors} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import {IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; -contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErrors { +contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents { address public GATEWAY_ADDRESS = address(1); + string public constant ROOT_CHAIN_NAME = "root"; + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); - ChildAxelarBridgeAdaptor public childAxelarBridgeAdaptor; + ERC20PresetMinterPauser public token; + ChildAxelarBridgeAdaptor public axelarAdaptor; MockChildERC20Bridge public mockChildERC20Bridge; MockChildAxelarGateway public mockChildAxelarGateway; MockChildAxelarGasService public mockChildAxelarGasService; function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); mockChildERC20Bridge = new MockChildERC20Bridge(); mockChildAxelarGateway = new MockChildAxelarGateway(); mockChildAxelarGasService = new MockChildAxelarGasService(); - childAxelarBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway)); - childAxelarBridgeAdaptor.initialize("root", address(mockChildERC20Bridge), address(mockChildAxelarGasService)); + axelarAdaptor = new ChildAxelarBridgeAdaptor(address(mockChildAxelarGateway)); + axelarAdaptor.initialize(ROOT_CHAIN_NAME, address(mockChildERC20Bridge), address(mockChildAxelarGasService)); } function test_Constructor_SetsValues() public { - assertEq(address(childAxelarBridgeAdaptor.childBridge()), address(mockChildERC20Bridge), "childBridge not set"); - assertEq(address(childAxelarBridgeAdaptor.gateway()), address(mockChildAxelarGateway), "gateway not set"); + assertEq(address(axelarAdaptor.childBridge()), address(mockChildERC20Bridge), "childBridge not set"); + assertEq(address(axelarAdaptor.gateway()), address(mockChildAxelarGateway), "gateway not set"); + assertEq(axelarAdaptor.rootChain(), ROOT_CHAIN_NAME, "rootChain not set"); } // TODO add more initialize tests @@ -49,6 +55,118 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro address(mockChildERC20Bridge), abi.encodeWithSelector(mockChildERC20Bridge.onMessageReceive.selector, sourceChain, sourceAddress, payload) ); - childAxelarBridgeAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } + + /// @dev For this unit test we just want to make sure the correct functions are called on the Axelar Gateway and Gas Service. + function test_sendMessage_CallsGasService() public { + address refundRecipient = address(123); + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + uint256 callValue = 300; + + vm.expectCall( + address(mockChildAxelarGasService), + callValue, + abi.encodeWithSelector( + mockChildAxelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + ROOT_CHAIN_NAME, + mockChildERC20Bridge.rootERC20BridgeAdaptor(), + payload, + refundRecipient + ) + ); + + vm.deal(address(mockChildERC20Bridge), callValue); + vm.prank(address(mockChildERC20Bridge)); + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + } + + function test_sendMessage_CallsGateway() public { + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + uint256 callValue = 300; + + vm.expectCall( + address(mockChildAxelarGateway), + abi.encodeWithSelector( + mockChildAxelarGateway.callContract.selector, ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload + ) + ); + + vm.deal(address(mockChildERC20Bridge), callValue); + vm.prank(address(mockChildERC20Bridge)); + axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); + } + + function test_sendMessage_EmitsAxelarMessageEvent() public { + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + uint256 callValue = 300; + + vm.expectEmit(); + emit AxelarMessage(ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload); + + vm.deal(address(mockChildERC20Bridge), callValue); + vm.prank(address(mockChildERC20Bridge)); + axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); + } + + function testFuzz_sendMessage_PaysGasToGasService(uint256 callValue) public { + vm.assume(callValue < address(this).balance); + vm.assume(callValue > 0); + vm.deal(address(mockChildERC20Bridge), callValue); + + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + + uint256 bridgePreBal = address(mockChildERC20Bridge).balance; + uint256 axelarGasServicePreBal = address(mockChildAxelarGasService).balance; + + vm.prank(address(mockChildERC20Bridge)); + axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); + + assertEq(address(mockChildERC20Bridge).balance, bridgePreBal - callValue, "ETH balance not decreased"); + assertEq(address(mockChildAxelarGasService).balance, axelarGasServicePreBal + callValue, "ETH not paid to gas service"); + } + + function test_sendMessage_GivesCorrectRefundRecipient() public { + address refundRecipient = address(0x3333); + uint256 callValue = 300; + + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + + vm.expectCall( + address(mockChildAxelarGasService), + callValue, + abi.encodeWithSelector( + mockChildAxelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + ROOT_CHAIN_NAME, + mockChildERC20Bridge.rootERC20BridgeAdaptor(), + payload, + refundRecipient + ) + ); + + vm.deal(address(mockChildERC20Bridge), callValue); + vm.prank(address(mockChildERC20Bridge)); + axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); + } + + function test_RevertIf_mapTokenCalledByNonRootBridge() public { + address payable prankster = payable(address(0x33)); + uint256 value = 300; + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + + // Have to call these above so the expectRevert works on the call to mapToken. + prankster.transfer(value); + vm.prank(prankster); + vm.expectRevert(CallerNotBridge.selector); + axelarAdaptor.sendMessage{value: value}(payload, address(123)); + } + + function test_RevertIf_mapTokenCalledWithNoValue() public { + bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); + vm.expectRevert(NoGas.selector); + vm.prank(address(mockChildERC20Bridge)); + axelarAdaptor.sendMessage{value: 0}(payload, address(123)); } } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index 86d5ae665..a9855d02a 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -18,6 +18,7 @@ import {Utils} from "../../../utils.t.sol"; contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address constant ROOT_BRIDGE = address(3); string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; @@ -51,6 +52,9 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, mapTokenData); childToken = ChildERC20(childBridge.rootTokenToChildToken(address(rootToken))); + vm.prank(address(childBridge)); + childToken.mint(address(this), 1000000 ether); + childToken.approve(address(childBridge), 1000000 ether); } function test_RevertsIf_WithdrawCalledWithEmptyChildToken() public { @@ -65,16 +69,6 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi childBridge.withdraw(IChildERC20(address(newToken)), 100); } - function test_RevertsIf_WithdrawCalledWithAChildTokenThatHasWrongBridge() public { - // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "bridge"` - uint256 bridgeSlot = 108; - bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); - vm.store(address(childToken), bridgeSlotBytes32, bytes32(uint256(uint160(address(0x123))))); - - vm.expectRevert(BridgeNotSet.selector); - childBridge.withdraw(IChildERC20(address(childToken)), 100); - } - function test_RevertsIf_WithdrawCalledWithAChildTokenWithUnsetRootToken() public { /* First, set rootToken of mapped token to zero */ @@ -97,8 +91,86 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi childBridge.withdraw(IChildERC20(address(childToken)), 100); } - /** - * @dev TODO NOTE: we are currently not testing the case where: - * token is mapped AND child token's `rootToken` == address(0). - */ + function test_RevertsIf_WithdrawCalledWithAChildTokenThatHasWrongBridge() public { + // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "bridge"` + uint256 bridgeSlot = 108; + bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); + vm.store(address(childToken), bridgeSlotBytes32, bytes32(uint256(uint160(address(0x123))))); + + vm.expectRevert(BridgeNotSet.selector); + childBridge.withdraw(IChildERC20(address(childToken)), 100); + } + + function test_RevertsIf_WithdrawWhenBurnFails() public { + // Replace the childToken with one that always returns `false` on failure. + deployCodeTo("ChildERC20FailOnBurn.sol", address(childToken)); + + vm.expectRevert(BurnFailed.selector); + childBridge.withdraw(IChildERC20(address(childToken)), 100); + } + + function test_withdraw_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, address(rootToken), address(this), address(this), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdraw{value: withdrawFee}(IChildERC20(address(childToken)), withdrawAmount); + } + + function test_withdraw_EmitsERC20WithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainERC20Withdraw(address(rootToken), address(childToken), address(this), address(this), withdrawAmount); + + childBridge.withdraw{value: withdrawFee}(IChildERC20(address(childToken)), withdrawAmount); + } + + function test_withdraw_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = childToken.balanceOf(address(this)); + + childBridge.withdraw{value: withdrawFee}(IChildERC20(address(childToken)), withdrawAmount); + + uint256 postBal = childToken.balanceOf(address(this)); + assertEq(postBal, preBal - withdrawAmount, "Balance not reduced"); + } + + function test_withdraw_ReducesTotalSupply() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preTotalSupply = childToken.totalSupply(); + + childBridge.withdraw{value: withdrawFee}(IChildERC20(address(childToken)), withdrawAmount); + + uint256 postTotalSupply = childToken.totalSupply(); + assertEq(postTotalSupply, preTotalSupply - withdrawAmount, "total supply not reduced"); + } + + function test_withdraw_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(mockAdaptor).balance; + uint256 thisPreBal = address(this).balance; + + childBridge.withdraw{value: withdrawFee}(IChildERC20(address(childToken)), withdrawAmount); + + uint256 postBal = address(mockAdaptor).balance; + uint256 thisPostBal = address(this).balance; + + assertEq(postBal, preBal + withdrawFee, "Adaptor balance not increased"); + assertEq(thisPostBal, thisPreBal - withdrawFee, "withdrawer's balance not decreased"); + } } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol new file mode 100644 index 000000000..47de22548 --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + ChildERC20 public childTokenTemplate; + ChildERC20 public rootToken; + ChildERC20 public childToken; + address public childETHToken; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + + function setUp() public { + rootToken = new ChildERC20(); + rootToken.initialize(address(456), "Test", "TST", 18); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + ); + + bytes memory mapTokenData = + abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.prank(address(mockAdaptor)); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, mapTokenData); + + childToken = ChildERC20(childBridge.rootTokenToChildToken(address(rootToken))); + vm.prank(address(childBridge)); + childToken.mint(address(this), 1000000 ether); + childToken.approve(address(childBridge), 1000000 ether); + } + + function test_RevertsIf_WithdrawToCalledWithEmptyChildToken() public { + vm.expectRevert(EmptyTokenContract.selector); + childBridge.withdrawTo(IChildERC20(address(2222222)), address(this), 100); + } + + function test_RevertsIf_WithdrawToCalledWithUnmappedToken() public { + ChildERC20 newToken = new ChildERC20(); + newToken.initialize(address(123), "Test", "TST", 18); + vm.expectRevert(NotMapped.selector); + childBridge.withdrawTo(IChildERC20(address(newToken)), address(this), 100); + } + + function test_RevertsIf_WithdrawToCalledWithAChildTokenWithUnsetRootToken() public { + /* First, set rootToken of mapped token to zero */ + + // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "rootToken"` + uint256 rootTokenSlot = 109; + bytes32 rootTokenSlotBytes32 = bytes32(rootTokenSlot); + vm.store(address(childToken), rootTokenSlotBytes32, bytes32(uint256(uint160(address(0))))); + + /* Then, set rootTokenToChildToken[address(0)] to the child token (to bypass the NotMapped check) */ + + // Slot is 2 because of the Ownable, Initializable contracts coming first. + // Found by running `forge inspect src/child/ChildERC20Bridge.sol:ChildERC20Bridge storageLayout | grep -B3 -A5 -i "rootTokenToChildToken"` + uint256 rootTokenToChildTokenMappingSlot = 2; + bytes32 slot = getMappingStorageSlotFor(address(0), rootTokenToChildTokenMappingSlot); + bytes32 data = bytes32(uint256(uint160(address(childToken)))); + + vm.store(address(childBridge), slot, data); + + vm.expectRevert(ZeroAddressRootToken.selector); + childBridge.withdrawTo(IChildERC20(address(childToken)), address(this), 100); + } + + function test_RevertsIf_WithdrawToCalledWithAChildTokenThatHasWrongBridge() public { + // Found by running `forge inspect src/child/ChildERC20.sol:ChildERC20 storageLayout | grep -B3 -A5 -i "bridge"` + uint256 bridgeSlot = 108; + bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); + vm.store(address(childToken), bridgeSlotBytes32, bytes32(uint256(uint160(address(0x123))))); + + vm.expectRevert(BridgeNotSet.selector); + childBridge.withdrawTo(IChildERC20(address(childToken)), address(this), 100); + } + + function test_RevertsIf_WithdrawToWhenBurnFails() public { + // Replace the childToken with one that always returns `false` on failure. + deployCodeTo("ChildERC20FailOnBurn.sol", address(childToken)); + + vm.expectRevert(BurnFailed.selector); + childBridge.withdrawTo(IChildERC20(address(childToken)), address(this), 100); + } + + function test_withdrawTo_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, address(rootToken), address(this), address(this), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); + } + + function test_withdrawTo_EmitsERC20WithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainERC20Withdraw(address(rootToken), address(childToken), address(this), address(this), withdrawAmount); + + childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); + } + + function test_withdraw_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = childToken.balanceOf(address(this)); + + childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); + + uint256 postBal = childToken.balanceOf(address(this)); + assertEq(postBal, preBal - withdrawAmount); + } + + function test_withdraw_ReducesTotalSupply() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preTotalSupply = childToken.totalSupply(); + + childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); + + uint256 postTotalSupply = childToken.totalSupply(); + assertEq(postTotalSupply, preTotalSupply - withdrawAmount); + } + + function test_withdrawTo_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(mockAdaptor).balance; + uint256 thisPreBal = address(this).balance; + + childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); + + uint256 postBal = address(mockAdaptor).balance; + uint256 thisPostBal = address(this).balance; + + assertEq(postBal, preBal + withdrawFee); + assertEq(thisPostBal, thisPreBal - withdrawFee); + } + + function test_withdrawTo_ToDifferentReceiverCallsMockAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + address receiver = address(123); + + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, address(rootToken), address(this), receiver, withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), receiver, withdrawAmount); + } +} diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 0853ee69a..1a475da27 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {MockAxelarGateway} from "../../../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../../../src/test/root/MockAxelarGasService.sol"; import { @@ -99,12 +98,12 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } - function test_sendMessage_EmitsMapTokenAxelarMessageEvent() public { + function test_sendMessage_EmitsAxelarMessageEvent() public { bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); uint256 callValue = 300; vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - emit MapTokenAxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptor, payload); + emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptor, payload); vm.prank(address(stubRootBridge)); axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } From eb1c5bcf733bccb5b87edc390c4bad99d547e0cd Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 09:43:29 +1100 Subject: [PATCH 08/49] Integration tests --- src/child/ChildAxelarBridgeAdaptor.sol | 1 - src/child/ChildERC20Bridge.sol | 1 - .../ChildAxelarBridgeWithdraw.t.sol | 496 ++++++++++++++++++ .../ChildAxelarBridgeWithdrawTo.t.sol | 1 + test/integration/root/RootERC20Bridge.t.sol | 3 +- .../unit/child/ChildAxelarBridgeAdaptor.t.sol | 22 +- .../ChildERC20BridgeWithdraw.t.sol | 9 +- .../ChildERC20BridgeWithdrawTo.t.sol | 12 +- test/utils.t.sol | 41 +- 9 files changed, 565 insertions(+), 21 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index a8e0ddfe0..24fcb77cd 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -68,7 +68,6 @@ contract ChildAxelarBridgeAdaptor is string memory _rootBridgeAdaptor = rootBridgeAdaptor; string memory _rootChain = rootChain; - // TODO For `sender` (first param), should likely be refundRecipient (and in which case refundRecipient should be renamed to sender and used as refund recipient) gasService.payNativeGasForContractCall{value: msg.value}( address(this), _rootChain, _rootBridgeAdaptor, payload, refundRecipient ); diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index a20269c2e..9a9e6212e 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -159,7 +159,6 @@ contract ChildERC20Bridge is revert BurnFailed(); } - // TODO Should we enforce receiver != 0? old poly contracts don't // Encode the message payload diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol index e69de29bb..c2406f452 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; + +contract ChildERC20BridgeWithdrawIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant IMX_TOKEN_ADDRESS = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + + uint256 constant withdrawFee = 200; + uint256 constant withdrawAmount = 99999999999; + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + } + + /** + * @dev A future test will assert that the computed childToken is the same as what gets deployed on L2. + * This test uses the same code as the mapToken function does to calculate this address, so we can + * not consider it sufficient. + */ + function test_withdraw_CallsBridgeAdaptor() public { + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, rootToken, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); + } + + function test_withdraw_Calls_AxelarGateway() public { + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, rootToken, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); + } + + function test_withdraw_Calls_GasService() public { + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, rootToken, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); + } + + function test_withdraw_emits_AxelarMessageEvent() public { + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, rootToken, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); + } + + function test_withdraw_BurnsFundsAndTransfersGas() public { + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); + + uint256 preBal = childToken.balanceOf(address(this)); + uint256 preGasBal = address(axelarGasService).balance; + + childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); + + uint256 postBal = childToken.balanceOf(address(this)); + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawAmount, "Balance not reduced"); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas not transferred"); + } + + function test_RevertIf_WithdrawWithNoGas() public { + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); + + vm.expectRevert(NoGas.selector); + childBridge.withdraw(childToken, withdrawAmount); + } + + // 7a8dc26796a1e50e6e190b70259f58f6a4edd5b22280ceecc82b687b8e982869 + // 000000000000000000000000000000000000000000000000000000000000ad9c + // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 + // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 + // 000000000000000000000000000000000000000000000000000000174876e7ff + // 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 + + // f20755ba + // 0000000000000000000000000000000000000000000000000000000000000040 + // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 + // 00000000000000000000000000000000000000000000000000000000000000a0 + // 2cef46a936bdc5b7e6e8c71aa04560c41cf7d88bb26901a7e7f4936ff02accad + // 000000000000000000000000000000000000000000000000000000000000ad9c + // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 + // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 + // 000000000000000000000000000000000000000000000000000000174876e7ff + + // bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); + // vm.expectEmit(true, true, true, false, address(axelarAdaptor)); + // emit AxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); + + // vm.expectEmit(true, true, false, false, address(rootBridge)); + // emit L1TokenMapped(address(token), childToken); + + // // Instead of using expectCalls, we could use expectEmit in combination with mock contracts emitting events. + // // expectCalls requires less boilerplate and is less dependant on mock code. + // vm.expectCall( + // address(axelarAdaptor), + // mapTokenFee, + // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, payload, address(this)) + // ); + + // // These are calls that the axelarAdaptor should make. + // vm.expectCall( + // address(axelarGasService), + // mapTokenFee, + // abi.encodeWithSelector( + // axelarGasService.payNativeGasForContractCall.selector, + // address(axelarAdaptor), + // CHILD_CHAIN_NAME, + // Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + // payload, + // address(this) + // ) + // ); + + // vm.expectCall( + // address(mockAxelarGateway), + // 0, + // abi.encodeWithSelector( + // mockAxelarGateway.callContract.selector, + // CHILD_CHAIN_NAME, + // Strings.toHexString(CHILD_BRIDGE_ADAPTOR), + // payload + // ) + // ); + + // // Check that we pay mapTokenFee to the axelarGasService. + // uint256 thisPreBal = address(this).balance; + // uint256 axelarGasServicePreBal = address(axelarGasService).balance; + + // rootBridge.mapToken{value: mapTokenFee}(token); + + // // Should update ETH balances as gas payment for message. + // assertEq(address(this).balance, thisPreBal - mapTokenFee, "ETH balance not decreased"); + // assertEq(address(axelarGasService).balance, axelarGasServicePreBal + mapTokenFee, "ETH not paid to gas service"); + + // assertEq(rootBridge.rootTokenToChildToken(address(token)), childToken, "childToken not set"); + // } + + // // TODO split into multiple tests + // function test_depositETH() public { + // uint256 tokenAmount = 300; + // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + + // (, bytes memory predictedPayload) = + // setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); + + // console2.logBytes(predictedPayload); + + // vm.expectEmit(address(axelarAdaptor)); + // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + // vm.expectEmit(address(rootBridge)); + // emit NativeEthDeposit( + // address(NATIVE_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount + // ); + + // vm.expectCall( + // address(axelarAdaptor), + // depositFee, + // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + // ); + + // vm.expectCall( + // address(axelarGasService), + // depositFee, + // abi.encodeWithSelector( + // axelarGasService.payNativeGasForContractCall.selector, + // address(axelarAdaptor), + // CHILD_CHAIN_NAME, + // childBridgeAdaptorString, + // predictedPayload, + // address(this) + // ) + // ); + + // vm.expectCall( + // address(mockAxelarGateway), + // 0, + // abi.encodeWithSelector( + // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload + // ) + // ); + + // uint256 bridgePreBal = address(rootBridge).balance; + + // uint256 thisNativePreBal = address(this).balance; + // uint256 gasServiceNativePreBal = address(axelarGasService).balance; + + // rootBridge.depositETH{value: tokenAmount + depositFee}(tokenAmount); + + // // Check that tokens are transferred + // assertEq(bridgePreBal + tokenAmount, address(rootBridge).balance, "ETH not transferred to bridge"); + // // Check that native asset transferred to gas service + // assertEq(thisNativePreBal - (depositFee + tokenAmount), address(this).balance, "ETH not paid from user"); + // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); + // } + + // // TODO split into multiple tests + // function test_depositIMXToken() public { + // uint256 tokenAmount = 300; + // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + + // (, bytes memory predictedPayload) = + // setupDeposit(IMX_TOKEN_ADDRESS, rootBridge, mapTokenFee, depositFee, tokenAmount, false); + + // vm.expectEmit(address(axelarAdaptor)); + // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + // vm.expectEmit(address(rootBridge)); + // emit IMXDeposit(address(IMX_TOKEN_ADDRESS), address(this), address(this), tokenAmount); + + // vm.expectCall( + // address(axelarAdaptor), + // depositFee, + // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + // ); + + // vm.expectCall( + // address(axelarGasService), + // depositFee, + // abi.encodeWithSelector( + // axelarGasService.payNativeGasForContractCall.selector, + // address(axelarAdaptor), + // CHILD_CHAIN_NAME, + // childBridgeAdaptorString, + // predictedPayload, + // address(this) + // ) + // ); + + // vm.expectCall( + // address(mockAxelarGateway), + // 0, + // abi.encodeWithSelector( + // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload + // ) + // ); + + // uint256 thisPreBal = imxToken.balanceOf(address(this)); + // uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + // uint256 thisNativePreBal = address(this).balance; + // uint256 gasServiceNativePreBal = address(axelarGasService).balance; + + // rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN_ADDRESS), tokenAmount); + + // // Check that tokens are transferred + // assertEq(thisPreBal - tokenAmount, imxToken.balanceOf(address(this)), "Tokens not transferred from user"); + // assertEq( + // bridgePreBal + tokenAmount, imxToken.balanceOf(address(rootBridge)), "Tokens not transferred to bridge" + // ); + // // Check that native asset transferred to gas service + // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); + // } + + // // TODO split into multiple tests + // function test_depositWETH() public { + // uint256 tokenAmount = 300; + // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + // (, bytes memory predictedPayload) = + // setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); + + // vm.expectEmit(address(axelarAdaptor)); + // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + // vm.expectEmit(address(rootBridge)); + // emit WETHDeposit(address(WRAPPED_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount); + // vm.expectCall( + // address(axelarAdaptor), + // depositFee, + // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + // ); + + // vm.expectCall( + // address(axelarGasService), + // depositFee, + // abi.encodeWithSelector( + // axelarGasService.payNativeGasForContractCall.selector, + // address(axelarAdaptor), + // CHILD_CHAIN_NAME, + // childBridgeAdaptorString, + // predictedPayload, + // address(this) + // ) + // ); + + // vm.expectCall( + // address(mockAxelarGateway), + // 0, + // abi.encodeWithSelector( + // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload + // ) + // ); + + // uint256 thisPreBal = IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)); + // uint256 bridgePreBal = address(rootBridge).balance; + + // uint256 thisNativePreBal = address(this).balance; + // uint256 gasServiceNativePreBal = address(axelarGasService).balance; + + // rootBridge.deposit{value: depositFee}(IERC20Metadata(WRAPPED_ETH), tokenAmount); + + // // Check that tokens are transferred + // assertEq( + // thisPreBal - tokenAmount, + // IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)), + // "Tokens not transferred from user" + // ); + // assertEq(bridgePreBal + tokenAmount, address(rootBridge).balance, "ETH not transferred to Bridge"); + // // Check that native asset transferred to gas service + // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH for fee not paid from user"); + // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); + // } + + // // TODO split into multiple tests + // function test_depositToken() public { + // uint256 tokenAmount = 300; + // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + // (address childToken, bytes memory predictedPayload) = + // setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, true); + + // vm.expectEmit(address(axelarAdaptor)); + // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + // vm.expectEmit(address(rootBridge)); + // emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); + + // vm.expectCall( + // address(axelarAdaptor), + // depositFee, + // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + // ); + + // vm.expectCall( + // address(axelarGasService), + // depositFee, + // abi.encodeWithSelector( + // axelarGasService.payNativeGasForContractCall.selector, + // address(axelarAdaptor), + // CHILD_CHAIN_NAME, + // childBridgeAdaptorString, + // predictedPayload, + // address(this) + // ) + // ); + + // vm.expectCall( + // address(mockAxelarGateway), + // 0, + // abi.encodeWithSelector( + // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload + // ) + // ); + + // uint256 thisPreBal = token.balanceOf(address(this)); + // uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + // uint256 thisNativePreBal = address(this).balance; + // uint256 gasServiceNativePreBal = address(axelarGasService).balance; + + // rootBridge.deposit{value: depositFee}(token, tokenAmount); + + // // Check that tokens are transferred + // assertEq(thisPreBal - tokenAmount, token.balanceOf(address(this)), "Tokens not transferred from user"); + // assertEq(bridgePreBal + tokenAmount, token.balanceOf(address(rootBridge)), "Tokens not transferred to bridge"); + // // Check that native asset transferred to gas service + // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); + // } + + // // TODO split into multiple tests + // function test_depositTo() public { + // uint256 tokenAmount = 300; + // address recipient = address(9876); + // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + // (address childToken, bytes memory predictedPayload) = + // setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); + + // vm.expectEmit(address(axelarAdaptor)); + // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + // vm.expectEmit(address(rootBridge)); + // emit ChildChainERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); + + // vm.expectCall( + // address(axelarAdaptor), + // depositFee, + // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + // ); + + // vm.expectCall( + // address(axelarGasService), + // depositFee, + // abi.encodeWithSelector( + // axelarGasService.payNativeGasForContractCall.selector, + // address(axelarAdaptor), + // CHILD_CHAIN_NAME, + // childBridgeAdaptorString, + // predictedPayload, + // address(this) + // ) + // ); + + // vm.expectCall( + // address(mockAxelarGateway), + // 0, + // abi.encodeWithSelector( + // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload + // ) + // ); + + // uint256 thisPreBal = token.balanceOf(address(this)); + // uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + // uint256 thisNativePreBal = address(this).balance; + // uint256 gasServiceNativePreBal = address(axelarGasService).balance; + + // rootBridge.depositTo{value: depositFee}(token, recipient, tokenAmount); + + // // Check that tokens are transferred + // assertEq(thisPreBal - tokenAmount, token.balanceOf(address(this)), "Tokens not transferred from user"); + // assertEq(bridgePreBal + tokenAmount, token.balanceOf(address(rootBridge)), "Tokens not transferred to bridge"); + // // Check that native asset transferred to gas service + // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); + // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); + // } +} diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol index e69de29bb..8b1378917 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawTo.t.sol @@ -0,0 +1 @@ + diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 183aa942c..e26e762b4 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -16,7 +16,6 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx address constant CHILD_BRIDGE = address(3); address constant CHILD_BRIDGE_ADAPTOR = address(4); string constant CHILD_CHAIN_NAME = "test"; - bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); address constant IMX_TOKEN_ADDRESS = address(0xccc); address constant NATIVE_ETH = address(0xeee); address constant WRAPPED_ETH = address(0xddd); @@ -35,7 +34,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); (imxToken, token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = - integrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH); + rootIntegrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH); } /** diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index d582bead1..5cdf8e049 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -9,7 +9,10 @@ import {ChildAxelarBridgeAdaptor} from "../../../src/child/ChildAxelarBridgeAdap import {MockChildERC20Bridge} from "../../../src/test/child/MockChildERC20Bridge.sol"; import {MockChildAxelarGateway} from "../../../src/test/child/MockChildAxelarGateway.sol"; import {MockChildAxelarGasService} from "../../../src/test/child/MockChildAxelarGasService.sol"; -import {IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; +import { + IChildAxelarBridgeAdaptorErrors, + IChildAxelarBridgeAdaptorEvents +} from "../../../src/interfaces/child/IChildAxelarBridgeAdaptor.sol"; contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErrors, IChildAxelarBridgeAdaptorEvents { address public GATEWAY_ADDRESS = address(1); @@ -58,7 +61,6 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); } - /// @dev For this unit test we just want to make sure the correct functions are called on the Axelar Gateway and Gas Service. function test_sendMessage_CallsGasService() public { address refundRecipient = address(123); bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); @@ -89,7 +91,10 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro vm.expectCall( address(mockChildAxelarGateway), abi.encodeWithSelector( - mockChildAxelarGateway.callContract.selector, ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload + mockChildAxelarGateway.callContract.selector, + ROOT_CHAIN_NAME, + mockChildERC20Bridge.rootERC20BridgeAdaptor(), + payload ) ); @@ -124,7 +129,11 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); assertEq(address(mockChildERC20Bridge).balance, bridgePreBal - callValue, "ETH balance not decreased"); - assertEq(address(mockChildAxelarGasService).balance, axelarGasServicePreBal + callValue, "ETH not paid to gas service"); + assertEq( + address(mockChildAxelarGasService).balance, + axelarGasServicePreBal + callValue, + "ETH not paid to gas service" + ); } function test_sendMessage_GivesCorrectRefundRecipient() public { @@ -151,19 +160,18 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.sendMessage{value: callValue}(payload, refundRecipient); } - function test_RevertIf_mapTokenCalledByNonRootBridge() public { + function test_RevertIf_sendMessageCalledByNonRootBridge() public { address payable prankster = payable(address(0x33)); uint256 value = 300; bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); - // Have to call these above so the expectRevert works on the call to mapToken. prankster.transfer(value); vm.prank(prankster); vm.expectRevert(CallerNotBridge.selector); axelarAdaptor.sendMessage{value: value}(payload, address(123)); } - function test_RevertIf_mapTokenCalledWithNoValue() public { + function test_RevertIf_sendMessageCalledWithNoValue() public { bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); vm.expectRevert(NoGas.selector); vm.prank(address(mockChildERC20Bridge)); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index a9855d02a..86360c0a1 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -17,8 +17,6 @@ import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { - bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); - bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address constant ROOT_BRIDGE = address(3); string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; @@ -113,7 +111,8 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; - bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, address(rootToken), address(this), address(this), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, address(rootToken), address(this), address(this), withdrawAmount); vm.expectCall( address(mockAdaptor), @@ -129,7 +128,9 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi uint256 withdrawAmount = 7 ether; vm.expectEmit(address(childBridge)); - emit ChildChainERC20Withdraw(address(rootToken), address(childToken), address(this), address(this), withdrawAmount); + emit ChildChainERC20Withdraw( + address(rootToken), address(childToken), address(this), address(this), withdrawAmount + ); childBridge.withdraw{value: withdrawFee}(IChildERC20(address(childToken)), withdrawAmount); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index 47de22548..a1e00709a 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -17,8 +17,6 @@ import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { - bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); - bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address constant ROOT_BRIDGE = address(3); string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; @@ -113,7 +111,8 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; - bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, address(rootToken), address(this), address(this), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, address(rootToken), address(this), address(this), withdrawAmount); vm.expectCall( address(mockAdaptor), @@ -129,7 +128,9 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC uint256 withdrawAmount = 7 ether; vm.expectEmit(address(childBridge)); - emit ChildChainERC20Withdraw(address(rootToken), address(childToken), address(this), address(this), withdrawAmount); + emit ChildChainERC20Withdraw( + address(rootToken), address(childToken), address(this), address(this), withdrawAmount + ); childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); } @@ -179,7 +180,8 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC uint256 withdrawAmount = 7 ether; address receiver = address(123); - bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, address(rootToken), address(this), receiver, withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, address(rootToken), address(this), receiver, withdrawAmount); vm.expectCall( address(mockAdaptor), diff --git a/test/utils.t.sol b/test/utils.t.sol index e2ac1566a..b186fbbc4 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -8,6 +8,7 @@ import {MockAxelarGateway} from "../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../src/test/root/MockAxelarGasService.sol"; import {RootERC20Bridge, IERC20Metadata} from "../src/root/RootERC20Bridge.sol"; import {ChildERC20Bridge} from "../src/child/ChildERC20Bridge.sol"; +import {ChildAxelarBridgeAdaptor} from "../src/child/ChildAxelarBridgeAdaptor.sol"; import {WETH} from "../src/test/root/WETH.sol"; import {IWETH} from "../src/interfaces/root/IWETH.sol"; @@ -15,7 +16,45 @@ import {IChildERC20, ChildERC20} from "../src/child/ChildERC20.sol"; import {RootAxelarBridgeAdaptor} from "../src/root/RootAxelarBridgeAdaptor.sol"; contract Utils is Test { - function integrationSetup( + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); + + function childIntegrationSetup() + public + returns ( + ChildERC20Bridge childBridge, + ChildAxelarBridgeAdaptor childBridgeAdaptor, + address rootToken, + address rootIMX, + ChildERC20 childTokenTemplate, + MockAxelarGasService axelarGasService, + MockAxelarGateway mockAxelarGateway + ) + { + string memory rootAdaptor = Strings.toHexString(address(99999)); + rootIMX = address(555555); + rootToken = address(44444); + + axelarGasService = new MockAxelarGasService(); + mockAxelarGateway = new MockAxelarGateway(); + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(1), "Test", "TST", 18); + childBridge = new ChildERC20Bridge(); + childBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockAxelarGateway)); + childBridge.initialize(address(childBridgeAdaptor), rootAdaptor, address(childTokenTemplate), "ROOT", rootIMX); + childBridgeAdaptor.initialize("ROOT", address(childBridge), address(axelarGasService)); + + bytes memory mapTokenData = abi.encode(MAP_TOKEN_SIG, rootToken, "TEST NAME", "TNM", 18); + vm.prank(address(childBridgeAdaptor)); + childBridge.onMessageReceive("ROOT", rootAdaptor, mapTokenData); + + ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(address(rootToken))); + vm.prank(address(childBridge)); + childToken.mint(address(this), 1000000 ether); + childToken.approve(address(childBridge), 1000000 ether); + } + + function rootIntegrationSetup( address childBridge, address childBridgeAdaptor, string memory childBridgeName, From 1bf7911676aca9ca015184b205757d40a84b4d72 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 13:58:30 +1100 Subject: [PATCH 09/49] Tests almost complete --- script/DeployRootContracts.s.sol | 11 +-- script/InitializeRootContracts.s.sol | 6 +- src/child/ChildAxelarBridgeAdaptor.sol | 4 +- src/interfaces/root/IRootERC20Bridge.sol | 26 ++++++++ src/root/RootAxelarBridgeAdaptor.sol | 32 +++++---- src/root/RootERC20Bridge.sol | 70 +++++++++++++++++++- src/test/child/ChildERC20FailOnBurn.sol | 2 +- src/test/root/MockAxelarGateway.sol | 4 ++ src/test/root/StubRootBridge.sol | 1 + test/unit/root/RootAxelarBridgeAdaptor.t.sol | 18 +++-- test/unit/root/RootERC20Bridge.t.sol | 41 +++++++++--- test/utils.t.sol | 9 ++- 12 files changed, 174 insertions(+), 50 deletions(-) diff --git a/script/DeployRootContracts.s.sol b/script/DeployRootContracts.s.sol index a1f49fceb..8c04b8b5f 100644 --- a/script/DeployRootContracts.s.sol +++ b/script/DeployRootContracts.s.sol @@ -21,6 +21,7 @@ contract DeployRootContracts is Script { uint256 rootPrivateKey = vm.envUint("ROOT_PRIVATE_KEY"); string memory rootRpcUrl = vm.envString("ROOT_RPC_URL"); string memory deployEnvironment = vm.envString("ENVIRONMENT"); + address rootGateway = vm.envAddress("ROOT_GATEWAY_ADDRESS"); /** * DEPLOY ROOT CHAIN CONTRACTS @@ -35,7 +36,9 @@ contract DeployRootContracts is Script { rootChainChildTokenTemplate.initialize(address(123), "TEMPLATE", "TPT", 18); RootERC20Bridge rootERC20BridgeImplementation = new RootERC20Bridge(); - rootERC20BridgeImplementation.initialize(address(1), address(1), "filler", address(1), address(1), address(1)); + rootERC20BridgeImplementation.initialize( + address(1), address(1), "filler", address(1), address(1), address(1), "filler_child_name" + ); TransparentUpgradeableProxy rootERC20BridgeProxy = new TransparentUpgradeableProxy( address(rootERC20BridgeImplementation), address(proxyAdmin), @@ -44,10 +47,8 @@ contract DeployRootContracts is Script { // TODO add dummy initialize of implementation contracts! - RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor(); - rootBridgeAdaptorImplementation.initialize( - address(rootERC20BridgeImplementation), "Filler", address(1), address(1) - ); + RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor(rootGateway); + rootBridgeAdaptorImplementation.initialize(address(rootERC20BridgeImplementation), "Filler", address(1)); TransparentUpgradeableProxy rootBridgeAdaptorProxy = new TransparentUpgradeableProxy( address(rootBridgeAdaptorImplementation), address(proxyAdmin), diff --git a/script/InitializeRootContracts.s.sol b/script/InitializeRootContracts.s.sol index c9a25c2c3..d29ee55cd 100644 --- a/script/InitializeRootContracts.s.sol +++ b/script/InitializeRootContracts.s.sol @@ -24,7 +24,6 @@ struct InitializeRootContractsParams { address rootIMXToken; address rootWETHToken; string childChainName; - address rootGateway; address rootGasService; } @@ -41,7 +40,6 @@ contract InitializeRootContracts is Script { rootIMXToken: vm.envAddress("ROOT_IMX_ADDRESS"), rootWETHToken: vm.envAddress("ROOT_WETH_ADDRESS"), childChainName: vm.envString("CHILD_CHAIN_NAME"), - rootGateway: vm.envAddress("ROOT_GATEWAY_ADDRESS"), rootGasService: vm.envAddress("ROOT_GAS_SERVICE_ADDRESS") }); @@ -60,13 +58,13 @@ contract InitializeRootContracts is Script { childBridgeAdaptorChecksum, params.rootChainChildTokenTemplate, params.rootIMXToken, - params.rootWETHToken + params.rootWETHToken, + params.childChainName ); params.rootBridgeAdaptor.initialize( address(params.rootERC20Bridge), // root bridge params.childChainName, // child chain name - params.rootGateway, // axelar gateway params.rootGasService // axelar gas service ); diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 144f6cac3..cb80d2456 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -22,7 +22,6 @@ contract ChildAxelarBridgeAdaptor is /// @notice Address of bridge to relay messages to. IChildERC20Bridge public childBridge; IAxelarGasService public gasService; - string public rootBridgeAdaptor; string public rootChain; constructor(address _gateway) AxelarExecutable(_gateway) {} @@ -39,7 +38,6 @@ contract ChildAxelarBridgeAdaptor is childBridge = IChildERC20Bridge(_childBridge); rootChain = _rootChain; gasService = IAxelarGasService(_gasService); - rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); } /** @@ -54,7 +52,7 @@ contract ChildAxelarBridgeAdaptor is } // Load from storage. - string memory _rootBridgeAdaptor = rootBridgeAdaptor; + string memory _rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); string memory _rootChain = rootChain; gasService.payNativeGasForContractCall{value: msg.value}( diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index edea801e9..e3fe11299 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -5,6 +5,14 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER interface IRootERC20Bridge { function childBridgeAdaptor() external view returns (string memory); + /** + * @notice Receives a bridge message from child chain, parsing the message type then executing. + * @param sourceChain The chain the message originated from. + * @param sourceAddress The address the message originated from. + * @param data The data payload of the message. + */ + function onMessageReceive(string calldata sourceChain, string calldata sourceAddress, bytes calldata data) + external; /** * @notice Initiate sending a mapToken message to the child chain. @@ -60,6 +68,14 @@ interface IRootERC20BridgeEvents { address indexed receiver, uint256 amount ); + + event RootChainERC20Withdraw( + address indexed rootToken, + address indexed childToken, + address withdrawer, + address indexed receiver, + uint256 amount + ); } interface IRootERC20BridgeErrors { @@ -69,6 +85,8 @@ interface IRootERC20BridgeErrors { error ZeroAmount(); /// @notice Error when a zero address is given when not valid. error ZeroAddress(); + /// @notice Error when the child chain name is invalid. + error InvalidChildChain(); /// @notice Error when a token is already mapped. error AlreadyMapped(); /// @notice Error when a token is not mapped when it should be. @@ -83,4 +101,12 @@ interface IRootERC20BridgeErrors { error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); /// @notice Error when the given child chain bridge adaptor is invalid. error InvalidChildERC20BridgeAdaptor(); + /// @notice Error when a message received has invalid data. + error InvalidData(); + /// @notice Error when a message received has invalid source address. + error InvalidSourceAddress(); + /// @notice Error when a message received has invalid source chain. + error InvalidSourceChain(); + /// @notice Error when caller is not the root bridge adaptor but should be. + error NotBridgeAdaptor(); } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 8c769959d..7c9d3877c 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; +import {AxelarExecutable} from "@axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -20,6 +21,7 @@ import {IRootERC20Bridge} from "../interfaces/root/IRootERC20Bridge.sol"; * @notice RootAxelarBridgeAdaptor is a bridge adaptor that allows the RootERC20Bridge to communicate with the Axelar Gateway. */ contract RootAxelarBridgeAdaptor is + AxelarExecutable, Initializable, IRootERC20BridgeAdaptor, IRootAxelarBridgeAdaptorEvents, @@ -27,34 +29,30 @@ contract RootAxelarBridgeAdaptor is { using SafeERC20 for IERC20Metadata; - address public rootBridge; + IRootERC20Bridge public rootBridge; string public childBridgeAdaptor; string public childChain; - IAxelarGateway public axelarGateway; IAxelarGasService public gasService; mapping(uint256 => string) public chainIdToChainName; + constructor(address _gateway) AxelarExecutable(_gateway) {} + /** * @notice Initilization function for RootAxelarBridgeAdaptor. * @param _rootBridge Address of root bridge contract. * @param _childChain Name of child chain. - * @param _axelarGateway Address of Axelar Gateway contract. * @param _gasService Address of Axelar Gas Service contract. */ - function initialize(address _rootBridge, string memory _childChain, address _axelarGateway, address _gasService) - public - initializer - { - if (_rootBridge == address(0) || _axelarGateway == address(0) || _gasService == address(0)) { + function initialize(address _rootBridge, string memory _childChain, address _gasService) public initializer { + if (_rootBridge == address(0) || _gasService == address(0)) { revert ZeroAddresses(); } if (bytes(_childChain).length == 0) { revert InvalidChildChain(); } - rootBridge = _rootBridge; + rootBridge = IRootERC20Bridge(_rootBridge); childChain = _childChain; - axelarGateway = IAxelarGateway(_axelarGateway); gasService = IAxelarGasService(_gasService); } @@ -66,7 +64,7 @@ contract RootAxelarBridgeAdaptor is if (msg.value == 0) { revert NoGas(); } - if (msg.sender != rootBridge) { + if (msg.sender != address(rootBridge)) { revert CallerNotBridge(); } @@ -79,7 +77,17 @@ contract RootAxelarBridgeAdaptor is address(this), _childChain, _childBridgeAdaptor, payload, refundRecipient ); - axelarGateway.callContract(_childChain, _childBridgeAdaptor, payload); + gateway.callContract(_childChain, _childBridgeAdaptor, payload); emit AxelarMessage(_childChain, _childBridgeAdaptor, payload); } + + /** + * @dev This function is called by the parent `AxelarExecutable` contract to execute the payload. + */ + function _execute(string calldata sourceChain_, string calldata sourceAddress_, bytes calldata payload_) + internal + override + { + rootBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_); + } } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 263a1ea8f..5ba1511cc 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -7,6 +7,7 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol"; import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bridge.sol"; import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol"; @@ -37,6 +38,7 @@ contract RootERC20Bridge is bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address public constant NATIVE_ETH = address(0xeee); IRootERC20BridgeAdaptor public rootBridgeAdaptor; @@ -52,6 +54,8 @@ contract RootERC20Bridge is address public childETHToken; /// @dev The address of the wETH ERC20 token on L1. address public rootWETHToken; + /// @dev The name of the chain that this bridge is connected to. + string public childChain; /** * @notice Initilization function for RootERC20Bridge. @@ -61,6 +65,7 @@ contract RootERC20Bridge is * @param newChildTokenTemplate Address of child token template to clone. * @param newRootIMXToken Address of ERC20 IMX on the root chain. * @param newRootWETHToken Address of ERC20 WETH on the root chain. + * @param newChildChain Name of child chain. * @dev Can only be called once. */ function initialize( @@ -69,7 +74,8 @@ contract RootERC20Bridge is string memory newChildBridgeAdaptor, address newChildTokenTemplate, address newRootIMXToken, - address newRootWETHToken + address newRootWETHToken, + string memory newChildChain ) public initializer { if ( newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0) @@ -80,6 +86,10 @@ contract RootERC20Bridge is if (bytes(newChildBridgeAdaptor).length == 0) { revert InvalidChildERC20BridgeAdaptor(); } + if (bytes(newChildChain).length == 0) { + revert InvalidChildChain(); + } + childERC20Bridge = newChildERC20Bridge; childTokenTemplate = newChildTokenTemplate; rootIMXToken = newRootIMXToken; @@ -89,6 +99,7 @@ contract RootERC20Bridge is ); rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); childBridgeAdaptor = newChildBridgeAdaptor; + childChain = newChildChain; } function updateRootBridgeAdaptor(address newRootBridgeAdaptor) external onlyOwner { @@ -103,6 +114,35 @@ contract RootERC20Bridge is */ receive() external payable {} + /** + * @inheritdoc IRootERC20Bridge + * @dev This is only callable by the root chain bridge adaptor. + * @dev Validates `sourceAddress` is the child chain's bridgeAdaptor. + */ + function onMessageReceive(string calldata messageSourceChain, string calldata sourceAddress, bytes calldata data) + external + override + { + if (msg.sender != address(rootBridgeAdaptor)) { + revert NotBridgeAdaptor(); + } + if (!Strings.equal(messageSourceChain, childChain)) { + revert InvalidSourceChain(); + } + if (!Strings.equal(sourceAddress, childBridgeAdaptor)) { + revert InvalidSourceAddress(); + } + if (data.length == 0) { + revert InvalidData(); + } + + if (bytes32(data[:32]) == WITHDRAW_SIG) { + _withdraw(data[32:]); + } else { + revert InvalidData(); + } + } + /** * @inheritdoc IRootERC20Bridge * @dev TODO when this becomes part of the deposit flow on a token's first bridge, this logic will need to be mostly moved into an internal function. @@ -270,4 +310,32 @@ contract RootERC20Bridge is emit ChildChainERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount); } } + + function _withdraw(bytes memory data) private { + (address rootToken, address withdrawer, address receiver, uint256 amount) = + abi.decode(data, (address, address, address, uint256)); + address childToken = rootTokenToChildToken[rootToken]; + if (childToken == address(0)) { + revert NotMapped(); + } + _executeTransfer(rootToken, childToken, withdrawer, receiver, amount); + } + + function _executeTransfer( + address rootToken, + address childToken, + address withdrawer, + address receiver, + uint256 amount + ) internal { + // TODO when withdrawing ETH/WETH, this next section will also need to check for the withdrawal of WETH (i.e. rootToken == NATIVE_ETH || rootToken == CHILD_WETH) + // Tests for this NATIVE_ETH branch not yet written. This should come as part of that PR. + if (rootToken == NATIVE_ETH) { + Address.sendValue(payable(receiver), amount); + } else { + IERC20Metadata(rootToken).safeTransfer(receiver, amount); + } + // slither-disable-next-line reentrancy-events + emit RootChainERC20Withdraw(address(rootToken), childToken, withdrawer, receiver, amount); + } } diff --git a/src/test/child/ChildERC20FailOnBurn.sol b/src/test/child/ChildERC20FailOnBurn.sol index f287319ca..5fd11ad86 100644 --- a/src/test/child/ChildERC20FailOnBurn.sol +++ b/src/test/child/ChildERC20FailOnBurn.sol @@ -12,7 +12,7 @@ import "../../child/ChildERC20.sol"; */ // solhint-disable reason-string contract ChildERC20FailOnBurn is ChildERC20 { - function burn(address account, uint256 amount) public virtual override returns (bool) { + function burn(address, /*account*/ uint256 /*amount*/ ) public virtual override returns (bool) { return false; } } diff --git a/src/test/root/MockAxelarGateway.sol b/src/test/root/MockAxelarGateway.sol index 756a61a62..8b16342a2 100644 --- a/src/test/root/MockAxelarGateway.sol +++ b/src/test/root/MockAxelarGateway.sol @@ -4,4 +4,8 @@ pragma solidity ^0.8.21; // @dev A contract for ensuring the Axelar Gateway is called correctly during unit tests. contract MockAxelarGateway { function callContract(string memory childChain, string memory childBridgeAdaptor, bytes memory payload) external {} + + function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) { + return true; + } } diff --git a/src/test/root/StubRootBridge.sol b/src/test/root/StubRootBridge.sol index da4081155..25806be91 100644 --- a/src/test/root/StubRootBridge.sol +++ b/src/test/root/StubRootBridge.sol @@ -7,4 +7,5 @@ contract StubRootBridge { function childBridgeAdaptor() external pure returns (string memory) { return Strings.toHexString(address(9999)); } + function onMessageReceive(string calldata, string calldata, bytes calldata) external {} } diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 2374409f2..5bd34aa86 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -32,30 +32,28 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR stubRootBridge = new StubRootBridge(); childBridgeAdaptor = stubRootBridge.childBridgeAdaptor(); - axelarAdaptor = new RootAxelarBridgeAdaptor(); - axelarAdaptor.initialize( - address(stubRootBridge), CHILD_CHAIN_NAME, address(mockAxelarGateway), address(axelarGasService) - ); + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + axelarAdaptor.initialize(address(stubRootBridge), CHILD_CHAIN_NAME, address(axelarGasService)); vm.deal(address(stubRootBridge), 99999999999); } function test_Constructor() public { - assertEq(axelarAdaptor.rootBridge(), address(stubRootBridge), "rootBridge not set"); + assertEq(address(axelarAdaptor.rootBridge()), address(stubRootBridge), "rootBridge not set"); assertEq(axelarAdaptor.childChain(), CHILD_CHAIN_NAME, "childChain not set"); - assertEq(address(axelarAdaptor.axelarGateway()), address(mockAxelarGateway), "axelarGateway not set"); + assertEq(address(axelarAdaptor.gateway()), address(mockAxelarGateway), "axelarGateway not set"); assertEq(address(axelarAdaptor.gasService()), address(axelarGasService), "axelarGasService not set"); } function test_RevertWhen_InitializerGivenZeroAddress() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(ZeroAddresses.selector); - newAdaptor.initialize(address(0), CHILD_CHAIN_NAME, address(mockAxelarGateway), address(axelarGasService)); + newAdaptor.initialize(address(0), CHILD_CHAIN_NAME, address(axelarGasService)); } function test_RevertWhen_ConstructorGivenEmptyChildChainName() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(InvalidChildChain.selector); - newAdaptor.initialize(address(this), "", address(mockAxelarGateway), address(axelarGasService)); + newAdaptor.initialize(address(this), "", address(axelarGasService)); } /// @dev For this unit test we just want to make sure the correct functions are called on the Axelar Gateway and Gas Service. diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index ef8ff0353..07d316ba2 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -53,7 +53,8 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN, - WRAPPED_ETH + WRAPPED_ETH, + CHILD_CHAIN_NAME ); } @@ -65,6 +66,11 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid assertEq(address(rootBridge.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); assertEq(rootBridge.childERC20Bridge(), CHILD_BRIDGE, "childERC20Bridge not set"); assertEq(rootBridge.childTokenTemplate(), address(token), "childTokenTemplate not set"); + assert(Strings.equal(rootBridge.childChain(), CHILD_CHAIN_NAME)); + assert(Strings.equal(CHILD_BRIDGE_ADAPTOR_STRING, rootBridge.childBridgeAdaptor())); + assertEq(address(token), rootBridge.childTokenTemplate(), "childTokenTemplate not set"); + assertEq(rootBridge.rootIMXToken(), IMX_TOKEN, "rootIMXToken not set"); + assertEq(rootBridge.rootWETHToken(), WRAPPED_ETH, "rootWETHToken not set"); } function test_RevertIfInitializeTwice() public { @@ -75,50 +81,67 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN, - WRAPPED_ETH + WRAPPED_ETH, + CHILD_CHAIN_NAME ); } function test_RevertIf_InitializeWithAZeroAddressRootAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1)); + bridge.initialize( + address(0), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1), CHILD_CHAIN_NAME + ); } function test_RevertIf_InitializeWithAZeroAddressChildBridge() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(0), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1)); + bridge.initialize( + address(1), address(0), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1), CHILD_CHAIN_NAME + ); } function test_RevertIf_InitializeWithEmptyChildAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(InvalidChildERC20BridgeAdaptor.selector); - bridge.initialize(address(1), address(1), "", address(1), address(1), address(1)); + bridge.initialize(address(1), address(1), "", address(1), address(1), address(1), CHILD_CHAIN_NAME); } function test_RevertIf_InitializeWithAZeroAddressTokenTemplate() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(0), address(1), address(1)); + bridge.initialize( + address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(0), address(1), address(1), CHILD_CHAIN_NAME + ); } function test_RevertIf_InitializeWithAZeroAddressIMXToken() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(0), address(1)); + bridge.initialize( + address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(0), address(1), CHILD_CHAIN_NAME + ); } function test_RevertIf_InitializeWithAZeroAddressWETHToken() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(0)); + bridge.initialize( + address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(0), CHILD_CHAIN_NAME + ); } function test_RevertIf_InitializeWithAZeroAddressAll() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(0), "", address(0), address(0), address(0)); + bridge.initialize(address(0), address(0), "", address(0), address(0), address(0), CHILD_CHAIN_NAME); + } + + function test_RevertIf_InitializeWithEmptyChildName() public { + RootERC20Bridge bridge = new RootERC20Bridge(); + vm.expectRevert(InvalidChildChain.selector); + bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1), ""); } /** diff --git a/test/utils.t.sol b/test/utils.t.sol index b186fbbc4..9e9b86cea 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -86,7 +86,7 @@ contract Utils is Test { mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); - axelarAdaptor = new RootAxelarBridgeAdaptor(); + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); rootBridge.initialize( address(axelarAdaptor), @@ -94,12 +94,11 @@ contract Utils is Test { Strings.toHexString(childBridgeAdaptor), address(token), imxTokenAddress, - wethTokenAddress + wethTokenAddress, + "CHILD" ); - axelarAdaptor.initialize( - address(rootBridge), childBridgeName, address(mockAxelarGateway), address(axelarGasService) - ); + axelarAdaptor.initialize(address(rootBridge), childBridgeName, address(axelarGasService)); } function setupDeposit( From e3efc7d9aaf9ccad29377b1b5c752e925cce4044 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 13:59:25 +1100 Subject: [PATCH 10/49] Add new test files --- .../RootERC20BridgeWithdraw.t.sol | 91 ++++++++++ .../RootAxelarBridgeAdaptorWithdraw.t.sol | 54 ++++++ .../withdrawals/RootERC20BridgeWithdraw.t.sol | 167 ++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol create mode 100644 test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol create mode 100644 test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol new file mode 100644 index 000000000..68cf79d6d --- /dev/null +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {RootERC20Bridge, IRootERC20BridgeEvents, IERC20Metadata} from "../../../../src/root/RootERC20Bridge.sol"; +import {RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents} from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; + +contract RootERC20BridgeWithdrawIntegrationTest is Test, IRootERC20BridgeEvents, IRootAxelarBridgeAdaptorEvents, Utils { + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "CHILD"; + address constant IMX_TOKEN_ADDRESS = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + + uint256 constant withdrawAmount = 0.5 ether; + + ERC20PresetMinterPauser public token; + ERC20PresetMinterPauser public imxToken; + RootERC20Bridge public rootBridge; + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); + + (imxToken, token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = + rootIntegrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH); + + // Need to first map the token. + rootBridge.mapToken{value:1}(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + } + + function test_withdraw_TransfersTokens() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 thisPreBal = token.balanceOf(address(this)); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 thisPostBal = token.balanceOf(address(this)); + uint256 bridgePostBal = token.balanceOf(address(rootBridge)); + + assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + + function test_withdraw_TransfersTokens_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 receiverPreBal = token.balanceOf(receiver); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 receiverPostBal = token.balanceOf(receiver); + uint256 bridgePostBal = token.balanceOf(address(rootBridge)); + + assertEq(receiverPostBal, receiverPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + + function test_withdraw_EmitsRootChainERC20WithdrawEvent() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw(address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), address(this), withdrawAmount); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } +} diff --git a/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol b/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol new file mode 100644 index 000000000..e162c59f7 --- /dev/null +++ b/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import { + RootAxelarBridgeAdaptor, + IRootAxelarBridgeAdaptorEvents, + IRootAxelarBridgeAdaptorErrors +} from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; +import {StubRootBridge} from "../../../../src/test/root/StubRootBridge.sol"; + +contract RootAxelarBridgeWithdrawAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IRootAxelarBridgeAdaptorErrors { + address constant CHILD_BRIDGE = address(3); + string public childBridgeAdaptor; + string constant CHILD_CHAIN_NAME = "test"; + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + + ERC20PresetMinterPauser public token; + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + StubRootBridge public stubRootBridge; + + function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + stubRootBridge = new StubRootBridge(); + childBridgeAdaptor = stubRootBridge.childBridgeAdaptor(); + + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + axelarAdaptor.initialize(address(stubRootBridge), CHILD_CHAIN_NAME, address(axelarGasService)); + vm.deal(address(stubRootBridge), 99999999999); + } + + + function test_execute_callsBridge() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectCall( + address(stubRootBridge), + abi.encodeWithSelector(stubRootBridge.onMessageReceive.selector, sourceChain, sourceAddress, payload) + ); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } +} diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol new file mode 100644 index 000000000..ec33afda5 --- /dev/null +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + RootERC20Bridge, + IRootERC20BridgeEvents, + IRootERC20BridgeErrors +} from "../../../../src/root/RootERC20Bridge.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootERC20BridgeErrors, Utils { + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string CHILD_BRIDGE_ADAPTOR_STRING = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + string constant CHILD_CHAIN_NAME = "test"; + address constant IMX_TOKEN = address(0xccc); + address constant WRAPPED_ETH = address(0xddd); + uint256 constant mapTokenFee = 300; + uint256 constant withdrawAmount = 0.5 ether; + + ERC20PresetMinterPauser public token; + RootERC20Bridge public rootBridge; + MockAdaptor public mockAxelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); + token.mint(address(this), 100 ether); + deployCodeTo("ERC20PresetMinterPauser.sol", abi.encode("ImmutableX", "IMX"), IMX_TOKEN); + + deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); + + rootBridge = new RootERC20Bridge(); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + + mockAxelarAdaptor = new MockAdaptor(); + + // The specific ERC20 token template does not matter for these unit tests + rootBridge.initialize( + address(mockAxelarAdaptor), + CHILD_BRIDGE, + CHILD_BRIDGE_ADAPTOR_STRING, + address(token), + IMX_TOKEN, + WRAPPED_ETH, + CHILD_CHAIN_NAME + ); + } + + function test_RevertsIf_WithdrawWithInvalidSender() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectRevert(NotBridgeAdaptor.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithInvalidSourceChain() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(InvalidSourceChain.selector); + rootBridge.onMessageReceive("ding_dong", CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithInvalidSourceAddress() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + + console2.log(CHILD_CHAIN_NAME); + console2.log(rootBridge.childChain()); + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(InvalidSourceAddress.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, "DING_DONG", data); + } + + function test_RevertsIf_OnMessageReceiveWithZeroDataLength() public { + bytes memory data; + + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(InvalidData.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithInvalidSignature() public { + bytes memory data = abi.encode(keccak256("RANDOM"), IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(InvalidData.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithUnmappedToken() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(NotMapped.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_onMessageReceive_TransfersTokens() public { + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + uint256 thisPreBal = token.balanceOf(address(this)); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(token.balanceOf(address(this)), thisPreBal + withdrawAmount, "Tokens not transferred to receiver"); + assertEq(token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge"); + } + + function test_onMessageReceive_TransfersTokens_DifferentReceiver() public { + address receiver = address(123456); + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + uint256 receiverPreBal = token.balanceOf(receiver); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(token.balanceOf(receiver), receiverPreBal + withdrawAmount, "Tokens not transferred to receiver"); + assertEq(token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge"); + } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent() public { + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw(address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(123456); + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), receiver, withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw(address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + +} From 15e208b65d70bb4c5b050843dc6e0d3e29750edd Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 14:06:54 +1100 Subject: [PATCH 11/49] Integration tests complete --- src/test/root/StubRootBridge.sol | 1 + .../RootERC20BridgeWithdraw.t.sol | 83 +++++++++++++++++-- .../RootAxelarBridgeAdaptorWithdraw.t.sol | 1 - .../withdrawals/RootERC20BridgeWithdraw.t.sol | 25 ++++-- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/test/root/StubRootBridge.sol b/src/test/root/StubRootBridge.sol index 25806be91..e8fa980c0 100644 --- a/src/test/root/StubRootBridge.sol +++ b/src/test/root/StubRootBridge.sol @@ -7,5 +7,6 @@ contract StubRootBridge { function childBridgeAdaptor() external pure returns (string memory) { return Strings.toHexString(address(9999)); } + function onMessageReceive(string calldata, string calldata, bytes calldata) external {} } diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 68cf79d6d..8e54d0ed6 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -7,12 +7,25 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; -import {RootERC20Bridge, IRootERC20BridgeEvents, IERC20Metadata} from "../../../../src/root/RootERC20Bridge.sol"; -import {RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents} from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; +import { + RootERC20Bridge, + IRootERC20BridgeEvents, + IERC20Metadata, + IRootERC20BridgeErrors +} from "../../../../src/root/RootERC20Bridge.sol"; +import { + RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents +} from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; import {WETH} from "../../../../src/test/root/WETH.sol"; -contract RootERC20BridgeWithdrawIntegrationTest is Test, IRootERC20BridgeEvents, IRootAxelarBridgeAdaptorEvents, Utils { +contract RootERC20BridgeWithdrawIntegrationTest is + Test, + IRootERC20BridgeErrors, + IRootERC20BridgeEvents, + IRootAxelarBridgeAdaptorEvents, + Utils +{ address constant CHILD_BRIDGE = address(3); address constant CHILD_BRIDGE_ADAPTOR = address(4); string constant CHILD_CHAIN_NAME = "CHILD"; @@ -36,11 +49,51 @@ contract RootERC20BridgeWithdrawIntegrationTest is Test, IRootERC20BridgeEvents, rootIntegrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH); // Need to first map the token. - rootBridge.mapToken{value:1}(token); + rootBridge.mapToken{value: 1}(token); // And give the bridge some tokens token.transfer(address(rootBridge), 100 ether); } + function test_RevertsIf_WithdrawWithInvalidSourceAddress() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectRevert(InvalidSourceChain.selector); + axelarAdaptor.execute(commandId, "INVALID", sourceAddress, data); + } + + function test_RevertsIf_WithdrawWithInvalidSourceChain() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = Strings.toHexString(address(123)); + + vm.expectRevert(InvalidSourceAddress.selector); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_RevertsIf_MessageWithEmptyData() public { + bytes memory data; + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectRevert(InvalidData.selector); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_RevertsIf_MessageWithInvalidSignature() public { + bytes memory data = abi.encode("INVALID_SIG", address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectRevert(InvalidData.selector); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + function test_withdraw_TransfersTokens() public { bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); @@ -85,7 +138,27 @@ contract RootERC20BridgeWithdrawIntegrationTest is Test, IRootERC20BridgeEvents, string memory sourceAddress = rootBridge.childBridgeAdaptor(); vm.expectEmit(); - emit RootChainERC20Withdraw(address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), address(this), withdrawAmount); + emit RootChainERC20Withdraw( + address(token), + rootBridge.rootTokenToChildToken(address(token)), + address(this), + address(this), + withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_withdraw_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), receiver, withdrawAmount + ); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } } diff --git a/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol b/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol index e162c59f7..6b997c686 100644 --- a/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol @@ -37,7 +37,6 @@ contract RootAxelarBridgeWithdrawAdaptorTest is Test, IRootAxelarBridgeAdaptorEv vm.deal(address(stubRootBridge), 99999999999); } - function test_execute_callsBridge() public { bytes32 commandId = bytes32("testCommandId"); string memory sourceChain = "test"; diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index ec33afda5..bc1c1cbac 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -5,9 +5,7 @@ import {Test, console2} from "forge-std/Test.sol"; import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import { - RootERC20Bridge, - IRootERC20BridgeEvents, - IRootERC20BridgeErrors + RootERC20Bridge, IRootERC20BridgeEvents, IRootERC20BridgeErrors } from "../../../../src/root/RootERC20Bridge.sol"; import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; @@ -116,7 +114,9 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); assertEq(token.balanceOf(address(this)), thisPreBal + withdrawAmount, "Tokens not transferred to receiver"); - assertEq(token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge"); + assertEq( + token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge" + ); } function test_onMessageReceive_TransfersTokens_DifferentReceiver() public { @@ -134,7 +134,9 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); assertEq(token.balanceOf(receiver), receiverPreBal + withdrawAmount, "Tokens not transferred to receiver"); - assertEq(token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge"); + assertEq( + token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge" + ); } function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent() public { @@ -145,7 +147,13 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); vm.expectEmit(); - emit RootChainERC20Withdraw(address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), address(this), withdrawAmount); + emit RootChainERC20Withdraw( + address(token), + rootBridge.rootTokenToChildToken(address(token)), + address(this), + address(this), + withdrawAmount + ); vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } @@ -159,9 +167,10 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), receiver, withdrawAmount); vm.expectEmit(); - emit RootChainERC20Withdraw(address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), receiver, withdrawAmount); + emit RootChainERC20Withdraw( + address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), receiver, withdrawAmount + ); vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } - } From 9b934277e8f4ba86bae4994b67ad38c9b4812c22 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 15:44:31 +1100 Subject: [PATCH 12/49] Add IMX deposit limit --- README.md | 4 + script/DeployRootContracts.s.sol | 6 +- script/InitializeRootContracts.s.sol | 7 +- src/interfaces/root/IRootERC20Bridge.sol | 8 ++ src/root/RootERC20Bridge.sol | 36 ++++++- test/integration/root/RootERC20Bridge.t.sol | 6 +- test/unit/root/RootERC20Bridge.t.sol | 105 ++++++++++++++++++-- test/utils.t.sol | 6 +- 8 files changed, 159 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b8d43b610..e7054c68b 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,12 @@ CHILD_GAS_SERVICE_ADDRESS= ROOT_CHAIN_NAME="ROOT" CHILD_CHAIN_NAME="CHILD" ROOT_IMX_ADDRESS= +INITIAL_IMX_CUMULATIVE_DEPOSIT_LIMIT="0" ``` where `{ROOT,CHILD}_{GATEWAY,GAS_SERVICE}_ADDRESS` refers to the gateway and gas service addresses used by Axelar. +`INITIAL_IMX_CUMULATIVE_DEPOSIT_LIMIT` refers to the cumulative amount of IMX that can be deposited. A value of `0` indicated unlimited. + We can just use dummy gateway/gas service addresses if we only want to test the deployment, and not bridging functionality. If wanting to use dummy addresses, any valid Ethereum address can be used here. 4. Run the deploy script. @@ -138,6 +141,7 @@ ROOT_GATEWAY_ADDRESS="0x013459EC3E8Aeced878C5C4bFfe126A366cd19E9" CHILD_GATEWAY_ADDRESS="0xc7B788E88BAaB770A6d4936cdcCcd5250E1bbAd8" ROOT_GAS_SERVICE_ADDRESS="0x28f8B50E1Be6152da35e923602a2641491E71Ed8" CHILD_GAS_SERVICE_ADDRESS="0xC573c722e21eD7fadD38A8f189818433e01Ae466" +INITIAL_IMX_CUMULATIVE_DEPOSIT_LIMIT="0" ENVIRONMENT="local" ``` (Note that `{ROOT,CHILD}_PRIVATE_KEY` can be any of the standard localhost private keys that get funded) diff --git a/script/DeployRootContracts.s.sol b/script/DeployRootContracts.s.sol index a1f49fceb..fee8e860c 100644 --- a/script/DeployRootContracts.s.sol +++ b/script/DeployRootContracts.s.sol @@ -35,15 +35,15 @@ contract DeployRootContracts is Script { rootChainChildTokenTemplate.initialize(address(123), "TEMPLATE", "TPT", 18); RootERC20Bridge rootERC20BridgeImplementation = new RootERC20Bridge(); - rootERC20BridgeImplementation.initialize(address(1), address(1), "filler", address(1), address(1), address(1)); + rootERC20BridgeImplementation.initialize( + address(1), address(1), "filler", address(1), address(1), address(1), 1 + ); TransparentUpgradeableProxy rootERC20BridgeProxy = new TransparentUpgradeableProxy( address(rootERC20BridgeImplementation), address(proxyAdmin), "" ); - // TODO add dummy initialize of implementation contracts! - RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor(); rootBridgeAdaptorImplementation.initialize( address(rootERC20BridgeImplementation), "Filler", address(1), address(1) diff --git a/script/InitializeRootContracts.s.sol b/script/InitializeRootContracts.s.sol index c9a25c2c3..8ea85a593 100644 --- a/script/InitializeRootContracts.s.sol +++ b/script/InitializeRootContracts.s.sol @@ -26,6 +26,7 @@ struct InitializeRootContractsParams { string childChainName; address rootGateway; address rootGasService; + uint256 initialIMXCumulativeDepositLimit; } contract InitializeRootContracts is Script { @@ -42,7 +43,8 @@ contract InitializeRootContracts is Script { rootWETHToken: vm.envAddress("ROOT_WETH_ADDRESS"), childChainName: vm.envString("CHILD_CHAIN_NAME"), rootGateway: vm.envAddress("ROOT_GATEWAY_ADDRESS"), - rootGasService: vm.envAddress("ROOT_GAS_SERVICE_ADDRESS") + rootGasService: vm.envAddress("ROOT_GAS_SERVICE_ADDRESS"), + initialIMXCumulativeDepositLimit: vm.envUint("INITIAL_IMX_CUMULATIVE_DEPOSIT_LIMIT") }); string[] memory checksumInputs = Utils.getChecksumInputs(params.childBridgeAdaptor); @@ -60,7 +62,8 @@ contract InitializeRootContracts is Script { childBridgeAdaptorChecksum, params.rootChainChildTokenTemplate, params.rootIMXToken, - params.rootWETHToken + params.rootWETHToken, + params.initialIMXCumulativeDepositLimit ); params.rootBridgeAdaptor.initialize( diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 75fa06c46..83837221b 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -35,6 +35,10 @@ interface IRootERC20Bridge { } interface IRootERC20BridgeEvents { + /// @notice Emitted when the child chain bridge adaptor is updated. + event NewRootBridgeAdaptor(address oldRootBridgeAdaptor, address newRootBridgeAdaptor); + /// @notice Emitted when the IMX deposit limit is updated. + event NewImxDepositLimit(uint256 oldImxDepositLimit, uint256 newImxDepositLimit); /// @notice Emitted when a map token message is sent to the child chain. event L1TokenMapped(address indexed rootToken, address indexed childToken); /// @notice Emitted when an ERC20 deposit message is sent to the child chain. @@ -83,4 +87,8 @@ interface IRootERC20BridgeErrors { error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); /// @notice Error when the given child chain bridge adaptor is invalid. error InvalidChildERC20BridgeAdaptor(); + /// @notice Error when the total IMX deposit limit is exceeded + error ImxDepositLimitExceeded(); + /// @notice Error when the IMX deposit limit is set below the amount of IMX already deposited + error ImxDepositLimitTooLow(); } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 23ae28359..6611ecf7b 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -52,6 +52,9 @@ contract RootERC20Bridge is address public childETHToken; /// @dev The address of the wETH ERC20 token on L1. address public rootWETHToken; + /// @dev The maximum cumulative amount of IMX that can be deposited into the bridge. + /// @dev A limit of zero indicates unlimited. + uint256 public imxCumulativeDepositLimit; /** * @notice Initilization function for RootERC20Bridge. @@ -61,6 +64,7 @@ contract RootERC20Bridge is * @param newChildTokenTemplate Address of child token template to clone. * @param newRootIMXToken Address of ERC20 IMX on the root chain. * @param newRootWETHToken Address of ERC20 WETH on the root chain. + * @param newImxCumulativeDepositLimit The cumulative IMX deposit limit. * @dev Can only be called once. */ function initialize( @@ -69,7 +73,8 @@ contract RootERC20Bridge is string memory newChildBridgeAdaptor, address newChildTokenTemplate, address newRootIMXToken, - address newRootWETHToken + address newRootWETHToken, + uint256 newImxCumulativeDepositLimit ) public initializer { if ( newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0) @@ -89,15 +94,38 @@ contract RootERC20Bridge is ); rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); childBridgeAdaptor = newChildBridgeAdaptor; + imxCumulativeDepositLimit = newImxCumulativeDepositLimit; } + /** + * @notice Updates the root bridge adaptor. + * @param newRootBridgeAdaptor Address of new root bridge adaptor. + * @dev Can only be called by owner. + */ function updateRootBridgeAdaptor(address newRootBridgeAdaptor) external onlyOwner { if (newRootBridgeAdaptor == address(0)) { revert ZeroAddress(); } + emit NewRootBridgeAdaptor(address(rootBridgeAdaptor), newRootBridgeAdaptor); rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); } + // TODO add updating of child bridge adaptor. Part of SMR-1908 + + /** + * @notice Updates the IMX deposit limit. + * @param newImxCumulativeDepositLimit The new cumulative IMX deposit limit. + * @dev Can only be called by owner. + * @dev The limit can decrease, but it can never decrease to below the contract's IMX balance. + */ + function updateImxCumulativeDepositLimit(uint256 newImxCumulativeDepositLimit) external onlyOwner { + if (newImxCumulativeDepositLimit < IERC20Metadata(rootIMXToken).balanceOf(address(this))) { + revert ImxDepositLimitTooLow(); + } + emit NewImxDepositLimit(imxCumulativeDepositLimit, newImxCumulativeDepositLimit); + imxCumulativeDepositLimit = newImxCumulativeDepositLimit; + } + /** * @dev method to receive the ETH back from the WETH contract when it is unwrapped */ @@ -227,6 +255,12 @@ contract RootERC20Bridge is if (amount == 0) { revert ZeroAmount(); } + if ( + address(rootToken) == rootIMXToken && imxCumulativeDepositLimit != 0 + && IERC20Metadata(rootIMXToken).balanceOf(address(this)) + amount > imxCumulativeDepositLimit + ) { + revert ImxDepositLimitExceeded(); + } // ETH, WETH and IMX do not need to be mapped since it should have been mapped on initialization // ETH also cannot be transferred since it was received in the payable function call diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 374ad9ff0..cb20d3954 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -20,6 +20,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx address constant IMX_TOKEN_ADDRESS = address(0xccc); address constant NATIVE_ETH = address(0xeee); address constant WRAPPED_ETH = address(0xddd); + uint256 constant unlimitedDepositLimit = 0; uint256 constant mapTokenFee = 300; uint256 constant depositFee = 200; @@ -34,8 +35,9 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx function setUp() public { deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); - (imxToken, token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = - integrationSetup(CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH); + (imxToken, token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = integrationSetup( + CHILD_BRIDGE, CHILD_BRIDGE_ADAPTOR, CHILD_CHAIN_NAME, IMX_TOKEN_ADDRESS, WRAPPED_ETH, unlimitedDepositLimit + ); } /** diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index 901a8f0bf..b527ebff5 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -27,6 +27,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address constant WRAPPED_ETH = address(0xddd); uint256 constant mapTokenFee = 300; uint256 constant depositFee = 200; + uint256 constant unlimitedIMXDeposits = 0; ERC20PresetMinterPauser public token; RootERC20Bridge public rootBridge; @@ -53,7 +54,8 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN, - WRAPPED_ETH + WRAPPED_ETH, + unlimitedIMXDeposits ); } @@ -75,50 +77,112 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid CHILD_BRIDGE_ADAPTOR_STRING, address(token), IMX_TOKEN, - WRAPPED_ETH + WRAPPED_ETH, + unlimitedIMXDeposits ); } function test_RevertIf_InitializeWithAZeroAddressRootAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1)); + bridge.initialize( + address(0), + address(1), + CHILD_BRIDGE_ADAPTOR_STRING, + address(1), + address(1), + address(1), + unlimitedIMXDeposits + ); } function test_RevertIf_InitializeWithAZeroAddressChildBridge() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(0), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(1)); + bridge.initialize( + address(1), + address(0), + CHILD_BRIDGE_ADAPTOR_STRING, + address(1), + address(1), + address(1), + unlimitedIMXDeposits + ); } function test_RevertIf_InitializeWithEmptyChildAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(InvalidChildERC20BridgeAdaptor.selector); - bridge.initialize(address(1), address(1), "", address(1), address(1), address(1)); + bridge.initialize(address(1), address(1), "", address(1), address(1), address(1), unlimitedIMXDeposits); } function test_RevertIf_InitializeWithAZeroAddressTokenTemplate() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(0), address(1), address(1)); + bridge.initialize( + address(1), + address(1), + CHILD_BRIDGE_ADAPTOR_STRING, + address(0), + address(1), + address(1), + unlimitedIMXDeposits + ); } function test_RevertIf_InitializeWithAZeroAddressIMXToken() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(0), address(1)); + bridge.initialize( + address(1), + address(1), + CHILD_BRIDGE_ADAPTOR_STRING, + address(1), + address(0), + address(1), + unlimitedIMXDeposits + ); } function test_RevertIf_InitializeWithAZeroAddressWETHToken() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), address(1), CHILD_BRIDGE_ADAPTOR_STRING, address(1), address(1), address(0)); + bridge.initialize( + address(1), + address(1), + CHILD_BRIDGE_ADAPTOR_STRING, + address(1), + address(1), + address(0), + unlimitedIMXDeposits + ); } function test_RevertIf_InitializeWithAZeroAddressAll() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(0), "", address(0), address(0), address(0)); + bridge.initialize(address(0), address(0), "", address(0), address(0), address(0), unlimitedIMXDeposits); + } + + /** + * UPDATE IMX CUMULATIVE DEPOSIT LIMIT + */ + function test_RevertsIf_IMXDepositLimitTooLow() public { + uint256 imxCumulativeDepositLimit = 700; + uint256 depositAmount = imxCumulativeDepositLimit + 1; + + rootBridge.updateImxCumulativeDepositLimit(imxCumulativeDepositLimit); + + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, depositAmount, false); + + IERC20Metadata(IMX_TOKEN).approve(address(rootBridge), type(uint256).max); + + rootBridge.updateImxCumulativeDepositLimit(depositAmount); + + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), depositAmount); + + vm.expectRevert(ImxDepositLimitTooLow.selector); + rootBridge.updateImxCumulativeDepositLimit(imxCumulativeDepositLimit); } /** @@ -406,6 +470,29 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid * DEPOSIT TOKEN */ + function test_RevertsIf_IMXDepositLimitExceeded() public { + uint256 imxCumulativeDepositLimit = 700; + + uint256 amount = 300; + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); + + IERC20Metadata(IMX_TOKEN).approve(address(rootBridge), type(uint256).max); + + rootBridge.updateImxCumulativeDepositLimit(imxCumulativeDepositLimit); + + // Valid + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); + + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); + // Valid + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); + + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); + // Invalid + vm.expectRevert(ImxDepositLimitExceeded.selector); + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); + } + function test_depositCallsSendMessage() public { uint256 amount = 100; (, bytes memory predictedPayload) = diff --git a/test/utils.t.sol b/test/utils.t.sol index 7151afd95..79ea01580 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -20,7 +20,8 @@ contract Utils is Test { address childBridgeAdaptor, string memory childBridgeName, address imxTokenAddress, - address wethTokenAddress + address wethTokenAddress, + uint256 imxCumulativeDepositLimit ) public returns ( @@ -55,7 +56,8 @@ contract Utils is Test { Strings.toHexString(childBridgeAdaptor), address(token), imxTokenAddress, - wethTokenAddress + wethTokenAddress, + imxCumulativeDepositLimit ); axelarAdaptor.initialize( From ff6b92de888a24ab7907fe8bbc33e83498b6a37e Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 15:50:59 +1100 Subject: [PATCH 13/49] Address PR comment --- .../ChildAxelarBridgeWithdraw.t.sol | 355 ------------------ 1 file changed, 355 deletions(-) diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol index c2406f452..e52abd117 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol @@ -138,359 +138,4 @@ contract ChildERC20BridgeWithdrawIntegrationTest is vm.expectRevert(NoGas.selector); childBridge.withdraw(childToken, withdrawAmount); } - - // 7a8dc26796a1e50e6e190b70259f58f6a4edd5b22280ceecc82b687b8e982869 - // 000000000000000000000000000000000000000000000000000000000000ad9c - // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 - // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 - // 000000000000000000000000000000000000000000000000000000174876e7ff - // 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 - - // f20755ba - // 0000000000000000000000000000000000000000000000000000000000000040 - // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 - // 00000000000000000000000000000000000000000000000000000000000000a0 - // 2cef46a936bdc5b7e6e8c71aa04560c41cf7d88bb26901a7e7f4936ff02accad - // 000000000000000000000000000000000000000000000000000000000000ad9c - // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 - // 0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 - // 000000000000000000000000000000000000000000000000000000174876e7ff - - // bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); - // vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - // emit AxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); - - // vm.expectEmit(true, true, false, false, address(rootBridge)); - // emit L1TokenMapped(address(token), childToken); - - // // Instead of using expectCalls, we could use expectEmit in combination with mock contracts emitting events. - // // expectCalls requires less boilerplate and is less dependant on mock code. - // vm.expectCall( - // address(axelarAdaptor), - // mapTokenFee, - // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, payload, address(this)) - // ); - - // // These are calls that the axelarAdaptor should make. - // vm.expectCall( - // address(axelarGasService), - // mapTokenFee, - // abi.encodeWithSelector( - // axelarGasService.payNativeGasForContractCall.selector, - // address(axelarAdaptor), - // CHILD_CHAIN_NAME, - // Strings.toHexString(CHILD_BRIDGE_ADAPTOR), - // payload, - // address(this) - // ) - // ); - - // vm.expectCall( - // address(mockAxelarGateway), - // 0, - // abi.encodeWithSelector( - // mockAxelarGateway.callContract.selector, - // CHILD_CHAIN_NAME, - // Strings.toHexString(CHILD_BRIDGE_ADAPTOR), - // payload - // ) - // ); - - // // Check that we pay mapTokenFee to the axelarGasService. - // uint256 thisPreBal = address(this).balance; - // uint256 axelarGasServicePreBal = address(axelarGasService).balance; - - // rootBridge.mapToken{value: mapTokenFee}(token); - - // // Should update ETH balances as gas payment for message. - // assertEq(address(this).balance, thisPreBal - mapTokenFee, "ETH balance not decreased"); - // assertEq(address(axelarGasService).balance, axelarGasServicePreBal + mapTokenFee, "ETH not paid to gas service"); - - // assertEq(rootBridge.rootTokenToChildToken(address(token)), childToken, "childToken not set"); - // } - - // // TODO split into multiple tests - // function test_depositETH() public { - // uint256 tokenAmount = 300; - // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - - // (, bytes memory predictedPayload) = - // setupDeposit(NATIVE_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); - - // console2.logBytes(predictedPayload); - - // vm.expectEmit(address(axelarAdaptor)); - // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); - // vm.expectEmit(address(rootBridge)); - // emit NativeEthDeposit( - // address(NATIVE_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount - // ); - - // vm.expectCall( - // address(axelarAdaptor), - // depositFee, - // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) - // ); - - // vm.expectCall( - // address(axelarGasService), - // depositFee, - // abi.encodeWithSelector( - // axelarGasService.payNativeGasForContractCall.selector, - // address(axelarAdaptor), - // CHILD_CHAIN_NAME, - // childBridgeAdaptorString, - // predictedPayload, - // address(this) - // ) - // ); - - // vm.expectCall( - // address(mockAxelarGateway), - // 0, - // abi.encodeWithSelector( - // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload - // ) - // ); - - // uint256 bridgePreBal = address(rootBridge).balance; - - // uint256 thisNativePreBal = address(this).balance; - // uint256 gasServiceNativePreBal = address(axelarGasService).balance; - - // rootBridge.depositETH{value: tokenAmount + depositFee}(tokenAmount); - - // // Check that tokens are transferred - // assertEq(bridgePreBal + tokenAmount, address(rootBridge).balance, "ETH not transferred to bridge"); - // // Check that native asset transferred to gas service - // assertEq(thisNativePreBal - (depositFee + tokenAmount), address(this).balance, "ETH not paid from user"); - // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); - // } - - // // TODO split into multiple tests - // function test_depositIMXToken() public { - // uint256 tokenAmount = 300; - // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - - // (, bytes memory predictedPayload) = - // setupDeposit(IMX_TOKEN_ADDRESS, rootBridge, mapTokenFee, depositFee, tokenAmount, false); - - // vm.expectEmit(address(axelarAdaptor)); - // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); - // vm.expectEmit(address(rootBridge)); - // emit IMXDeposit(address(IMX_TOKEN_ADDRESS), address(this), address(this), tokenAmount); - - // vm.expectCall( - // address(axelarAdaptor), - // depositFee, - // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) - // ); - - // vm.expectCall( - // address(axelarGasService), - // depositFee, - // abi.encodeWithSelector( - // axelarGasService.payNativeGasForContractCall.selector, - // address(axelarAdaptor), - // CHILD_CHAIN_NAME, - // childBridgeAdaptorString, - // predictedPayload, - // address(this) - // ) - // ); - - // vm.expectCall( - // address(mockAxelarGateway), - // 0, - // abi.encodeWithSelector( - // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload - // ) - // ); - - // uint256 thisPreBal = imxToken.balanceOf(address(this)); - // uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); - - // uint256 thisNativePreBal = address(this).balance; - // uint256 gasServiceNativePreBal = address(axelarGasService).balance; - - // rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN_ADDRESS), tokenAmount); - - // // Check that tokens are transferred - // assertEq(thisPreBal - tokenAmount, imxToken.balanceOf(address(this)), "Tokens not transferred from user"); - // assertEq( - // bridgePreBal + tokenAmount, imxToken.balanceOf(address(rootBridge)), "Tokens not transferred to bridge" - // ); - // // Check that native asset transferred to gas service - // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); - // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); - // } - - // // TODO split into multiple tests - // function test_depositWETH() public { - // uint256 tokenAmount = 300; - // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - // (, bytes memory predictedPayload) = - // setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); - - // vm.expectEmit(address(axelarAdaptor)); - // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); - // vm.expectEmit(address(rootBridge)); - // emit WETHDeposit(address(WRAPPED_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount); - // vm.expectCall( - // address(axelarAdaptor), - // depositFee, - // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) - // ); - - // vm.expectCall( - // address(axelarGasService), - // depositFee, - // abi.encodeWithSelector( - // axelarGasService.payNativeGasForContractCall.selector, - // address(axelarAdaptor), - // CHILD_CHAIN_NAME, - // childBridgeAdaptorString, - // predictedPayload, - // address(this) - // ) - // ); - - // vm.expectCall( - // address(mockAxelarGateway), - // 0, - // abi.encodeWithSelector( - // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload - // ) - // ); - - // uint256 thisPreBal = IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)); - // uint256 bridgePreBal = address(rootBridge).balance; - - // uint256 thisNativePreBal = address(this).balance; - // uint256 gasServiceNativePreBal = address(axelarGasService).balance; - - // rootBridge.deposit{value: depositFee}(IERC20Metadata(WRAPPED_ETH), tokenAmount); - - // // Check that tokens are transferred - // assertEq( - // thisPreBal - tokenAmount, - // IERC20Metadata(WRAPPED_ETH).balanceOf(address(this)), - // "Tokens not transferred from user" - // ); - // assertEq(bridgePreBal + tokenAmount, address(rootBridge).balance, "ETH not transferred to Bridge"); - // // Check that native asset transferred to gas service - // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH for fee not paid from user"); - // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); - // } - - // // TODO split into multiple tests - // function test_depositToken() public { - // uint256 tokenAmount = 300; - // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - // (address childToken, bytes memory predictedPayload) = - // setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, true); - - // vm.expectEmit(address(axelarAdaptor)); - // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); - // vm.expectEmit(address(rootBridge)); - // emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); - - // vm.expectCall( - // address(axelarAdaptor), - // depositFee, - // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) - // ); - - // vm.expectCall( - // address(axelarGasService), - // depositFee, - // abi.encodeWithSelector( - // axelarGasService.payNativeGasForContractCall.selector, - // address(axelarAdaptor), - // CHILD_CHAIN_NAME, - // childBridgeAdaptorString, - // predictedPayload, - // address(this) - // ) - // ); - - // vm.expectCall( - // address(mockAxelarGateway), - // 0, - // abi.encodeWithSelector( - // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload - // ) - // ); - - // uint256 thisPreBal = token.balanceOf(address(this)); - // uint256 bridgePreBal = token.balanceOf(address(rootBridge)); - - // uint256 thisNativePreBal = address(this).balance; - // uint256 gasServiceNativePreBal = address(axelarGasService).balance; - - // rootBridge.deposit{value: depositFee}(token, tokenAmount); - - // // Check that tokens are transferred - // assertEq(thisPreBal - tokenAmount, token.balanceOf(address(this)), "Tokens not transferred from user"); - // assertEq(bridgePreBal + tokenAmount, token.balanceOf(address(rootBridge)), "Tokens not transferred to bridge"); - // // Check that native asset transferred to gas service - // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); - // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); - // } - - // // TODO split into multiple tests - // function test_depositTo() public { - // uint256 tokenAmount = 300; - // address recipient = address(9876); - // string memory childBridgeAdaptorString = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); - // (address childToken, bytes memory predictedPayload) = - // setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); - - // vm.expectEmit(address(axelarAdaptor)); - // emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); - // vm.expectEmit(address(rootBridge)); - // emit ChildChainERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); - - // vm.expectCall( - // address(axelarAdaptor), - // depositFee, - // abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) - // ); - - // vm.expectCall( - // address(axelarGasService), - // depositFee, - // abi.encodeWithSelector( - // axelarGasService.payNativeGasForContractCall.selector, - // address(axelarAdaptor), - // CHILD_CHAIN_NAME, - // childBridgeAdaptorString, - // predictedPayload, - // address(this) - // ) - // ); - - // vm.expectCall( - // address(mockAxelarGateway), - // 0, - // abi.encodeWithSelector( - // mockAxelarGateway.callContract.selector, CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload - // ) - // ); - - // uint256 thisPreBal = token.balanceOf(address(this)); - // uint256 bridgePreBal = token.balanceOf(address(rootBridge)); - - // uint256 thisNativePreBal = address(this).balance; - // uint256 gasServiceNativePreBal = address(axelarGasService).balance; - - // rootBridge.depositTo{value: depositFee}(token, recipient, tokenAmount); - - // // Check that tokens are transferred - // assertEq(thisPreBal - tokenAmount, token.balanceOf(address(this)), "Tokens not transferred from user"); - // assertEq(bridgePreBal + tokenAmount, token.balanceOf(address(rootBridge)), "Tokens not transferred to bridge"); - // // Check that native asset transferred to gas service - // assertEq(thisNativePreBal - depositFee, address(this).balance, "ETH not paid from user"); - // assertEq(gasServiceNativePreBal + depositFee, address(axelarGasService).balance, "ETH not paid to adaptor"); - // } } From c5450a617139202cdd1204650adafcc29476cab9 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Sat, 4 Nov 2023 08:20:25 +1100 Subject: [PATCH 14/49] Address PR comments --- src/root/RootERC20Bridge.sol | 8 ++- test/unit/root/RootERC20Bridge.t.sol | 76 ++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 6611ecf7b..1abc92f18 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -35,6 +35,7 @@ contract RootERC20Bridge is /// @dev leave this as the first param for the integration tests mapping(address => address) public rootTokenToChildToken; + uint256 public constant NO_DEPOSIT_LIMIT = 0; bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); address public constant NATIVE_ETH = address(0xeee); @@ -119,7 +120,10 @@ contract RootERC20Bridge is * @dev The limit can decrease, but it can never decrease to below the contract's IMX balance. */ function updateImxCumulativeDepositLimit(uint256 newImxCumulativeDepositLimit) external onlyOwner { - if (newImxCumulativeDepositLimit < IERC20Metadata(rootIMXToken).balanceOf(address(this))) { + if ( + newImxCumulativeDepositLimit != NO_DEPOSIT_LIMIT + && newImxCumulativeDepositLimit < IERC20Metadata(rootIMXToken).balanceOf(address(this)) + ) { revert ImxDepositLimitTooLow(); } emit NewImxDepositLimit(imxCumulativeDepositLimit, newImxCumulativeDepositLimit); @@ -256,7 +260,7 @@ contract RootERC20Bridge is revert ZeroAmount(); } if ( - address(rootToken) == rootIMXToken && imxCumulativeDepositLimit != 0 + address(rootToken) == rootIMXToken && imxCumulativeDepositLimit != NO_DEPOSIT_LIMIT && IERC20Metadata(rootIMXToken).balanceOf(address(this)) + amount > imxCumulativeDepositLimit ) { revert ImxDepositLimitExceeded(); diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index b527ebff5..07e612e4d 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -27,7 +27,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address constant WRAPPED_ETH = address(0xddd); uint256 constant mapTokenFee = 300; uint256 constant depositFee = 200; - uint256 constant unlimitedIMXDeposits = 0; + uint256 constant UNLIMITED_IMX_DEPOSITS = 0; ERC20PresetMinterPauser public token; RootERC20Bridge public rootBridge; @@ -55,7 +55,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(token), IMX_TOKEN, WRAPPED_ETH, - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } @@ -78,7 +78,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(token), IMX_TOKEN, WRAPPED_ETH, - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } @@ -92,7 +92,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(1), address(1), - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } @@ -106,14 +106,14 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(1), address(1), - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } function test_RevertIf_InitializeWithEmptyChildAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(InvalidChildERC20BridgeAdaptor.selector); - bridge.initialize(address(1), address(1), "", address(1), address(1), address(1), unlimitedIMXDeposits); + bridge.initialize(address(1), address(1), "", address(1), address(1), address(1), UNLIMITED_IMX_DEPOSITS); } function test_RevertIf_InitializeWithAZeroAddressTokenTemplate() public { @@ -126,7 +126,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(0), address(1), address(1), - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } @@ -140,7 +140,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(0), address(1), - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } @@ -154,14 +154,14 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(1), address(0), - unlimitedIMXDeposits + UNLIMITED_IMX_DEPOSITS ); } function test_RevertIf_InitializeWithAZeroAddressAll() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(0), "", address(0), address(0), address(0), unlimitedIMXDeposits); + bridge.initialize(address(0), address(0), "", address(0), address(0), address(0), UNLIMITED_IMX_DEPOSITS); } /** @@ -262,7 +262,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.mapToken{value: 300}(IERC20Metadata(NATIVE_ETH)); } - function test_updateRootBridgeAdaptor() public { + function test_updateRootBridgeAdaptor_UpdatesRootBridgeAdaptor() public { address newAdaptorAddress = address(0x11111); assertEq(address(rootBridge.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); @@ -270,6 +270,15 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid assertEq(address(rootBridge.rootBridgeAdaptor()), newAdaptorAddress, "bridgeAdaptor not updated"); } + function test_updateRootBridgeAdaptor_EmitsNewRootBridgeAdaptorEvent() public { + address newAdaptorAddress = address(0x11111); + + vm.expectEmit(); + emit NewRootBridgeAdaptor(address(rootBridge.rootBridgeAdaptor()), newAdaptorAddress); + + rootBridge.updateRootBridgeAdaptor(newAdaptorAddress); + } + function test_RevertIf_updateRootBridgeAdaptorCalledByNonOwner() public { vm.prank(address(0xf00f00)); vm.expectRevert("Ownable: caller is not the owner"); @@ -493,6 +502,51 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); } + function test_deposit_whenSettingImxDepositLimitToUnlimited() public { + uint256 imxCumulativeDepositLimit = 700; + + uint256 amount = 300; + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); + + IERC20Metadata(IMX_TOKEN).approve(address(rootBridge), type(uint256).max); + + rootBridge.updateImxCumulativeDepositLimit(imxCumulativeDepositLimit); + + // Valid + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); + + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); + // Valid + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); + + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, amount, false); + // Invalid + vm.expectRevert(ImxDepositLimitExceeded.selector); + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), amount); + + rootBridge.updateImxCumulativeDepositLimit(UNLIMITED_IMX_DEPOSITS); + + uint256 bigDepositAmount = 999999999999 ether; + setupDeposit(IMX_TOKEN, rootBridge, mapTokenFee, depositFee, bigDepositAmount, false); + + uint256 thisPreBal = IERC20Metadata(IMX_TOKEN).balanceOf(address(this)); + uint256 bridgePreBal = IERC20Metadata(IMX_TOKEN).balanceOf(address(rootBridge)); + + rootBridge.deposit{value: depositFee}(IERC20Metadata(IMX_TOKEN), bigDepositAmount); + + // Check that tokens are transferred + assertEq( + thisPreBal - bigDepositAmount, + IERC20Metadata(IMX_TOKEN).balanceOf(address(this)), + "Tokens not transferred from user" + ); + assertEq( + bridgePreBal + bigDepositAmount, + IERC20Metadata(IMX_TOKEN).balanceOf(address(rootBridge)), + "Tokens not transferred to bridge" + ); + } + function test_depositCallsSendMessage() public { uint256 amount = 100; (, bytes memory predictedPayload) = From dec0180bad7b2c4420eb08d18004c884c597bb78 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Mon, 6 Nov 2023 08:40:44 +1100 Subject: [PATCH 15/49] Add length <32 check to child bridge --- src/child/ChildERC20Bridge.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 9a9e6212e..851de8249 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -109,11 +109,12 @@ contract ChildERC20Bridge is if (!Strings.equal(messageSourceChain, rootChain)) { revert InvalidSourceChain(); } - if (!Strings.equal(sourceAddress, rootERC20BridgeAdaptor)) { revert InvalidSourceAddress(); } - if (data.length == 0) { + if (data.length <= 32) { + // Data must always be greater than 32. + // 32 bytes for the signature, and at least some information for the payload revert InvalidData(); } From 1fc0d9770e83004ddec1b83f50882483b28ceb06 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Mon, 6 Nov 2023 08:43:29 +1100 Subject: [PATCH 16/49] Add checks and update tests --- src/child/ChildERC20Bridge.sol | 2 +- src/interfaces/child/IChildERC20Bridge.sol | 2 ++ src/interfaces/root/IRootERC20Bridge.sol | 2 ++ src/root/RootERC20Bridge.sol | 6 ++++-- test/integration/child/ChildAxelarBridge.t.sol | 2 +- .../root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol | 2 +- test/unit/child/ChildERC20Bridge.t.sol | 2 +- test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol | 2 +- 8 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 851de8249..9037215dd 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -115,7 +115,7 @@ contract ChildERC20Bridge is if (data.length <= 32) { // Data must always be greater than 32. // 32 bytes for the signature, and at least some information for the payload - revert InvalidData(); + revert DataTooShort(); } if (bytes32(data[:32]) == MAP_TOKEN_SIG) { diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 087daa893..62d7cfe77 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -72,6 +72,8 @@ interface IChildERC20BridgeErrors { error AlreadyMapped(); /// @notice Error when a message is given to the bridge from an address not the designated bridge adaptor. error NotBridgeAdaptor(); + /// @notice Error when the message's payload is too short. + error DataTooShort(); /// @notice Error when the message's payload is not valid. error InvalidData(); /// @notice Error when the message's source chain is not valid. diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 13bfb1e82..a0dd34dd9 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -105,6 +105,8 @@ interface IRootERC20BridgeErrors { error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); /// @notice Error when the given child chain bridge adaptor is invalid. error InvalidChildERC20BridgeAdaptor(); + /// @notice Error when the message's payload is too short. + error DataTooShort(); /// @notice Error when a message received has invalid data. error InvalidData(); /// @notice Error when a message received has invalid source address. diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index bf72034dd..b67a3ddda 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -164,8 +164,10 @@ contract RootERC20Bridge is if (!Strings.equal(sourceAddress, childBridgeAdaptor)) { revert InvalidSourceAddress(); } - if (data.length == 0) { - revert InvalidData(); + if (data.length <= 32) { + // Data must always be greater than 32. + // 32 bytes for the signature, and at least some information for the payload + revert DataTooShort(); } if (bytes32(data[:32]) == WITHDRAW_SIG) { diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index c73f41a68..cad11ba88 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -109,7 +109,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil bytes32 commandId = bytes32("testCommandId"); bytes memory payload = ""; - vm.expectRevert(InvalidData.selector); + vm.expectRevert(DataTooShort.selector); childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); } diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 70b0d1400..319dc104f 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -87,7 +87,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is bytes32 commandId = bytes32("testCommandId"); string memory sourceAddress = rootBridge.childBridgeAdaptor(); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(DataTooShort.selector); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 7bb37a263..95d72974f 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -168,7 +168,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B function test_RevertIf_onMessageReceiveCalledWithDataLengthZero() public { bytes memory data = ""; - vm.expectRevert(InvalidData.selector); + vm.expectRevert(DataTooShort.selector); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); } diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index 5ea3c0e43..9fc1813da 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -84,7 +84,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE bytes memory data; vm.prank(address(mockAxelarAdaptor)); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(DataTooShort.selector); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } From 6b8bd5543ca7ec30216fe3c55a48109dfc73a97a Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 08:31:53 +1000 Subject: [PATCH 17/49] IMX Withdraw L2 --- src/child/ChildERC20Bridge.sol | 76 +++++-- src/interfaces/child/IChildERC20Bridge.sol | 9 + .../ChildAxelarBridgeWithdrawIMX.t.sol | 133 +++++++++++ .../ChildAxelarBridgeWithdrawToIMX.t.sol | 208 ++++++++++++++++++ .../ChildERC20BridgeWithdrawIMX.t.sol | 99 +++++++++ .../ChildERC20BridgeWithdrawToIMX.t.sol | 131 +++++++++++ 6 files changed, 639 insertions(+), 17 deletions(-) create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 9a9e6212e..410968a0c 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -40,6 +40,7 @@ contract ChildERC20Bridge is bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address public constant NATIVE_ETH = address(0xeee); + address public constant NATIVE_IMX = address(0xfff); IChildERC20BridgeAdaptor public bridgeAdaptor; @@ -134,29 +135,65 @@ contract ChildERC20Bridge is _withdraw(childToken, receiver, amount); } - function _withdraw(IChildERC20 childToken, address receiver, uint256 amount) private { - if (address(childToken).code.length == 0) { - revert EmptyTokenContract(); - } + function withdrawIMX(uint256 amount) external payable { + _withdrawIMX(msg.sender, amount); + } - address rootToken = childToken.rootToken(); + function withdrawToIMX(address receiver, uint256 amount) external payable { + _withdrawIMX(receiver, amount); + } - if (rootTokenToChildToken[rootToken] != address(childToken)) { - revert NotMapped(); + function _withdrawIMX(address receiver, uint256 amount) private { + if (msg.value < amount) { + revert InsufficientValue(); } - // A mapped token should never have root token unset - if (rootToken == address(0)) { - revert ZeroAddressRootToken(); + uint256 expectedBalance = address(this).balance - (msg.value - amount); + + _withdraw(IChildERC20(NATIVE_IMX), receiver, amount); + + if (address(this).balance != expectedBalance) { + revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance); } + } - // A mapped token should never have the bridge unset - if (childToken.bridge() != address(this)) { - revert BridgeNotSet(); + function _withdraw(IChildERC20 childToken, address receiver, uint256 amount) private { + if (address(childToken) == address(0)) { + revert ZeroAddress(); + } + if (amount == 0) { + revert ZeroAmount(); } - if (!childToken.burn(msg.sender, amount)) { - revert BurnFailed(); + address rootToken; + uint256 feeAmount = msg.value; + + if (address(childToken) == NATIVE_IMX) { + feeAmount = msg.value - amount; + rootToken = rootIMXToken; + } else { + if (address(childToken).code.length == 0) { + revert EmptyTokenContract(); + } + rootToken = childToken.rootToken(); + + if (rootTokenToChildToken[rootToken] != address(childToken)) { + revert NotMapped(); + } + + // A mapped token should never have root token unset + if (rootToken == address(0)) { + revert ZeroAddressRootToken(); + } + + // A mapped token should never have the bridge unset + if (childToken.bridge() != address(this)) { + revert BridgeNotSet(); + } + + if (!childToken.burn(msg.sender, amount)) { + revert BurnFailed(); + } } // TODO Should we enforce receiver != 0? old poly contracts don't @@ -165,9 +202,14 @@ contract ChildERC20Bridge is bytes memory payload = abi.encode(WITHDRAW_SIG, rootToken, msg.sender, receiver, amount); // Send the message to the bridge adaptor and up to root chain - bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); - emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount); + bridgeAdaptor.sendMessage{value: feeAmount}(payload, msg.sender); + + if (address(childToken) == NATIVE_IMX) { + emit ChildChainNativeIMXWithdraw(rootToken, msg.sender, receiver, amount); + } else { + emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount); + } } function _mapToken(bytes calldata data) private { diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 087daa893..170df909c 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -32,6 +32,9 @@ interface IChildERC20BridgeEvents { address indexed receiver, uint256 amount ); + event ChildChainNativeIMXWithdraw( + address indexed rootToken, address depositor, address indexed receiver, uint256 amount + ); event ChildChainERC20Deposit( address indexed rootToken, @@ -52,6 +55,10 @@ interface IChildERC20BridgeEvents { // TODO add parameters to errors if it makes sense interface IChildERC20BridgeErrors { + /// @notice Error when the amount requested is less than the value sent. + error InsufficientValue(); + /// @notice Error when there is no gas payment received. + error ZeroAmount(); /// @notice Error when the contract to mint had no bytecode. error EmptyTokenContract(); /// @notice Error when the mint operation failed. @@ -84,4 +91,6 @@ interface IChildERC20BridgeErrors { error BridgeNotSet(); /// @notice Error when a call to the given child token's `burn` function fails. error BurnFailed(); + /// @notice Error when token balance invariant check fails. + error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol new file mode 100644 index 000000000..a0d9135ae --- /dev/null +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; + +contract ChildERC20BridgeWithdrawIMXIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(555555); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + } + + function test_WithdrawIMX_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + } + + function test_WithdrawIMX_CallsAxelarGateway() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + } + + function test_WithdrawIMX_CallsGasService() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + } + + function test_WithdrawIMXEmitsAxelarMessageEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + } + + function test_WithdrawIMX_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + uint256 preGasBal = address(axelarGasService).balance; + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + + uint256 postBal = address(this).balance; + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawFee - withdrawAmount, "Balance not reduced"); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); + } +} diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol new file mode 100644 index 000000000..46fab829b --- /dev/null +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; + +contract ChildERC20BridgeWithdrawToIMXIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(555555); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + } + + function test_WithdrawToIMX_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + } + + function test_WithdrawToIMXWithDifferentAccount_CallsBridgeAdaptor() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + } + + function test_WithdrawToIMX_CallsAxelarGateway() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + } + + function test_WithdrawToIMXWithDifferentAccount_CallsAxelarGateway() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + } + + function test_WithdrawToIMX_CallsGasService() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + } + + function test_WithdrawToIMXWithDifferentAccount_CallsGasService() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + } + + function test_WithdrawToIMX_EmitsAxelarMessageEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + } + + function test_WithdrawToIMXWithDifferentAccount_EmitsAxelarMessageEvent() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + } + + function test_WithdrawToIMX_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + uint256 preGasBal = address(axelarGasService).balance; + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + + uint256 postBal = address(this).balance; + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawFee - withdrawAmount, "Balance not reduced"); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); + } +} diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol new file mode 100644 index 000000000..21fd728ab --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract ChildERC20BridgeWithdrawIMXUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + ChildERC20 public childTokenTemplate; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + + function setUp() public { + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + ); + } + + function test_RevertsIf_WithdrawIMXCalledWithInsufficientFund() public { + uint256 withdrawAmount = 7 ether; + + vm.expectRevert(InsufficientValue.selector); + childBridge.withdrawIMX{value: withdrawAmount - 1}(withdrawAmount); + } + + function test_RevertIf_ZeroAmountIsProvided() public { + uint256 withdrawFee = 300; + + vm.expectRevert(ZeroAmount.selector); + childBridge.withdrawIMX{value: withdrawFee}(0); + } + + function test_WithdrawIMX_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + } + + function test_WithdrawIMX_EmitsNativeIMXWithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainNativeIMXWithdraw(ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + } + + function test_WithdrawIMX_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + + uint256 postBal = address(this).balance; + assertEq(postBal, preBal - withdrawAmount - withdrawFee, "Balance not reduced"); + } + + function test_WithdrawIMX_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(mockAdaptor).balance; + + childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); + + uint256 postBal = address(mockAdaptor).balance; + assertEq(postBal, preBal + withdrawFee, "Adaptor balance not increased"); + } +} diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol new file mode 100644 index 000000000..1f5a1f9be --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + ChildERC20 public childTokenTemplate; + ChildERC20 public rootToken; + ChildERC20 public childToken; + address public childETHToken; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + + function setUp() public { + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + ); + } + + function test_RevertsIf_WithdrawToIMXCalledWithInsufficientFund() public { + uint256 withdrawAmount = 7 ether; + + vm.expectRevert(InsufficientValue.selector); + childBridge.withdrawToIMX{value: withdrawAmount - 1}(address(this), withdrawAmount); + } + + function test_RevertIf_ZeroAmountIsProvided() public { + uint256 withdrawFee = 300; + + vm.expectRevert(ZeroAmount.selector); + childBridge.withdrawToIMX{value: withdrawFee}(address(this), 0); + } + + function test_WithdrawToIMX_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + } + + function test_WithdrawToIMXWithDifferentAccount_CallsBridgeAdaptor() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + } + + function test_WithdrawToIMX_EmitsNativeIMXWithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainNativeIMXWithdraw(ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + } + + function test_WithdrawToIMXWithDifferentAccount_EmitsNativeIMXWithdrawEvent() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainNativeIMXWithdraw(ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + } + + function test_WithdrawIMX_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + + uint256 postBal = address(this).balance; + assertEq(postBal, preBal - withdrawAmount - withdrawFee, "Balance not reduced"); + } + + function test_WithdrawIMX_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(mockAdaptor).balance; + + childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + + uint256 postBal = address(mockAdaptor).balance; + assertEq(postBal, preBal + withdrawFee, "Adaptor balance not increased"); + } +} From 0044293d5fa7efbded1a590dcb9e012adfbf7654 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 10:16:24 +1000 Subject: [PATCH 18/49] Rename function --- src/child/ChildERC20Bridge.sol | 2 +- .../ChildAxelarBridgeWithdrawToIMX.t.sol | 38 +++++++++---------- .../ChildERC20BridgeWithdrawToIMX.t.sol | 28 +++++++------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 410968a0c..7fff259bf 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -139,7 +139,7 @@ contract ChildERC20Bridge is _withdrawIMX(msg.sender, amount); } - function withdrawToIMX(address receiver, uint256 amount) external payable { + function withdrawIMXTo(address receiver, uint256 amount) external payable { _withdrawIMX(receiver, amount); } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol index 46fab829b..8c6e1fea6 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol @@ -17,7 +17,7 @@ import {Utils} from "../../../utils.t.sol"; import {WETH} from "../../../../src/test/root/WETH.sol"; import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; -contract ChildERC20BridgeWithdrawToIMXIntegrationTest is +contract ChildERC20BridgewithdrawIMXToIntegrationTest is Test, IChildERC20BridgeEvents, IChildAxelarBridgeAdaptorEvents, @@ -44,7 +44,7 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is childIntegrationSetup(); } - function test_WithdrawToIMX_CallsBridgeAdaptor() public { + function test_withdrawIMXTo_CallsBridgeAdaptor() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -56,10 +56,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_WithdrawToIMXWithDifferentAccount_CallsBridgeAdaptor() public { + function test_withdrawIMXToWithDifferentAccount_CallsBridgeAdaptor() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -72,10 +72,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_WithdrawToIMX_CallsAxelarGateway() public { + function test_withdrawIMXTo_CallsAxelarGateway() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -92,10 +92,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is ) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_WithdrawToIMXWithDifferentAccount_CallsAxelarGateway() public { + function test_withdrawIMXToWithDifferentAccount_CallsAxelarGateway() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -113,10 +113,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is ) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_WithdrawToIMX_CallsGasService() public { + function test_withdrawIMXTo_CallsGasService() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -136,10 +136,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is ) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_WithdrawToIMXWithDifferentAccount_CallsGasService() public { + function test_withdrawIMXToWithDifferentAccount_CallsGasService() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -160,10 +160,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is ) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_WithdrawToIMX_EmitsAxelarMessageEvent() public { + function test_withdrawIMXTo_EmitsAxelarMessageEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -173,10 +173,10 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is vm.expectEmit(address(axelarAdaptor)); emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_WithdrawToIMXWithDifferentAccount_EmitsAxelarMessageEvent() public { + function test_withdrawIMXToWithDifferentAccount_EmitsAxelarMessageEvent() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -187,17 +187,17 @@ contract ChildERC20BridgeWithdrawToIMXIntegrationTest is vm.expectEmit(address(axelarAdaptor)); emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_WithdrawToIMX_ReducesBalance() public { + function test_withdrawIMXTo_ReducesBalance() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; uint256 preBal = address(this).balance; uint256 preGasBal = address(axelarGasService).balance; - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); uint256 postBal = address(this).balance; uint256 postGasBal = address(axelarGasService).balance; diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol index 1f5a1f9be..c8d997ea9 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol @@ -16,7 +16,7 @@ import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; -contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { +contract ChildERC20BridgewithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { address constant ROOT_BRIDGE = address(3); string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; @@ -41,21 +41,21 @@ contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, ); } - function test_RevertsIf_WithdrawToIMXCalledWithInsufficientFund() public { + function test_RevertsIf_withdrawIMXToCalledWithInsufficientFund() public { uint256 withdrawAmount = 7 ether; vm.expectRevert(InsufficientValue.selector); - childBridge.withdrawToIMX{value: withdrawAmount - 1}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawAmount - 1}(address(this), withdrawAmount); } function test_RevertIf_ZeroAmountIsProvided() public { uint256 withdrawFee = 300; vm.expectRevert(ZeroAmount.selector); - childBridge.withdrawToIMX{value: withdrawFee}(address(this), 0); + childBridge.withdrawIMXTo{value: withdrawFee}(address(this), 0); } - function test_WithdrawToIMX_CallsBridgeAdaptor() public { + function test_withdrawIMXTo_CallsBridgeAdaptor() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -67,10 +67,10 @@ contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, withdrawFee, abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_WithdrawToIMXWithDifferentAccount_CallsBridgeAdaptor() public { + function test_withdrawIMXToWithDifferentAccount_CallsBridgeAdaptor() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -83,26 +83,26 @@ contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, withdrawFee, abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_WithdrawToIMX_EmitsNativeIMXWithdrawEvent() public { + function test_withdrawIMXTo_EmitsNativeIMXWithdrawEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; vm.expectEmit(address(childBridge)); emit ChildChainNativeIMXWithdraw(ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_WithdrawToIMXWithDifferentAccount_EmitsNativeIMXWithdrawEvent() public { + function test_withdrawIMXToWithDifferentAccount_EmitsNativeIMXWithdrawEvent() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; vm.expectEmit(address(childBridge)); emit ChildChainNativeIMXWithdraw(ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } function test_WithdrawIMX_ReducesBalance() public { @@ -111,7 +111,7 @@ contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, uint256 preBal = address(this).balance; - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); uint256 postBal = address(this).balance; assertEq(postBal, preBal - withdrawAmount - withdrawFee, "Balance not reduced"); @@ -123,7 +123,7 @@ contract ChildERC20BridgeWithdrawToIMXUnitTest is Test, IChildERC20BridgeEvents, uint256 preBal = address(mockAdaptor).balance; - childBridge.withdrawToIMX{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); + childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); uint256 postBal = address(mockAdaptor).balance; assertEq(postBal, preBal + withdrawFee, "Adaptor balance not increased"); From c7808253afeacb4e497b629361266e8baf062569 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 12:30:55 +1000 Subject: [PATCH 19/49] Add withdraw IMX handler on L1 --- src/root/RootERC20Bridge.sol | 12 ++- .../RootERC20BridgeWithdraw.t.sol | 74 ++++++++++++++++ .../withdrawals/RootERC20BridgeWithdraw.t.sol | 85 +++++++++++++++++-- 3 files changed, 163 insertions(+), 8 deletions(-) diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index bf72034dd..2e58e0b9a 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -41,6 +41,7 @@ contract RootERC20Bridge is bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address public constant NATIVE_ETH = address(0xeee); + address public constant NATIVE_IMX = address(0xfff); IRootERC20BridgeAdaptor public rootBridgeAdaptor; /// @dev Used to verify source address in messages sent from child chain. @@ -352,9 +353,14 @@ contract RootERC20Bridge is function _withdraw(bytes memory data) private { (address rootToken, address withdrawer, address receiver, uint256 amount) = abi.decode(data, (address, address, address, uint256)); - address childToken = rootTokenToChildToken[rootToken]; - if (childToken == address(0)) { - revert NotMapped(); + address childToken; + if (address(rootToken) == rootIMXToken) { + childToken = NATIVE_IMX; + } else { + childToken = rootTokenToChildToken[rootToken]; + if (childToken == address(0)) { + revert NotMapped(); + } } _executeTransfer(rootToken, childToken, withdrawer, receiver, amount); } diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 70b0d1400..be75eb22d 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -32,6 +32,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is address constant IMX_TOKEN_ADDRESS = address(0xccc); address constant NATIVE_ETH = address(0xeee); address constant WRAPPED_ETH = address(0xddd); + address public constant NATIVE_IMX = address(0xfff); uint256 constant UNLIMITED_IMX_DEPOSIT_LIMIT = 0; uint256 constant withdrawAmount = 0.5 ether; @@ -59,6 +60,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is rootBridge.mapToken{value: 1}(token); // And give the bridge some tokens token.transfer(address(rootBridge), 100 ether); + imxToken.transfer(address(rootBridge), 100 ether); } function test_RevertsIf_WithdrawWithInvalidSourceAddress() public { @@ -119,6 +121,24 @@ contract RootERC20BridgeWithdrawIntegrationTest is assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } + function test_withdrawIMX_TransfersIMX() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 thisPreBal = imxToken.balanceOf(address(this)); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 thisPostBal = imxToken.balanceOf(address(this)); + uint256 bridgePostBal = imxToken.balanceOf(address(rootBridge)); + + assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + function test_withdraw_TransfersTokens_DifferentReceiver() public { address receiver = address(987654321); bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); @@ -138,6 +158,25 @@ contract RootERC20BridgeWithdrawIntegrationTest is assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } + function test_withdrawIMX_TransfersIMX_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 thisPreBal = imxToken.balanceOf(receiver); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 thisPostBal = imxToken.balanceOf(receiver); + uint256 bridgePostBal = imxToken.balanceOf(address(rootBridge)); + + assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + function test_withdraw_EmitsRootChainERC20WithdrawEvent() public { bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); @@ -155,6 +194,23 @@ contract RootERC20BridgeWithdrawIntegrationTest is axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } + function test_withdrawIMX_EmitsRootChainERC20WithdrawEvent() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(imxToken), + NATIVE_IMX, + address(this), + address(this), + withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + function test_withdraw_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { address receiver = address(987654321); bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); @@ -168,4 +224,22 @@ contract RootERC20BridgeWithdrawIntegrationTest is ); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } + + function test_withdrawIMX_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(imxToken), + NATIVE_IMX, + address(this), + receiver, + withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } } diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index 5ea3c0e43..c4a050c78 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -17,13 +17,16 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE address constant CHILD_BRIDGE_ADAPTOR = address(4); string CHILD_BRIDGE_ADAPTOR_STRING = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); string constant CHILD_CHAIN_NAME = "test"; + address constant UnmappedToken = address(0xbbb); address constant IMX_TOKEN = address(0xccc); address constant WRAPPED_ETH = address(0xddd); + address public constant NATIVE_IMX = address(0xfff); uint256 constant mapTokenFee = 300; uint256 constant withdrawAmount = 0.5 ether; uint256 constant UNLIMITED_IMX_DEPOSIT_LIMIT = 0; ERC20PresetMinterPauser public token; + ERC20PresetMinterPauser public imxToken; RootERC20Bridge public rootBridge; MockAdaptor public mockAxelarAdaptor; MockAxelarGateway public mockAxelarGateway; @@ -33,6 +36,8 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE token = new ERC20PresetMinterPauser("Test", "TST"); token.mint(address(this), 100 ether); deployCodeTo("ERC20PresetMinterPauser.sol", abi.encode("ImmutableX", "IMX"), IMX_TOKEN); + imxToken = ERC20PresetMinterPauser(IMX_TOKEN); + imxToken.mint(address(this), 100 ether); deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); @@ -56,14 +61,14 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE } function test_RevertsIf_WithdrawWithInvalidSender() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); vm.expectRevert(NotBridgeAdaptor.selector); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } function test_RevertsIf_OnMessageReceiveWithInvalidSourceChain() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); vm.expectRevert(InvalidSourceChain.selector); @@ -71,7 +76,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE } function test_RevertsIf_OnMessageReceiveWithInvalidSourceAddress() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); console2.log(CHILD_CHAIN_NAME); console2.log(rootBridge.childChain()); @@ -89,14 +94,14 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE } function test_RevertsIf_OnMessageReceiveWithInvalidSignature() public { - bytes memory data = abi.encode(keccak256("RANDOM"), IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(keccak256("RANDOM"), token, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); vm.expectRevert(InvalidData.selector); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } function test_RevertsIf_OnMessageReceiveWithUnmappedToken() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, UnmappedToken, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); vm.expectRevert(NotMapped.selector); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); @@ -121,6 +126,23 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE ); } + function test_onMessageReceive_TransfersIMXTokens() public { + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + uint256 thisPreBal = imxToken.balanceOf(address(this)); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(imxToken.balanceOf(address(this)), thisPreBal + withdrawAmount, "IMX not transferred to receiver"); + assertEq( + imxToken.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "IMX not transferred from bridge" + ); + } + function test_onMessageReceive_TransfersTokens_DifferentReceiver() public { address receiver = address(123456); // Need to first map the token. @@ -141,6 +163,24 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE ); } + function test_onMessageReceive_TransfersIMXTokens_DifferentReceiver() public { + address receiver = address(123456); + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + uint256 thisPreBal = imxToken.balanceOf(address(receiver)); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(imxToken.balanceOf(address(receiver)), thisPreBal + withdrawAmount, "IMX not transferred to receiver"); + assertEq( + imxToken.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "IMX not transferred from bridge" + ); + } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent() public { // Need to first map the token. rootBridge.mapToken(token); @@ -160,6 +200,23 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEventForIMX() public { + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), address(this), withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(imxToken), + NATIVE_IMX, + address(this), + address(this), + withdrawAmount + ); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { address receiver = address(123456); // Need to first map the token. @@ -175,4 +232,22 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEventForIMX_DifferentReceiver() public { + address receiver = address(123456); + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), receiver, withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(imxToken), + NATIVE_IMX, + address(this), + receiver, + withdrawAmount + ); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } } From 8adc3199573ed590a0bd10dfa6b7e2e92e6f7cb8 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 12:31:12 +1000 Subject: [PATCH 20/49] Fmt --- .../RootERC20BridgeWithdraw.t.sol | 16 ++-------------- .../withdrawals/RootERC20BridgeWithdraw.t.sol | 16 ++-------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index be75eb22d..1446194eb 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -201,13 +201,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is string memory sourceAddress = rootBridge.childBridgeAdaptor(); vm.expectEmit(); - emit RootChainERC20Withdraw( - address(imxToken), - NATIVE_IMX, - address(this), - address(this), - withdrawAmount - ); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), address(this), withdrawAmount); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } @@ -233,13 +227,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is string memory sourceAddress = rootBridge.childBridgeAdaptor(); vm.expectEmit(); - emit RootChainERC20Withdraw( - address(imxToken), - NATIVE_IMX, - address(this), - receiver, - withdrawAmount - ); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), receiver, withdrawAmount); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } } diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index c4a050c78..14d5d32b2 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -206,13 +206,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), address(this), withdrawAmount); vm.expectEmit(); - emit RootChainERC20Withdraw( - address(imxToken), - NATIVE_IMX, - address(this), - address(this), - withdrawAmount - ); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } @@ -240,13 +234,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), receiver, withdrawAmount); vm.expectEmit(); - emit RootChainERC20Withdraw( - address(imxToken), - NATIVE_IMX, - address(this), - receiver, - withdrawAmount - ); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), receiver, withdrawAmount); vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } From 67e909474a7bf29f1f13f9cb01a50bbc62c5d326 Mon Sep 17 00:00:00 2001 From: Zhenyang Shi Date: Mon, 6 Nov 2023 16:32:18 +1000 Subject: [PATCH 21/49] Update test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol Co-authored-by: Ermyas Abebe --- .../child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol index a0d9135ae..0396c0dee 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol @@ -102,7 +102,7 @@ contract ChildERC20BridgeWithdrawIMXIntegrationTest is childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); } - function test_WithdrawIMXEmitsAxelarMessageEvent() public { + function test_WithdrawIMX_EmitsAxelarMessageEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; From ed220a8f4cbba1f06f1765122bcef06e1d97f493 Mon Sep 17 00:00:00 2001 From: Zhenyang Shi Date: Mon, 6 Nov 2023 16:32:24 +1000 Subject: [PATCH 22/49] Update src/interfaces/child/IChildERC20Bridge.sol Co-authored-by: Ermyas Abebe --- src/interfaces/child/IChildERC20Bridge.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 170df909c..dc57fc912 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -57,7 +57,7 @@ interface IChildERC20BridgeEvents { interface IChildERC20BridgeErrors { /// @notice Error when the amount requested is less than the value sent. error InsufficientValue(); - /// @notice Error when there is no gas payment received. + /// @notice Error when the withdrawal amount is zero error ZeroAmount(); /// @notice Error when the contract to mint had no bytecode. error EmptyTokenContract(); From d3c14a3363a81d77419215c43ccbc155f908c4b7 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 16:43:54 +1000 Subject: [PATCH 23/49] Update IChildERC20.sol --- src/interfaces/child/IChildERC20.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interfaces/child/IChildERC20.sol b/src/interfaces/child/IChildERC20.sol index 768af1ca4..6d580432d 100644 --- a/src/interfaces/child/IChildERC20.sol +++ b/src/interfaces/child/IChildERC20.sol @@ -25,8 +25,8 @@ interface IChildERC20 is IERC20MetadataUpgradeable { function bridge() external view returns (address); /** - * @notice Returns bridge address controlling the child token - * @return address Returns the address of the Bridge + * @notice Returns the address of the mapped token on the root chain + * @return address Returns the address of the root token */ function rootToken() external view returns (address); From b27968bae59f61d6c29311e08f7aacda0ee8df03 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 16:47:39 +1000 Subject: [PATCH 24/49] Update --- src/child/ChildERC20Bridge.sol | 2 +- src/interfaces/child/IChildERC20Bridge.sol | 2 ++ test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol | 2 +- test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 7fff259bf..3a8553eab 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -188,7 +188,7 @@ contract ChildERC20Bridge is // A mapped token should never have the bridge unset if (childToken.bridge() != address(this)) { - revert BridgeNotSet(); + revert IncorrectBridgeAddress(); } if (!childToken.burn(msg.sender, amount)) { diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 170df909c..b262153c3 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -89,6 +89,8 @@ interface IChildERC20BridgeErrors { error ZeroAddressRootToken(); /// @notice Error when a given child token's bridge address is not set. error BridgeNotSet(); + /// @notice Error when a given child token's bridge address is incorrect. + error IncorrectBridgeAddress(); /// @notice Error when a call to the given child token's `burn` function fails. error BurnFailed(); /// @notice Error when token balance invariant check fails. diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index 86360c0a1..532b1f43e 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -95,7 +95,7 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); vm.store(address(childToken), bridgeSlotBytes32, bytes32(uint256(uint160(address(0x123))))); - vm.expectRevert(BridgeNotSet.selector); + vm.expectRevert(IncorrectBridgeAddress.selector); childBridge.withdraw(IChildERC20(address(childToken)), 100); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index a1e00709a..a2b67262b 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -95,7 +95,7 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC bytes32 bridgeSlotBytes32 = bytes32(bridgeSlot); vm.store(address(childToken), bridgeSlotBytes32, bytes32(uint256(uint160(address(0x123))))); - vm.expectRevert(BridgeNotSet.selector); + vm.expectRevert(IncorrectBridgeAddress.selector); childBridge.withdrawTo(IChildERC20(address(childToken)), address(this), 100); } From 2bff7a929704e06d664ae637175545660acb9f89 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Mon, 6 Nov 2023 17:08:53 +1000 Subject: [PATCH 25/49] Update --- .../root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol | 6 +++--- test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 1446194eb..e571e5e44 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -165,15 +165,15 @@ contract RootERC20BridgeWithdrawIntegrationTest is bytes32 commandId = bytes32("testCommandId"); string memory sourceAddress = rootBridge.childBridgeAdaptor(); - uint256 thisPreBal = imxToken.balanceOf(receiver); + uint256 receiverPreBal = imxToken.balanceOf(receiver); uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); - uint256 thisPostBal = imxToken.balanceOf(receiver); + uint256 receiverPostBal = imxToken.balanceOf(receiver); uint256 bridgePostBal = imxToken.balanceOf(address(rootBridge)); - assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(receiverPostBal, receiverPreBal + withdrawAmount, "Incorrect user balance after withdraw"); assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index 14d5d32b2..61c79980c 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -168,14 +168,16 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE // Give bridge some IMX tokens imxToken.transfer(address(rootBridge), 100 ether); - uint256 thisPreBal = imxToken.balanceOf(address(receiver)); + uint256 receiverPreBal = imxToken.balanceOf(address(receiver)); uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), receiver, withdrawAmount); vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); - assertEq(imxToken.balanceOf(address(receiver)), thisPreBal + withdrawAmount, "IMX not transferred to receiver"); + assertEq( + imxToken.balanceOf(address(receiver)), receiverPreBal + withdrawAmount, "IMX not transferred to receiver" + ); assertEq( imxToken.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "IMX not transferred from bridge" ); From 2759eddcebb2d7f2186683872fb9a3540a63a778 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Tue, 7 Nov 2023 09:04:26 +1100 Subject: [PATCH 26/49] InvalidData with string reason --- src/child/ChildERC20Bridge.sol | 4 ++-- src/interfaces/child/IChildERC20Bridge.sol | 4 +--- src/interfaces/root/IRootERC20Bridge.sol | 4 +--- src/root/RootERC20Bridge.sol | 4 ++-- test/integration/child/ChildAxelarBridge.t.sol | 4 ++-- .../root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol | 4 ++-- test/unit/child/ChildERC20Bridge.t.sol | 4 ++-- test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol | 6 ++++-- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 9037215dd..d4eae07a4 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -115,7 +115,7 @@ contract ChildERC20Bridge is if (data.length <= 32) { // Data must always be greater than 32. // 32 bytes for the signature, and at least some information for the payload - revert DataTooShort(); + revert InvalidData("Data too short"); } if (bytes32(data[:32]) == MAP_TOKEN_SIG) { @@ -123,7 +123,7 @@ contract ChildERC20Bridge is } else if (bytes32(data[:32]) == DEPOSIT_SIG) { _deposit(data[32:]); } else { - revert InvalidData(); + revert InvalidData("Unsupported action signature"); } } diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 62d7cfe77..aaf4195d7 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -72,10 +72,8 @@ interface IChildERC20BridgeErrors { error AlreadyMapped(); /// @notice Error when a message is given to the bridge from an address not the designated bridge adaptor. error NotBridgeAdaptor(); - /// @notice Error when the message's payload is too short. - error DataTooShort(); /// @notice Error when the message's payload is not valid. - error InvalidData(); + error InvalidData(string reason); /// @notice Error when the message's source chain is not valid. error InvalidSourceChain(); /// @notice Error when the source chain's message sender is not a recognised address. diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index a0dd34dd9..51d235d05 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -105,10 +105,8 @@ interface IRootERC20BridgeErrors { error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); /// @notice Error when the given child chain bridge adaptor is invalid. error InvalidChildERC20BridgeAdaptor(); - /// @notice Error when the message's payload is too short. - error DataTooShort(); /// @notice Error when a message received has invalid data. - error InvalidData(); + error InvalidData(string reason); /// @notice Error when a message received has invalid source address. error InvalidSourceAddress(); /// @notice Error when a message received has invalid source chain. diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index b67a3ddda..266777a8a 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -167,13 +167,13 @@ contract RootERC20Bridge is if (data.length <= 32) { // Data must always be greater than 32. // 32 bytes for the signature, and at least some information for the payload - revert DataTooShort(); + revert InvalidData("Data too short"); } if (bytes32(data[:32]) == WITHDRAW_SIG) { _withdraw(data[32:]); } else { - revert InvalidData(); + revert InvalidData("Unsupported action signature"); } } diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index cad11ba88..bf8e0da28 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -83,7 +83,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil bytes32 commandId = bytes32("testCommandId"); bytes memory payload = abi.encode("invalid payload"); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); } @@ -109,7 +109,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil bytes32 commandId = bytes32("testCommandId"); bytes memory payload = ""; - vm.expectRevert(DataTooShort.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); } diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 319dc104f..bedf6d979 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -87,7 +87,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is bytes32 commandId = bytes32("testCommandId"); string memory sourceAddress = rootBridge.childBridgeAdaptor(); - vm.expectRevert(DataTooShort.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } @@ -97,7 +97,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is bytes32 commandId = bytes32("testCommandId"); string memory sourceAddress = rootBridge.childBridgeAdaptor(); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 95d72974f..72694bcac 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -168,7 +168,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B function test_RevertIf_onMessageReceiveCalledWithDataLengthZero() public { bytes memory data = ""; - vm.expectRevert(DataTooShort.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); } @@ -176,7 +176,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B bytes memory data = abi.encode("FAKEDATA", address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); } diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index 9fc1813da..be1999cdc 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -84,14 +84,16 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE bytes memory data; vm.prank(address(mockAxelarAdaptor)); - vm.expectRevert(DataTooShort.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } function test_RevertsIf_OnMessageReceiveWithInvalidSignature() public { bytes memory data = abi.encode(keccak256("RANDOM"), IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } From 3ae15f5c0ae103f22d4771c38f376a0c0f510ddd Mon Sep 17 00:00:00 2001 From: Benjimmutable <123430337+Benjimmutable@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:16:34 +1100 Subject: [PATCH 27/49] Update src/root/RootERC20Bridge.sol Co-authored-by: Ermyas Abebe --- src/root/RootERC20Bridge.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 266777a8a..132fda725 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -376,6 +376,6 @@ contract RootERC20Bridge is IERC20Metadata(rootToken).safeTransfer(receiver, amount); } // slither-disable-next-line reentrancy-events - emit RootChainERC20Withdraw(address(rootToken), childToken, withdrawer, receiver, amount); + emit RootChainERC20Withdraw(rootToken, childToken, withdrawer, receiver, amount); } } From ad5a3d84c967330952eff279bcafc840b46a84ea Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Tue, 7 Nov 2023 09:27:39 +1100 Subject: [PATCH 28/49] Add event to _execute --- src/child/ChildAxelarBridgeAdaptor.sol | 1 + .../child/IChildAxelarBridgeAdaptor.sol | 2 ++ .../root/IRootAxelarBridgeAdaptor.sol | 2 ++ src/root/RootAxelarBridgeAdaptor.sol | 1 + .../RootERC20BridgeWithdraw.t.sol | 4 +-- .../unit/child/ChildAxelarBridgeAdaptor.t.sol | 14 +++++++++- test/unit/root/RootAxelarBridgeAdaptor.t.sol | 27 +++++++++++++++++++ 7 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index cb80d2456..48dbad40a 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -71,6 +71,7 @@ contract ChildAxelarBridgeAdaptor is internal override { + emit AdaptorExecute(sourceChain_, sourceAddress_, payload_); childBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_); } } diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index f7cd63330..ae86f45a1 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -13,4 +13,6 @@ interface IChildAxelarBridgeAdaptorErrors { interface IChildAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the root chain. event AxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); + /// @notice Emitted when an Axelar message is received from the root chain. + event AdaptorExecute(string sourceChain, string sourceAddress_, bytes payload_); } diff --git a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol index d134c9410..1876f24df 100644 --- a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol +++ b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -15,4 +15,6 @@ interface IRootAxelarBridgeAdaptorErrors { interface IRootAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the child chain. event AxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); + /// @notice Emitted when an Axelar message is received from the child chain. + event AdaptorExecute(string sourceChain, string sourceAddress_, bytes payload_); } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 7c9d3877c..4460ec19a 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -88,6 +88,7 @@ contract RootAxelarBridgeAdaptor is internal override { + emit AdaptorExecute(sourceChain_, sourceAddress_, payload_); rootBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_); } } diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 70b0d1400..cf306eb07 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -61,7 +61,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is token.transfer(address(rootBridge), 100 ether); } - function test_RevertsIf_WithdrawWithInvalidSourceAddress() public { + function test_RevertsIf_WithdrawWithInvalidSourceChain() public { bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); bytes32 commandId = bytes32("testCommandId"); @@ -71,7 +71,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is axelarAdaptor.execute(commandId, "INVALID", sourceAddress, data); } - function test_RevertsIf_WithdrawWithInvalidSourceChain() public { + function test_RevertsIf_WithdrawWithInvalidSourceAddress() public { bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); bytes32 commandId = bytes32("testCommandId"); diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 5cdf8e049..40bcfad2b 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -47,7 +47,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro newAdaptor.initialize("root", address(0), address(mockChildAxelarGasService)); } - function test_Execute() public { + function test_Execute_CallsBridge() public { bytes32 commandId = bytes32("testCommandId"); string memory sourceChain = "test"; string memory sourceAddress = Strings.toHexString(address(123)); @@ -61,6 +61,18 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); } + function test_Execute_EmitsAdaptorExecuteEvent() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectEmit(); + emit AdaptorExecute(sourceChain, sourceAddress, payload); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } + function test_sendMessage_CallsGasService() public { address refundRecipient = address(123); bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 5bd34aa86..6323008a6 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {MockAxelarGateway} from "../../../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../../../src/test/root/MockAxelarGasService.sol"; @@ -56,6 +57,32 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR newAdaptor.initialize(address(this), "", address(axelarGasService)); } + function test_Execute_CallsBridge() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectCall( + address(stubRootBridge), + abi.encodeWithSelector(stubRootBridge.onMessageReceive.selector, sourceChain, sourceAddress, payload) + ); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } + + function test_Execute_EmitsAdaptorExecuteEvent() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectEmit(); + emit AdaptorExecute(sourceChain, sourceAddress, payload); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } + /// @dev For this unit test we just want to make sure the correct functions are called on the Axelar Gateway and Gas Service. function test_sendMessage_CallsGasService() public { address refundRecipient = address(123); From 7da7c83fed983f4ab61408d8644c4335ab400500 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Tue, 7 Nov 2023 09:29:26 +1100 Subject: [PATCH 29/49] AxelarMessageSent --- src/child/ChildAxelarBridgeAdaptor.sol | 2 +- src/interfaces/child/IChildAxelarBridgeAdaptor.sol | 2 +- src/interfaces/root/IRootAxelarBridgeAdaptor.sol | 2 +- src/root/RootAxelarBridgeAdaptor.sol | 2 +- .../withdrawals/ChildAxelarBridgeWithdraw.t.sol | 4 ++-- test/integration/root/RootERC20Bridge.t.sol | 12 ++++++------ test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 4 ++-- test/unit/root/RootAxelarBridgeAdaptor.t.sol | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 48dbad40a..5e725257e 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -60,7 +60,7 @@ contract ChildAxelarBridgeAdaptor is ); gateway.callContract(_rootChain, _rootBridgeAdaptor, payload); - emit AxelarMessage(_rootChain, _rootBridgeAdaptor, payload); + emit AxelarMessageSent(_rootChain, _rootBridgeAdaptor, payload); } /** diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index ae86f45a1..cca42edcf 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -12,7 +12,7 @@ interface IChildAxelarBridgeAdaptorErrors { interface IChildAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the root chain. - event AxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); + event AxelarMessageSent(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); /// @notice Emitted when an Axelar message is received from the root chain. event AdaptorExecute(string sourceChain, string sourceAddress_, bytes payload_); } diff --git a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol index 1876f24df..e1cddac17 100644 --- a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol +++ b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -14,7 +14,7 @@ interface IRootAxelarBridgeAdaptorErrors { interface IRootAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the child chain. - event AxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); + event AxelarMessageSent(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); /// @notice Emitted when an Axelar message is received from the child chain. event AdaptorExecute(string sourceChain, string sourceAddress_, bytes payload_); } diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 4460ec19a..e603dadba 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -78,7 +78,7 @@ contract RootAxelarBridgeAdaptor is ); gateway.callContract(_childChain, _childBridgeAdaptor, payload); - emit AxelarMessage(_childChain, _childBridgeAdaptor, payload); + emit AxelarMessageSent(_childChain, _childBridgeAdaptor, payload); } /** diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol index e52abd117..5b098ac91 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol @@ -106,14 +106,14 @@ contract ChildERC20BridgeWithdrawIntegrationTest is childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); } - function test_withdraw_emits_AxelarMessageEvent() public { + function test_withdraw_emits_AxelarMessageSentEvent() public { ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, rootToken, address(this), address(this), withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); } diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 9728d5959..3a767d299 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -51,7 +51,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); vm.expectEmit(true, true, false, false, address(rootBridge)); emit L1TokenMapped(address(token), childToken); @@ -113,7 +113,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx console2.logBytes(predictedPayload); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit NativeEthDeposit( address(NATIVE_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount @@ -169,7 +169,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(IMX_TOKEN_ADDRESS, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit IMXDeposit(address(IMX_TOKEN_ADDRESS), address(this), address(this), tokenAmount); @@ -226,7 +226,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit WETHDeposit(address(WRAPPED_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount); vm.expectCall( @@ -284,7 +284,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, true); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); @@ -340,7 +340,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit ChildChainERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 40bcfad2b..148445c60 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -115,12 +115,12 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } - function test_sendMessage_EmitsAxelarMessageEvent() public { + function test_sendMessage_EmitsAxelarMessageSentEvent() public { bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); uint256 callValue = 300; vm.expectEmit(); - emit AxelarMessage(ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload); + emit AxelarMessageSent(ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload); vm.deal(address(mockChildERC20Bridge), callValue); vm.prank(address(mockChildERC20Bridge)); diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 6323008a6..76281fce0 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -121,12 +121,12 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } - function test_sendMessage_EmitsAxelarMessageEvent() public { + function test_sendMessage_EmitsAxelarMessageSentEvent() public { bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); uint256 callValue = 300; vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptor, payload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptor, payload); vm.prank(address(stubRootBridge)); axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } From fe96c6c095ec7d16369f1b8f0a5f47167b1ad04b Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 10:12:09 +1000 Subject: [PATCH 30/49] Fix build issue --- .../child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol | 4 ++-- .../withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol index 0396c0dee..61dd35c21 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol @@ -102,7 +102,7 @@ contract ChildERC20BridgeWithdrawIMXIntegrationTest is childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); } - function test_WithdrawIMX_EmitsAxelarMessageEvent() public { + function test_WithdrawIMX_EmitsAxelarMessageSentEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -110,7 +110,7 @@ contract ChildERC20BridgeWithdrawIMXIntegrationTest is abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol index 8c6e1fea6..a2a09223c 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol @@ -163,7 +163,7 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_withdrawIMXTo_EmitsAxelarMessageEvent() public { + function test_withdrawIMXTo_EmitsAxelarMessageSentEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -171,12 +171,12 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_withdrawIMXToWithDifferentAccount_EmitsAxelarMessageEvent() public { + function test_withdrawIMXToWithDifferentAccount_EmitsAxelarMessageSentEvent() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -185,7 +185,7 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } From bd95888dace0a4c68e0ef2faf59d6c2461eabb32 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 11:16:46 +1000 Subject: [PATCH 31/49] Add wimx --- .env.sample | 1 + script/DeployChildContracts.s.sol | 8 ++-- script/InitializeChildContracts.s.sol | 8 +++- src/child/ChildERC20Bridge.sol | 17 ++++++++- .../integration/child/ChildAxelarBridge.t.sol | 4 +- test/unit/child/ChildERC20Bridge.t.sol | 37 +++++++++++++++---- .../ChildERC20BridgeWithdraw.t.sol | 8 +++- .../ChildERC20BridgeWithdrawIMX.t.sol | 8 +++- .../ChildERC20BridgeWithdrawTo.t.sol | 8 +++- .../ChildERC20BridgeWithdrawToIMX.t.sol | 8 +++- test/utils.t.sol | 5 ++- 11 files changed, 92 insertions(+), 20 deletions(-) diff --git a/.env.sample b/.env.sample index 2fd22f8eb..1201cf15d 100644 --- a/.env.sample +++ b/.env.sample @@ -10,6 +10,7 @@ ROOT_GAS_SERVICE_ADDRESS= CHILD_GAS_SERVICE_ADDRESS= ROOT_CHAIN_NAME= CHILD_CHAIN_NAME= +CHILD_WIMX_ADDRESS= ROOT_IMX_ADDRESS= ROOT_WETH_ADDRESS= ENVIRONMENT= diff --git a/script/DeployChildContracts.s.sol b/script/DeployChildContracts.s.sol index 530eadb4e..dbae02ee1 100644 --- a/script/DeployChildContracts.s.sol +++ b/script/DeployChildContracts.s.sol @@ -22,13 +22,17 @@ contract DeployChildContracts is Script { vm.createSelectFork(childRpcUrl); vm.startBroadcast(deployerPrivateKey); + WIMX wrappedIMX = new WIMX(); + ProxyAdmin proxyAdmin = new ProxyAdmin(); ChildERC20 childTokenTemplate = new ChildERC20(); childTokenTemplate.initialize(address(123), "TEMPLATE", "TPT", 18); ChildERC20Bridge childERC20BridgeImplementation = new ChildERC20Bridge(); - childERC20BridgeImplementation.initialize(address(1), "0x123", address(1), "root", address(1)); + childERC20BridgeImplementation.initialize( + address(1), "0x123", address(1), "root", address(1), address(wrappedIMX) + ); TransparentUpgradeableProxy childERC20BridgeProxy = new TransparentUpgradeableProxy( address(childERC20BridgeImplementation), @@ -46,8 +50,6 @@ contract DeployChildContracts is Script { "" ); - WIMX wrappedIMX = new WIMX(); - vm.stopBroadcast(); console2.log("====CHILD ADDRESSES===="); diff --git a/script/InitializeChildContracts.s.sol b/script/InitializeChildContracts.s.sol index 314ade367..6b5ef69ec 100644 --- a/script/InitializeChildContracts.s.sol +++ b/script/InitializeChildContracts.s.sol @@ -20,6 +20,7 @@ contract InitializeChildContracts is Script { string memory childRpcUrl = vm.envString("CHILD_RPC_URL"); string memory rootChainName = vm.envString("ROOT_CHAIN_NAME"); address rootIMXToken = vm.envAddress("ROOT_IMX_ADDRESS"); + address wIMXToken = vm.envAddress("CHILD_WIMX_ADDRESS"); address childGasService = vm.envAddress("CHILD_GAS_SERVICE_ADDRESS"); // Not yet used. /** @@ -33,7 +34,12 @@ contract InitializeChildContracts is Script { vm.startBroadcast(deployerPrivateKey); childERC20Bridge.initialize( - address(childAxelarBridgeAdaptor), rootBridgeAdaptorString, childTokenTemplate, rootChainName, rootIMXToken + address(childAxelarBridgeAdaptor), + rootBridgeAdaptorString, + childTokenTemplate, + rootChainName, + rootIMXToken, + wIMXToken ); childAxelarBridgeAdaptor.initialize(rootChainName, address(childERC20Bridge), childGasService); diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 3a8553eab..858e0d850 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -15,6 +15,7 @@ import { } from "../interfaces/child/IChildERC20Bridge.sol"; import {IChildERC20BridgeAdaptor} from "../interfaces/child/IChildERC20BridgeAdaptor.sol"; import {IChildERC20} from "../interfaces/child/IChildERC20.sol"; +import {IWIMX} from "../interfaces/child/IWIMX.sol"; /** * @notice RootERC20Bridge is a bridge that allows ERC20 tokens to be transferred from the root chain to the child chain. @@ -54,6 +55,8 @@ contract ChildERC20Bridge is address public rootIMXToken; /// @dev The address of the ETH ERC20 token on L2. address public childETHToken; + /// @dev The address of the wrapped IMX token on L2. + address public wIMXToken; /** * @notice Initilization function for RootERC20Bridge. @@ -69,9 +72,13 @@ contract ChildERC20Bridge is string memory newRootERC20BridgeAdaptor, address newChildTokenTemplate, string memory newRootChain, - address newRootIMXToken + address newRootIMXToken, + address newWIMXToken ) public initializer { - if (newBridgeAdaptor == address(0) || newChildTokenTemplate == address(0) || newRootIMXToken == address(0)) { + if ( + newBridgeAdaptor == address(0) || newChildTokenTemplate == address(0) || newRootIMXToken == address(0) + || newWIMXToken == address(0) + ) { revert ZeroAddress(); } @@ -88,6 +95,7 @@ contract ChildERC20Bridge is bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor); rootChain = newRootChain; rootIMXToken = newRootIMXToken; + wIMXToken = newWIMXToken; IChildERC20 clonedETHToken = IChildERC20(Clones.cloneDeterministic(childTokenTemplate, keccak256(abi.encodePacked(NATIVE_ETH)))); @@ -171,6 +179,11 @@ contract ChildERC20Bridge is if (address(childToken) == NATIVE_IMX) { feeAmount = msg.value - amount; rootToken = rootIMXToken; + } else if (address(childToken) == wIMXToken) { + rootToken = rootIMXToken; + IWIMX wIMX = IWIMX(wIMXToken); + require(wIMX.transferFrom(msg.sender, address(this), amount), "ChildERC20Bridge: fail to transfer wIMX"); + wIMX.withdraw(amount); } else { if (address(childToken).code.length == 0) { revert EmptyTokenContract(); diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index c73f41a68..7d13f13a9 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -20,6 +20,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil string public ROOT_ADAPTOR_ADDRESS = Strings.toHexString(address(1)); string public ROOT_CHAIN_NAME = "ROOT_CHAIN"; address constant IMX_TOKEN_ADDRESS = address(0xccc); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); address constant NATIVE_ETH = address(0xeee); ChildERC20Bridge public childERC20Bridge; @@ -42,7 +43,8 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil ROOT_ADAPTOR_ADDRESS, address(childERC20), ROOT_CHAIN_NAME, - IMX_TOKEN_ADDRESS + IMX_TOKEN_ADDRESS, + WIMX_TOKEN_ADDRESS ); childAxelarBridgeAdaptor.initialize( diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 7bb37a263..08211482c 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -20,6 +20,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); + address constant CHILD_WIMX_TOKEN = address(0xabc); address constant NATIVE_ETH = address(0xeee); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; @@ -35,7 +36,12 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B childBridge = new ChildERC20Bridge(); childBridge.initialize( - address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + address(this), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + CHILD_WIMX_TOKEN ); } @@ -52,44 +58,59 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B function test_RevertIfInitializeTwice() public { vm.expectRevert("Initializable: contract is already initialized"); childBridge.initialize( - address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + address(this), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + CHILD_WIMX_TOKEN ); } function test_RevertIf_InitializeWithAZeroAddressAdapter() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(1)); + bridge.initialize(address(0), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressChildTemplate() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(0), ROOT_CHAIN_NAME, address(1)); + bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(0), ROOT_CHAIN_NAME, address(1), address(1)); } function test_RevertIf_InitializeWithAZeroAddressIMXToken() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(0)); + bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(0), address(1)); + } + + function test_RevertIf_InitializeWithAZeroAddressWIMXToken() public { + ChildERC20Bridge bridge = new ChildERC20Bridge(); + vm.expectRevert(ZeroAddress.selector); + bridge.initialize(address(1), ROOT_BRIDGE_ADAPTOR, address(1), ROOT_CHAIN_NAME, address(1), address(0)); } function test_RevertIf_InitializeWithAZeroAddressAll() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), ROOT_BRIDGE_ADAPTOR, address(0), ROOT_CHAIN_NAME, address(0)); + bridge.initialize(address(0), ROOT_BRIDGE_ADAPTOR, address(0), ROOT_CHAIN_NAME, address(0), address(0)); } function test_RevertIf_InitializeWithAnEmptyBridgeAdaptorString() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(InvalidRootERC20BridgeAdaptor.selector); - bridge.initialize(address(this), "", address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN); + bridge.initialize( + address(this), "", address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN, CHILD_WIMX_TOKEN + ); } function test_RevertIf_InitializeWithAnEmptyChainNameString() public { ChildERC20Bridge bridge = new ChildERC20Bridge(); vm.expectRevert(InvalidRootChain.selector); - bridge.initialize(address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), "", ROOT_IMX_TOKEN); + bridge.initialize( + address(this), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), "", ROOT_IMX_TOKEN, CHILD_WIMX_TOKEN + ); } function test_onMessageReceive_EmitsTokenMappedEvent() public { diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index 532b1f43e..b9f468511 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -22,6 +22,7 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); address constant NATIVE_ETH = address(0xeee); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; ChildERC20 public childToken; @@ -40,7 +41,12 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi childBridge = new ChildERC20Bridge(); childBridge.initialize( - address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + WIMX_TOKEN_ADDRESS ); bytes memory mapTokenData = diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol index 21fd728ab..0640ad868 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMX.t.sol @@ -19,6 +19,7 @@ contract ChildERC20BridgeWithdrawIMXUnitTest is Test, IChildERC20BridgeEvents, I string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); ChildERC20 public childTokenTemplate; ChildERC20Bridge public childBridge; MockAdaptor public mockAdaptor; @@ -31,7 +32,12 @@ contract ChildERC20BridgeWithdrawIMXUnitTest is Test, IChildERC20BridgeEvents, I childBridge = new ChildERC20Bridge(); childBridge.initialize( - address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + WIMX_TOKEN_ADDRESS ); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index a2b67262b..11e6a30ad 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -21,6 +21,7 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); address constant NATIVE_ETH = address(0xeee); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; @@ -40,7 +41,12 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC childBridge = new ChildERC20Bridge(); childBridge.initialize( - address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + WIMX_TOKEN_ADDRESS ); bytes memory mapTokenData = diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol index c8d997ea9..2dba0d99d 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol @@ -21,6 +21,7 @@ contract ChildERC20BridgewithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); address constant NATIVE_ETH = address(0xeee); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; @@ -37,7 +38,12 @@ contract ChildERC20BridgewithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, childBridge = new ChildERC20Bridge(); childBridge.initialize( - address(mockAdaptor), ROOT_BRIDGE_ADAPTOR, address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + WIMX_TOKEN_ADDRESS ); } diff --git a/test/utils.t.sol b/test/utils.t.sol index b186fbbc4..91850d2a7 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -34,6 +34,7 @@ contract Utils is Test { string memory rootAdaptor = Strings.toHexString(address(99999)); rootIMX = address(555555); rootToken = address(44444); + address childWIMX = address(0xabc); axelarGasService = new MockAxelarGasService(); mockAxelarGateway = new MockAxelarGateway(); @@ -41,7 +42,9 @@ contract Utils is Test { childTokenTemplate.initialize(address(1), "Test", "TST", 18); childBridge = new ChildERC20Bridge(); childBridgeAdaptor = new ChildAxelarBridgeAdaptor(address(mockAxelarGateway)); - childBridge.initialize(address(childBridgeAdaptor), rootAdaptor, address(childTokenTemplate), "ROOT", rootIMX); + childBridge.initialize( + address(childBridgeAdaptor), rootAdaptor, address(childTokenTemplate), "ROOT", rootIMX, childWIMX + ); childBridgeAdaptor.initialize("ROOT", address(childBridge), address(axelarGasService)); bytes memory mapTokenData = abi.encode(MAP_TOKEN_SIG, rootToken, "TEST NAME", "TNM", 18); From 898c46bbfb4f70a42a7b00ba881e3ff8737a67f7 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 12:44:14 +1000 Subject: [PATCH 32/49] Update --- src/child/ChildERC20Bridge.sol | 33 ++++++++++++++++--- src/interfaces/child/IChildERC20Bridge.sol | 5 +++ .../ChildERC20BridgeWithdraw.t.sol | 7 ++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 858e0d850..1a8ddc6e7 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -151,6 +151,14 @@ contract ChildERC20Bridge is _withdrawIMX(receiver, amount); } + function withdrawWIMX(uint256 amount) external payable { + _withdrawWIMX(msg.sender, amount); + } + + function withdrawWIMXTo(address receiver, uint256 amount) external payable { + _withdrawWIMX(receiver, amount); + } + function _withdrawIMX(address receiver, uint256 amount) private { if (msg.value < amount) { revert InsufficientValue(); @@ -179,11 +187,6 @@ contract ChildERC20Bridge is if (address(childToken) == NATIVE_IMX) { feeAmount = msg.value - amount; rootToken = rootIMXToken; - } else if (address(childToken) == wIMXToken) { - rootToken = rootIMXToken; - IWIMX wIMX = IWIMX(wIMXToken); - require(wIMX.transferFrom(msg.sender, address(this), amount), "ChildERC20Bridge: fail to transfer wIMX"); - wIMX.withdraw(amount); } else { if (address(childToken).code.length == 0) { revert EmptyTokenContract(); @@ -225,6 +228,26 @@ contract ChildERC20Bridge is } } + function _withdrawWIMX(address receiver, uint256 amount) private { + uint256 expectedBalance = address(this).balance + amount; + + IWIMX wIMX = IWIMX(wIMXToken); + if (!wIMX.transferFrom(msg.sender, address(this), amount)) { + revert TransferWIMXFailed(); + } + wIMX.withdraw(amount); + + if (address(this).balance != expectedBalance) { + revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance); + } + + bytes memory payload = abi.encode(WITHDRAW_SIG, rootIMXToken, msg.sender, receiver, amount); + + bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); + + emit ChildChainWrappedIMXWithdraw(rootIMXToken, msg.sender, receiver, amount); + } + function _mapToken(bytes calldata data) private { (, address rootToken, string memory name, string memory symbol, uint8 decimals) = abi.decode(data, (bytes32, address, string, string, uint8)); diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 24f6d0332..862e722f5 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -35,6 +35,9 @@ interface IChildERC20BridgeEvents { event ChildChainNativeIMXWithdraw( address indexed rootToken, address depositor, address indexed receiver, uint256 amount ); + event ChildChainWrappedIMXWithdraw( + address indexed rootToken, address depositor, address indexed receiver, uint256 amount + ); event ChildChainERC20Deposit( address indexed rootToken, @@ -93,6 +96,8 @@ interface IChildERC20BridgeErrors { error IncorrectBridgeAddress(); /// @notice Error when a call to the given child token's `burn` function fails. error BurnFailed(); + /// @notice Error when a call to WIMX token's `transferFrom` fails. + error TransferWIMXFailed(); /// @notice Error when token balance invariant check fails. error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); } diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index b9f468511..c1240bd41 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -13,6 +13,7 @@ import { } from "../../../../src/child/ChildERC20Bridge.sol"; import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; @@ -22,10 +23,10 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); address constant NATIVE_ETH = address(0xeee); - address constant WIMX_TOKEN_ADDRESS = address(0xabc); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; ChildERC20 public childToken; + WIMX public wIMXToken; address public childETHToken; ChildERC20Bridge public childBridge; MockAdaptor public mockAdaptor; @@ -37,6 +38,8 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi childTokenTemplate = new ChildERC20(); childTokenTemplate.initialize(address(123), "Test", "TST", 18); + wIMXToken = new WIMX(); + mockAdaptor = new MockAdaptor(); childBridge = new ChildERC20Bridge(); @@ -46,7 +49,7 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN, - WIMX_TOKEN_ADDRESS + address(wIMXToken) ); bytes memory mapTokenData = From 136e262415a88304b06a138a3d2aabf37f25848f Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 12:48:35 +1000 Subject: [PATCH 33/49] Update --- test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol index c1240bd41..b9f468511 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdraw.t.sol @@ -13,7 +13,6 @@ import { } from "../../../../src/child/ChildERC20Bridge.sol"; import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; -import {WIMX} from "../../../../src/child/WIMX.sol"; import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; @@ -23,10 +22,10 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi string constant ROOT_CHAIN_NAME = "test"; address constant ROOT_IMX_TOKEN = address(0xccc); address constant NATIVE_ETH = address(0xeee); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); ChildERC20 public childTokenTemplate; ChildERC20 public rootToken; ChildERC20 public childToken; - WIMX public wIMXToken; address public childETHToken; ChildERC20Bridge public childBridge; MockAdaptor public mockAdaptor; @@ -38,8 +37,6 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi childTokenTemplate = new ChildERC20(); childTokenTemplate.initialize(address(123), "Test", "TST", 18); - wIMXToken = new WIMX(); - mockAdaptor = new MockAdaptor(); childBridge = new ChildERC20Bridge(); @@ -49,7 +46,7 @@ contract ChildERC20BridgeWithdrawUnitTest is Test, IChildERC20BridgeEvents, IChi address(childTokenTemplate), ROOT_CHAIN_NAME, ROOT_IMX_TOKEN, - address(wIMXToken) + WIMX_TOKEN_ADDRESS ); bytes memory mapTokenData = From 962a9f5ff04cc7de1a01dc833a2119157608fae8 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 12:54:28 +1000 Subject: [PATCH 34/49] Update ChildERC20Bridge.sol --- src/child/ChildERC20Bridge.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 1a8ddc6e7..6d7d8e626 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -229,6 +229,10 @@ contract ChildERC20Bridge is } function _withdrawWIMX(address receiver, uint256 amount) private { + if (amount == 0) { + revert ZeroAmount(); + } + uint256 expectedBalance = address(this).balance + amount; IWIMX wIMX = IWIMX(wIMXToken); From 0275ef81bcf2f75097788f984ff275cfc7ee7316 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 14:22:53 +1000 Subject: [PATCH 35/49] Update --- script/InitializeChildContracts.s.sol | 2 +- src/child/ChildERC20Bridge.sol | 5 + .../ChildERC20BridgeWithdrawWIMX.t.sol | 127 ++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol diff --git a/script/InitializeChildContracts.s.sol b/script/InitializeChildContracts.s.sol index 6b5ef69ec..b657cf281 100644 --- a/script/InitializeChildContracts.s.sol +++ b/script/InitializeChildContracts.s.sol @@ -12,7 +12,7 @@ import {Utils} from "./Utils.sol"; contract InitializeChildContracts is Script { function run() public { uint256 deployerPrivateKey = vm.envUint("CHILD_PRIVATE_KEY"); - ChildERC20Bridge childERC20Bridge = ChildERC20Bridge(vm.envAddress("CHILD_ERC20_BRIDGE")); + ChildERC20Bridge childERC20Bridge = ChildERC20Bridge(payable(vm.envAddress("CHILD_ERC20_BRIDGE"))); ChildAxelarBridgeAdaptor childAxelarBridgeAdaptor = ChildAxelarBridgeAdaptor(vm.envAddress("CHILD_BRIDGE_ADAPTOR")); address childTokenTemplate = vm.envAddress("CHILDCHAIN_CHILD_TOKEN_TEMPLATE"); diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 6d7d8e626..e28e79a96 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -58,6 +58,11 @@ contract ChildERC20Bridge is /// @dev The address of the wrapped IMX token on L2. address public wIMXToken; + /** + * @notice Fallback function on recieving native IMX. + */ + receive() external payable {} + /** * @notice Initilization function for RootERC20Bridge. * @param newBridgeAdaptor Address of StateSender to send deposit information to. diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol new file mode 100644 index 000000000..efb351157 --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import "forge-std/console.sol"; + +contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); + ChildERC20 public childTokenTemplate; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + WIMX public wIMXToken; + + function setUp() public { + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + wIMXToken = new WIMX(); + Address.sendValue(payable(wIMXToken), 100 ether); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + address(wIMXToken) + ); + } + + function test_RevertsIf_WithdrawWIMXCalledWithInsufficientFund() public { + uint256 withdrawAmount = 101 ether; + uint256 withdrawFee = 300; + + wIMXToken.approve(address(childBridge), withdrawAmount); + vm.expectRevert(bytes("Wrapped IMX: Insufficient balance")); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_RevertsIf_WithdrawWIMXCalledWithInsufficientAllowance() public { + uint256 withdrawAmount = 99 ether; + uint256 withdrawFee = 300; + + wIMXToken.approve(address(childBridge), withdrawAmount - 1); + vm.expectRevert(bytes("Wrapped IMX: Insufficient allowance")); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_RevertsIf_ZeroAmountIsProvided() public { + uint256 withdrawFee = 300; + + vm.expectRevert(ZeroAmount.selector); + childBridge.withdrawWIMX{value: withdrawFee}(0); + } + + function test_WithdrawWIMX_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + wIMXToken.approve(address(childBridge), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawWIMX_EmitsWrappedIMXWithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + vm.expectEmit(address(childBridge)); + emit ChildChainWrappedIMXWithdraw(ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawWIMX_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = wIMXToken.balanceOf(address(this)); + + wIMXToken.approve(address(childBridge), withdrawAmount); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + + uint256 postBal = wIMXToken.balanceOf(address(this)); + assertEq(postBal, preBal - withdrawAmount, "Balance not reduced"); + } + + function test_WithdrawWIMX_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + + wIMXToken.approve(address(childBridge), withdrawAmount); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + + uint256 postBal = address(this).balance; + assertEq(postBal, preBal - withdrawFee, "Fee not paid"); + } +} From 753b1bca8af19670270f7b1b66cec92bb0daf58c Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 19:38:33 +1000 Subject: [PATCH 36/49] Update --- .../ChildERC20BridgeWithdrawWIMX.t.sol | 1 - .../ChildERC20BridgeWithdrawWIMXTo.t.sol | 154 ++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol index efb351157..04be2d6f5 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMX.t.sol @@ -15,7 +15,6 @@ import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; import {WIMX} from "../../../../src/child/WIMX.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import "forge-std/console.sol"; contract ChildERC20BridgeWithdrawWIMXUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { address constant ROOT_BRIDGE = address(3); diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol new file mode 100644 index 000000000..393cc6ce7 --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); + ChildERC20 public childTokenTemplate; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + WIMX public wIMXToken; + + function setUp() public { + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + wIMXToken = new WIMX(); + Address.sendValue(payable(wIMXToken), 100 ether); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + address(wIMXToken) + ); + } + + function test_RevertsIf_WithdrawWIMXToCalledWithInsufficientFund() public { + uint256 withdrawAmount = 101 ether; + uint256 withdrawFee = 300; + + wIMXToken.approve(address(childBridge), withdrawAmount); + vm.expectRevert(bytes("Wrapped IMX: Insufficient balance")); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_RevertsIf_WithdrawWIMXToCalledWithInsufficientAllowance() public { + uint256 withdrawAmount = 99 ether; + uint256 withdrawFee = 300; + + wIMXToken.approve(address(childBridge), withdrawAmount - 1); + vm.expectRevert(bytes("Wrapped IMX: Insufficient allowance")); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_RevertsIf_ZeroAmountIsProvided() public { + uint256 withdrawFee = 300; + + vm.expectRevert(ZeroAmount.selector); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), 0); + } + + function test_WithdrawWIMXTo_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + wIMXToken.approve(address(childBridge), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawWIMXToWithDifferentAccount_CallsBridgeAdaptor() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + wIMXToken.approve(address(childBridge), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawWIMXTo_EmitsWrappedIMXWithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + vm.expectEmit(address(childBridge)); + emit ChildChainWrappedIMXWithdraw(ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawWIMXToWithDifferentAccount_EmitsWrappedIMXWithdrawEvent() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + vm.expectEmit(address(childBridge)); + emit ChildChainWrappedIMXWithdraw(ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawWIMXTo_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = wIMXToken.balanceOf(address(this)); + + wIMXToken.approve(address(childBridge), withdrawAmount); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + + uint256 postBal = wIMXToken.balanceOf(address(this)); + assertEq(postBal, preBal - withdrawAmount, "Balance not reduced"); + } + + function test_WithdrawWIMXTo_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + + wIMXToken.approve(address(childBridge), withdrawAmount); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + + uint256 postBal = address(this).balance; + assertEq(postBal, preBal - withdrawFee, "Fee not paid"); + } +} \ No newline at end of file From 4224bd6e2b40c6ca2088c982bfafd6be748ec152 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Tue, 7 Nov 2023 20:09:23 +1000 Subject: [PATCH 37/49] Update --- .../ChildAxelarBridgeWithdrawWIMX.t.sol | 148 ++++++++++++ .../ChildAxelarBridgeWithdrawWIMXTo.t.sol | 228 ++++++++++++++++++ test/utils.t.sol | 3 + 3 files changed, 379 insertions(+) create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol new file mode 100644 index 000000000..0e0a44c2a --- /dev/null +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract ChildERC20BridgeWithdrawWIMXIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(555555); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + address constant WRAPPED_IMX = address(0xabc); + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + WIMX public wIMXToken; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + wIMXToken = WIMX(payable(WRAPPED_IMX)); + Address.sendValue(payable(wIMXToken), 100 ether); + } + + function test_WithdrawWIMX_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawWIMX_CallsAxelarGateway() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawWIMX_CallsGasService() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawWIMX_EmitsAxelarMessageSentEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawWIMX_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + uint256 preTokenBal = wIMXToken.balanceOf(address(this)); + uint256 preGasBal = address(axelarGasService).balance; + + wIMXToken.approve(address(childBridge), withdrawAmount); + childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); + + uint256 postBal = address(this).balance; + uint256 postTokenBal = wIMXToken.balanceOf(address(this)); + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawFee, "Balance not reduced"); + assertEq(postTokenBal, preTokenBal - withdrawAmount); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); + } +} \ No newline at end of file diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol new file mode 100644 index 000000000..d8110e412 --- /dev/null +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract ChildERC20BridgeWithdrawWIMXToIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(555555); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + address constant WRAPPED_IMX = address(0xabc); + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + WIMX public wIMXToken; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + wIMXToken = WIMX(payable(WRAPPED_IMX)); + Address.sendValue(payable(wIMXToken), 100 ether); + } + + function test_WithdrawWIMXTo_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawWIMXToWithDifferentAccount_CallsBridgeAdaptor() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + + childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawWIMXTo_CallsAxelarGateway() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawWIMXToWithDifferentAccount_CallsAxelarGateway() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawWIMXTo_CallsGasService() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawWIMXToWithDifferentAccount_CallsGasService() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawWIMXTo_EmitsAxelarMessageSentEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawWIMXToWithDifferentAccount_EmitsAxelarMessageSentEvent() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + wIMXToken.approve(address(childBridge), withdrawAmount); + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawWIMXTo_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + uint256 preTokenBal = wIMXToken.balanceOf(address(this)); + uint256 preGasBal = address(axelarGasService).balance; + + wIMXToken.approve(address(childBridge), withdrawAmount); + childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); + + uint256 postBal = address(this).balance; + uint256 postTokenBal = wIMXToken.balanceOf(address(this)); + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawFee, "Balance not reduced"); + assertEq(postTokenBal, preTokenBal - withdrawAmount); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); + } +} \ No newline at end of file diff --git a/test/utils.t.sol b/test/utils.t.sol index 2eecfec9f..c297cb41a 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -11,6 +11,7 @@ import {ChildERC20Bridge} from "../src/child/ChildERC20Bridge.sol"; import {ChildAxelarBridgeAdaptor} from "../src/child/ChildAxelarBridgeAdaptor.sol"; import {WETH} from "../src/test/root/WETH.sol"; import {IWETH} from "../src/interfaces/root/IWETH.sol"; +import {WIMX} from "../src/child/WIMX.sol"; import {IChildERC20, ChildERC20} from "../src/child/ChildERC20.sol"; import {RootAxelarBridgeAdaptor} from "../src/root/RootAxelarBridgeAdaptor.sol"; @@ -36,6 +37,8 @@ contract Utils is Test { rootToken = address(44444); address childWIMX = address(0xabc); + deployCodeTo("WIMX.sol", childWIMX); + axelarGasService = new MockAxelarGasService(); mockAxelarGateway = new MockAxelarGateway(); childTokenTemplate = new ChildERC20(); From 7631a969b12e224aa67557713ed6e24b32ad5f2e Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 7 Nov 2023 21:19:38 +1100 Subject: [PATCH 38/49] Fix test naming inconsistencies --- test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol | 4 ++-- .../child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol index a2b67262b..0f187841a 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawTo.t.sol @@ -135,7 +135,7 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC childBridge.withdrawTo{value: withdrawFee}(IChildERC20(address(childToken)), address(this), withdrawAmount); } - function test_withdraw_ReducesBalance() public { + function test_withdrawTo_ReducesBalance() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -147,7 +147,7 @@ contract ChildERC20BridgeWithdrawToUnitTest is Test, IChildERC20BridgeEvents, IC assertEq(postBal, preBal - withdrawAmount); } - function test_withdraw_ReducesTotalSupply() public { + function test_withdrawTo_ReducesTotalSupply() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol index c8d997ea9..e69be5261 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol @@ -16,7 +16,7 @@ import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; -contract ChildERC20BridgewithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { +contract ChildERC20BridgeWithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { address constant ROOT_BRIDGE = address(3); string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); string constant ROOT_CHAIN_NAME = "test"; From cb03f92a4119fcd6d56e5091b10326187b1a76c1 Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 7 Nov 2023 22:01:45 +1100 Subject: [PATCH 39/49] Fix incorrect test name --- test/unit/root/RootAxelarBridgeAdaptor.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 76281fce0..0182e95cc 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -38,20 +38,20 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR vm.deal(address(stubRootBridge), 99999999999); } - function test_Constructor() public { + function test_Initialize() public { assertEq(address(axelarAdaptor.rootBridge()), address(stubRootBridge), "rootBridge not set"); assertEq(axelarAdaptor.childChain(), CHILD_CHAIN_NAME, "childChain not set"); assertEq(address(axelarAdaptor.gateway()), address(mockAxelarGateway), "axelarGateway not set"); assertEq(address(axelarAdaptor.gasService()), address(axelarGasService), "axelarGasService not set"); } - function test_RevertWhen_InitializerGivenZeroAddress() public { + function test_RevertWhen_InitializeGivenZeroAddress() public { RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(ZeroAddresses.selector); newAdaptor.initialize(address(0), CHILD_CHAIN_NAME, address(axelarGasService)); } - function test_RevertWhen_ConstructorGivenEmptyChildChainName() public { + function test_RevertWhen_InitializeGivenEmptyChildChainName() public { RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(InvalidChildChain.selector); newAdaptor.initialize(address(this), "", address(axelarGasService)); From 1ae35ccfe131282d28f56c0633bb40728f33726d Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Tue, 7 Nov 2023 22:24:49 +1100 Subject: [PATCH 40/49] Fix incorrect test name --- test/unit/child/ChildAxelarBridgeAdaptor.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 148445c60..eaea950c0 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -34,7 +34,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.initialize(ROOT_CHAIN_NAME, address(mockChildERC20Bridge), address(mockChildAxelarGasService)); } - function test_Constructor_SetsValues() public { + function test_Initialize() public { assertEq(address(axelarAdaptor.childBridge()), address(mockChildERC20Bridge), "childBridge not set"); assertEq(address(axelarAdaptor.gateway()), address(mockChildAxelarGateway), "gateway not set"); assertEq(axelarAdaptor.rootChain(), ROOT_CHAIN_NAME, "rootChain not set"); From d5776a788203aacb9a342d6af6b85e2c9ea39962 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 08:01:34 +1000 Subject: [PATCH 41/49] Fmt --- src/child/ChildERC20Bridge.sol | 2 +- .../child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol | 3 +-- .../child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol | 4 +--- .../child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 158d51d5e..2f01817a7 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -61,7 +61,7 @@ contract ChildERC20Bridge is /** * @notice Fallback function on recieving native IMX. */ - receive() external payable {} + receive() external payable {} /** * @notice Initilization function for RootERC20Bridge. diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol index 0e0a44c2a..7fdc4a91b 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMX.t.sol @@ -63,7 +63,6 @@ contract ChildERC20BridgeWithdrawWIMXIntegrationTest is abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawWIMX{value: withdrawFee}(withdrawAmount); } @@ -145,4 +144,4 @@ contract ChildERC20BridgeWithdrawWIMXIntegrationTest is assertEq(postTokenBal, preTokenBal - withdrawAmount); assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); } -} \ No newline at end of file +} diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol index d8110e412..68ef03436 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawWIMXTo.t.sol @@ -63,7 +63,6 @@ contract ChildERC20BridgeWithdrawWIMXToIntegrationTest is abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawWIMXTo{value: withdrawFee}(address(this), withdrawAmount); } @@ -81,7 +80,6 @@ contract ChildERC20BridgeWithdrawWIMXToIntegrationTest is abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) ); - childBridge.withdrawWIMXTo{value: withdrawFee}(receiver, withdrawAmount); } @@ -225,4 +223,4 @@ contract ChildERC20BridgeWithdrawWIMXToIntegrationTest is assertEq(postTokenBal, preTokenBal - withdrawAmount); assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); } -} \ No newline at end of file +} diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol index 393cc6ce7..326826715 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawWIMXTo.t.sol @@ -151,4 +151,4 @@ contract ChildERC20BridgeWithdrawWIMXToUnitTest is Test, IChildERC20BridgeEvents uint256 postBal = address(this).balance; assertEq(postBal, preBal - withdrawFee, "Fee not paid"); } -} \ No newline at end of file +} From c3898fd4c5b0cb3498fc73e9802b5634d7e1708f Mon Sep 17 00:00:00 2001 From: Ermyas Abebe Date: Wed, 8 Nov 2023 09:06:23 +1100 Subject: [PATCH 42/49] Fix incorrect test name --- .../child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol index e69be5261..1aea350d1 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol @@ -105,7 +105,7 @@ contract ChildERC20BridgeWithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_WithdrawIMX_ReducesBalance() public { + function test_WithdrawIMXTo_ReducesBalance() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -117,7 +117,7 @@ contract ChildERC20BridgeWithdrawIMXToUnitTest is Test, IChildERC20BridgeEvents, assertEq(postBal, preBal - withdrawAmount - withdrawFee, "Balance not reduced"); } - function test_WithdrawIMX_PaysFee() public { + function test_WithdrawIMXTo_PaysFee() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; From baf38b6caf7e7f24380e2f6bccd3e65362fd77f6 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 13:16:21 +1000 Subject: [PATCH 43/49] Code simplify --- src/child/ChildERC20Bridge.sol | 102 ++++++++++++++------------------- 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 2f01817a7..a07294fe2 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -108,6 +108,13 @@ contract ChildERC20Bridge is childETHToken = address(clonedETHToken); } + function updateBridgeAdaptor(address newBridgeAdaptor) external override onlyOwner { + if (newBridgeAdaptor == address(0)) { + revert ZeroAddress(); + } + bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor); + } + /** * @inheritdoc IChildERC20Bridge * @dev This is only callable by the child chain bridge adaptor. @@ -142,45 +149,31 @@ contract ChildERC20Bridge is } function withdraw(IChildERC20 childToken, uint256 amount) external payable { - _withdraw(childToken, msg.sender, amount); + _withdraw(address(childToken), msg.sender, amount); } function withdrawTo(IChildERC20 childToken, address receiver, uint256 amount) external payable { - _withdraw(childToken, receiver, amount); + _withdraw(address(childToken), receiver, amount); } function withdrawIMX(uint256 amount) external payable { - _withdrawIMX(msg.sender, amount); + _withdraw(NATIVE_IMX, msg.sender, amount); } function withdrawIMXTo(address receiver, uint256 amount) external payable { - _withdrawIMX(receiver, amount); + _withdraw(NATIVE_IMX, receiver, amount); } function withdrawWIMX(uint256 amount) external payable { - _withdrawWIMX(msg.sender, amount); + _withdraw(wIMXToken, msg.sender, amount); } function withdrawWIMXTo(address receiver, uint256 amount) external payable { - _withdrawWIMX(receiver, amount); - } - - function _withdrawIMX(address receiver, uint256 amount) private { - if (msg.value < amount) { - revert InsufficientValue(); - } - - uint256 expectedBalance = address(this).balance - (msg.value - amount); - - _withdraw(IChildERC20(NATIVE_IMX), receiver, amount); - - if (address(this).balance != expectedBalance) { - revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance); - } + _withdraw(wIMXToken, receiver, amount); } - function _withdraw(IChildERC20 childToken, address receiver, uint256 amount) private { - if (address(childToken) == address(0)) { + function _withdraw(address childTokenAddr, address receiver, uint256 amount) private { + if (childTokenAddr == address(0)) { revert ZeroAddress(); } if (amount == 0) { @@ -189,11 +182,34 @@ contract ChildERC20Bridge is address rootToken; uint256 feeAmount = msg.value; + if (childTokenAddr == NATIVE_IMX) { + // Native IMX. + if (msg.value < amount) { + revert InsufficientValue(); + } - if (address(childToken) == NATIVE_IMX) { feeAmount = msg.value - amount; + rootToken = rootIMXToken; + } else if (childTokenAddr == wIMXToken) { + // Wrapped IMX. + // Transfer and unwrap IMX. + uint256 expectedBalance = address(this).balance + amount; + + IWIMX wIMX = IWIMX(wIMXToken); + if (!wIMX.transferFrom(msg.sender, address(this), amount)) { + revert TransferWIMXFailed(); + } + wIMX.withdraw(amount); + + if (address(this).balance != expectedBalance) { + revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance); + } + rootToken = rootIMXToken; } else { + // Other ERC20 Tokens + IChildERC20 childToken = IChildERC20(childTokenAddr); + if (address(childToken).code.length == 0) { revert EmptyTokenContract(); } @@ -218,46 +234,21 @@ contract ChildERC20Bridge is } } - // TODO Should we enforce receiver != 0? old poly contracts don't - // Encode the message payload bytes memory payload = abi.encode(WITHDRAW_SIG, rootToken, msg.sender, receiver, amount); // Send the message to the bridge adaptor and up to root chain - bridgeAdaptor.sendMessage{value: feeAmount}(payload, msg.sender); - if (address(childToken) == NATIVE_IMX) { + if (childTokenAddr == NATIVE_IMX) { emit ChildChainNativeIMXWithdraw(rootToken, msg.sender, receiver, amount); + } else if (childTokenAddr == wIMXToken) { + emit ChildChainWrappedIMXWithdraw(rootToken, msg.sender, receiver, amount); } else { - emit ChildChainERC20Withdraw(rootToken, address(childToken), msg.sender, receiver, amount); + emit ChildChainERC20Withdraw(rootToken, childTokenAddr, msg.sender, receiver, amount); } } - function _withdrawWIMX(address receiver, uint256 amount) private { - if (amount == 0) { - revert ZeroAmount(); - } - - uint256 expectedBalance = address(this).balance + amount; - - IWIMX wIMX = IWIMX(wIMXToken); - if (!wIMX.transferFrom(msg.sender, address(this), amount)) { - revert TransferWIMXFailed(); - } - wIMX.withdraw(amount); - - if (address(this).balance != expectedBalance) { - revert BalanceInvariantCheckFailed(address(this).balance, expectedBalance); - } - - bytes memory payload = abi.encode(WITHDRAW_SIG, rootIMXToken, msg.sender, receiver, amount); - - bridgeAdaptor.sendMessage{value: msg.value}(payload, msg.sender); - - emit ChildChainWrappedIMXWithdraw(rootIMXToken, msg.sender, receiver, amount); - } - function _mapToken(bytes calldata data) private { (, address rootToken, string memory name, string memory symbol, uint8 decimals) = abi.decode(data, (bytes32, address, string, string, uint8)); @@ -325,11 +316,4 @@ contract ChildERC20Bridge is emit IMXDeposit(address(rootToken), sender, receiver, amount); } } - - function updateBridgeAdaptor(address newBridgeAdaptor) external override onlyOwner { - if (newBridgeAdaptor == address(0)) { - revert ZeroAddress(); - } - bridgeAdaptor = IChildERC20BridgeAdaptor(newBridgeAdaptor); - } } From 32c2d3cbd380ea8c43c07aeb0bfc85599175b506 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 15:54:39 +1000 Subject: [PATCH 44/49] Update --- src/child/ChildERC20Bridge.sol | 18 ++ src/interfaces/child/IChildERC20Bridge.sol | 3 + .../ChildERC20BridgeWithdrawETH.t.sol | 130 +++++++++++++++ .../ChildERC20BridgeWithdrawETHTo.t.sol | 156 ++++++++++++++++++ ...ol => ChildERC20BridgeWithdrawIMXTo.t.sol} | 0 5 files changed, 307 insertions(+) create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol create mode 100644 test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol rename test/unit/child/withdrawals/{ChildERC20BridgeWithdrawToIMX.t.sol => ChildERC20BridgeWithdrawIMXTo.t.sol} (100%) diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index a07294fe2..77f6535b3 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -172,6 +172,14 @@ contract ChildERC20Bridge is _withdraw(wIMXToken, receiver, amount); } + function withdrawETH(uint256 amount) external payable { + _withdraw(childETHToken, msg.sender, amount); + } + + function withdrawETHTo(address receiver, uint256 amount) external payable { + _withdraw(childETHToken, receiver, amount); + } + function _withdraw(address childTokenAddr, address receiver, uint256 amount) private { if (childTokenAddr == address(0)) { revert ZeroAddress(); @@ -206,6 +214,14 @@ contract ChildERC20Bridge is } rootToken = rootIMXToken; + } else if (childTokenAddr == childETHToken) { + // Wrapped ETH. + IChildERC20 childToken = IChildERC20(childTokenAddr); + rootToken = NATIVE_ETH; + + if (!childToken.burn(msg.sender, amount)) { + revert BurnFailed(); + } } else { // Other ERC20 Tokens IChildERC20 childToken = IChildERC20(childTokenAddr); @@ -244,6 +260,8 @@ contract ChildERC20Bridge is emit ChildChainNativeIMXWithdraw(rootToken, msg.sender, receiver, amount); } else if (childTokenAddr == wIMXToken) { emit ChildChainWrappedIMXWithdraw(rootToken, msg.sender, receiver, amount); + } else if (childTokenAddr == childETHToken) { + emit ChildChainEthWithdraw(msg.sender, receiver, amount); } else { emit ChildChainERC20Withdraw(rootToken, childTokenAddr, msg.sender, receiver, amount); } diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index f52271207..3c3b169ea 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -38,6 +38,9 @@ interface IChildERC20BridgeEvents { event ChildChainWrappedIMXWithdraw( address indexed rootToken, address depositor, address indexed receiver, uint256 amount ); + event ChildChainEthWithdraw( + address depositor, address indexed receiver, uint256 amount + ); event ChildChainERC20Deposit( address indexed rootToken, diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol new file mode 100644 index 000000000..eceed16ff --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); + ChildERC20 public childTokenTemplate; + ChildERC20 public rootToken; + ChildERC20 public childToken; + ChildERC20 public childETHToken; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + + function setUp() public { + rootToken = new ChildERC20(); + rootToken.initialize(address(456), "Test", "TST", 18); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + WIMX_TOKEN_ADDRESS + ); + + bytes memory mapTokenData = + abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.prank(address(mockAdaptor)); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, mapTokenData); + + childToken = ChildERC20(childBridge.rootTokenToChildToken(address(rootToken))); + vm.prank(address(childBridge)); + childToken.mint(address(this), 1000000 ether); + childToken.approve(address(childBridge), 1000000 ether); + + childETHToken = ChildERC20(childBridge.childETHToken()); + vm.prank(address(childBridge)); + childETHToken.mint(address(this), 100 ether); + } + + function test_RevertsIf_WithdrawWEthCalledWithInsufficientFund() public { + uint256 withdrawAmount = 101 ether; + uint256 withdrawFee = 300; + + vm.expectRevert(bytes("ERC20: burn amount exceeds balance")); + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_RevertsIf_ZeroAmountIsProvided() public { + uint256 withdrawFee = 300; + + vm.expectRevert(ZeroAmount.selector); + childBridge.withdrawETH{value: withdrawFee}(0); + } + + function test_WithdrawETH_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawETH_EmitsChildChainEthWithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainEthWithdraw(address(this), address(this), withdrawAmount); + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawETH_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = childETHToken.balanceOf(address(this)); + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + + uint256 postBal = childETHToken.balanceOf(address(this)); + assertEq(postBal, preBal - withdrawAmount, "Balance not reduced"); + } + + function test_WithdrawETH_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + + uint256 postBal = address(this).balance; + assertEq(postBal, preBal - withdrawFee, "Fee not paid"); + } +} \ No newline at end of file diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol new file mode 100644 index 000000000..bae53c83f --- /dev/null +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ChildERC20Bridge, + IChildERC20BridgeEvents, + IERC20Metadata, + IChildERC20BridgeErrors +} from "../../../../src/child/ChildERC20Bridge.sol"; +import {IChildERC20} from "../../../../src/interfaces/child/IChildERC20.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, IChildERC20BridgeErrors, Utils { + address constant ROOT_BRIDGE = address(3); + string public ROOT_BRIDGE_ADAPTOR = Strings.toHexString(address(4)); + string constant ROOT_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + address constant WIMX_TOKEN_ADDRESS = address(0xabc); + ChildERC20 public childTokenTemplate; + ChildERC20 public rootToken; + ChildERC20 public childToken; + ChildERC20 public childETHToken; + ChildERC20Bridge public childBridge; + MockAdaptor public mockAdaptor; + + function setUp() public { + rootToken = new ChildERC20(); + rootToken.initialize(address(456), "Test", "TST", 18); + + childTokenTemplate = new ChildERC20(); + childTokenTemplate.initialize(address(123), "Test", "TST", 18); + + mockAdaptor = new MockAdaptor(); + + childBridge = new ChildERC20Bridge(); + childBridge.initialize( + address(mockAdaptor), + ROOT_BRIDGE_ADAPTOR, + address(childTokenTemplate), + ROOT_CHAIN_NAME, + ROOT_IMX_TOKEN, + WIMX_TOKEN_ADDRESS + ); + + bytes memory mapTokenData = + abi.encode(MAP_TOKEN_SIG, rootToken, rootToken.name(), rootToken.symbol(), rootToken.decimals()); + + vm.prank(address(mockAdaptor)); + childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, mapTokenData); + + childToken = ChildERC20(childBridge.rootTokenToChildToken(address(rootToken))); + vm.prank(address(childBridge)); + childToken.mint(address(this), 1000000 ether); + childToken.approve(address(childBridge), 1000000 ether); + + childETHToken = ChildERC20(childBridge.childETHToken()); + vm.prank(address(childBridge)); + childETHToken.mint(address(this), 100 ether); + } + + function test_RevertsIf_WithdrawWEthToCalledWithInsufficientFund() public { + uint256 withdrawAmount = 101 ether; + uint256 withdrawFee = 300; + + vm.expectRevert(bytes("ERC20: burn amount exceeds balance")); + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_RevertsIf_ZeroAmountIsProvided() public { + uint256 withdrawFee = 300; + + vm.expectRevert(ZeroAmount.selector); + childBridge.withdrawETHTo{value: withdrawFee}(address(this), 0); + } + + function test_WithdrawETHTo_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawETHToWithDifferentAccount_CallsBridgeAdaptor() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + + vm.expectCall( + address(mockAdaptor), + withdrawFee, + abi.encodeWithSelector(mockAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + childBridge.withdrawETHTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawETHTo_EmitsChildChainEthWithdrawEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainEthWithdraw(address(this), address(this), withdrawAmount); + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawETHToWithDifferentAccount_EmitsChildChainEthWithdrawEvent() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + vm.expectEmit(address(childBridge)); + emit ChildChainEthWithdraw(address(this), receiver, withdrawAmount); + childBridge.withdrawETHTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawETHTo_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = childETHToken.balanceOf(address(this)); + + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + + uint256 postBal = childETHToken.balanceOf(address(this)); + assertEq(postBal, preBal - withdrawAmount, "Balance not reduced"); + } + + function test_WithdrawETHTo_PaysFee() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + + uint256 postBal = address(this).balance; + assertEq(postBal, preBal - withdrawFee, "Fee not paid"); + } +} \ No newline at end of file diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol similarity index 100% rename from test/unit/child/withdrawals/ChildERC20BridgeWithdrawToIMX.t.sol rename to test/unit/child/withdrawals/ChildERC20BridgeWithdrawIMXTo.t.sol From e07c82e10875b677255c369d0b83b4500e9e453c Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 16:06:17 +1000 Subject: [PATCH 45/49] Fix typo --- test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol | 2 +- test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol index eceed16ff..775677e4a 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -65,7 +65,7 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I childETHToken.mint(address(this), 100 ether); } - function test_RevertsIf_WithdrawWEthCalledWithInsufficientFund() public { + function test_RevertsIf_WithdrawEthCalledWithInsufficientFund() public { uint256 withdrawAmount = 101 ether; uint256 withdrawFee = 300; diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol index bae53c83f..a719fe1ad 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -65,7 +65,7 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, childETHToken.mint(address(this), 100 ether); } - function test_RevertsIf_WithdrawWEthToCalledWithInsufficientFund() public { + function test_RevertsIf_WithdrawEthToCalledWithInsufficientFund() public { uint256 withdrawAmount = 101 ether; uint256 withdrawFee = 300; From 52475d793bfbe694ad4a4c0ab9c30b49b2aca1ae Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 16:15:21 +1000 Subject: [PATCH 46/49] Add integration test --- .../ChildAxelarBridgeWithdrawETH.t.sol | 147 +++++++++++++++ .../ChildAxelarBridgeWithdrawETHTo.t.sol | 175 ++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol create mode 100644 test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol new file mode 100644 index 000000000..473995d78 --- /dev/null +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract ChildERC20BridgeWithdrawETHIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(555555); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + address constant WRAPPED_IMX = address(0xabc); + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + WIMX public wIMXToken; + ChildERC20 public childETHToken; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + wIMXToken = WIMX(payable(WRAPPED_IMX)); + Address.sendValue(payable(wIMXToken), 100 ether); + + childETHToken = ChildERC20(childBridge.childETHToken()); + vm.prank(address(childBridge)); + childETHToken.mint(address(this), 100 ether); + } + + function test_WithdrawETH_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawETH_CallsAxelarGateway() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawETH_CallsGasService() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + vm.expectCall( + address(axelarGasService), + withdrawFee, + abi.encodeWithSelector( + axelarGasService.payNativeGasForContractCall.selector, + address(axelarAdaptor), + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload, + address(this) + ) + ); + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawETH_EmitsAxelarMessageSentEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + } + + function test_WithdrawETH_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + uint256 preTokenBal = childETHToken.balanceOf(address(this)); + uint256 preGasBal = address(axelarGasService).balance; + + childBridge.withdrawETH{value: withdrawFee}(withdrawAmount); + + uint256 postBal = address(this).balance; + uint256 postTokenBal = childETHToken.balanceOf(address(this)); + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawFee, "Balance not reduced"); + assertEq(postTokenBal, preTokenBal - withdrawAmount); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); + } +} \ No newline at end of file diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol new file mode 100644 index 000000000..f79e6cdd4 --- /dev/null +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {ChildERC20Bridge, IChildERC20BridgeEvents} from "../../../../src/child/ChildERC20Bridge.sol"; +import { + ChildAxelarBridgeAdaptor, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors +} from "../../../../src/child/ChildAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; +import {WIMX} from "../../../../src/child/WIMX.sol"; +import {ChildERC20} from "../../../../src/child/ChildERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract ChildERC20BridgeWithdrawETHToIntegrationTest is + Test, + IChildERC20BridgeEvents, + IChildAxelarBridgeAdaptorEvents, + IChildAxelarBridgeAdaptorErrors, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "test"; + address constant ROOT_IMX_TOKEN = address(555555); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + address constant WRAPPED_IMX = address(0xabc); + + ChildERC20Bridge public childBridge; + ChildAxelarBridgeAdaptor public axelarAdaptor; + address public rootToken; + address public rootImxToken; + ChildERC20 public childTokenTemplate; + MockAxelarGasService public axelarGasService; + MockAxelarGateway public mockAxelarGateway; + WIMX public wIMXToken; + ChildERC20 public childETHToken; + + function setUp() public { + (childBridge, axelarAdaptor, rootToken, rootImxToken, childTokenTemplate, axelarGasService, mockAxelarGateway) = + childIntegrationSetup(); + wIMXToken = WIMX(payable(WRAPPED_IMX)); + Address.sendValue(payable(wIMXToken), 100 ether); + + childETHToken = ChildERC20(childBridge.childETHToken()); + vm.prank(address(childBridge)); + childETHToken.mint(address(this), 100 ether); + } + + function test_WithdrawETHTo_CallsBridgeAdaptor() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawETHToWithDifferentAccount_CallsBridgeAdaptor() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + vm.expectCall( + address(axelarAdaptor), + withdrawFee, + abi.encodeWithSelector(axelarAdaptor.sendMessage.selector, predictedPayload, address(this)) + ); + + childBridge.withdrawETHTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawETHTo_CallsAxelarGateway() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawETHToWithDifferentAccount_CallsAxelarGateway() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + vm.expectCall( + address(mockAxelarGateway), + 0, + abi.encodeWithSelector( + mockAxelarGateway.callContract.selector, + childBridge.rootChain(), + childBridge.rootERC20BridgeAdaptor(), + predictedPayload + ) + ); + + childBridge.withdrawETHTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawETHTo_EmitsAxelarMessageSentEvent() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + } + + function test_WithdrawETHToWithDifferentAccount_EmitsAxelarMessageSentEvent() public { + address receiver = address(0xabcd); + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + bytes memory predictedPayload = + abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + + vm.expectEmit(address(axelarAdaptor)); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + + childBridge.withdrawETHTo{value: withdrawFee}(receiver, withdrawAmount); + } + + function test_WithdrawETHTo_ReducesBalance() public { + uint256 withdrawFee = 300; + uint256 withdrawAmount = 7 ether; + + uint256 preBal = address(this).balance; + uint256 preTokenBal = childETHToken.balanceOf(address(this)); + uint256 preGasBal = address(axelarGasService).balance; + + childBridge.withdrawETHTo{value: withdrawFee}(address(this), withdrawAmount); + + uint256 postBal = address(this).balance; + uint256 postTokenBal = childETHToken.balanceOf(address(this)); + uint256 postGasBal = address(axelarGasService).balance; + + assertEq(postBal, preBal - withdrawFee, "Balance not reduced"); + assertEq(postTokenBal, preTokenBal - withdrawAmount); + assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); + } +} \ No newline at end of file From fc10c6469b3e0cd9c2262b11da0461f0223bb746 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 16:15:50 +1000 Subject: [PATCH 47/49] Fmt --- src/interfaces/child/IChildERC20Bridge.sol | 4 +--- .../withdrawals/ChildAxelarBridgeWithdrawETH.t.sol | 2 +- .../withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol | 11 ++++------- .../withdrawals/ChildERC20BridgeWithdrawETH.t.sol | 4 ++-- .../withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol | 7 +++---- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 3c3b169ea..5eb213607 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -38,9 +38,7 @@ interface IChildERC20BridgeEvents { event ChildChainWrappedIMXWithdraw( address indexed rootToken, address depositor, address indexed receiver, uint256 amount ); - event ChildChainEthWithdraw( - address depositor, address indexed receiver, uint256 amount - ); + event ChildChainEthWithdraw(address depositor, address indexed receiver, uint256 amount); event ChildChainERC20Deposit( address indexed rootToken, diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol index 473995d78..1ce03cad5 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETH.t.sol @@ -144,4 +144,4 @@ contract ChildERC20BridgeWithdrawETHIntegrationTest is assertEq(postTokenBal, preTokenBal - withdrawAmount); assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); } -} \ No newline at end of file +} diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol index f79e6cdd4..9b71e9f30 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawETHTo.t.sol @@ -75,8 +75,7 @@ contract ChildERC20BridgeWithdrawETHToIntegrationTest is uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; - bytes memory predictedPayload = - abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); vm.expectCall( address(axelarAdaptor), withdrawFee, @@ -111,8 +110,7 @@ contract ChildERC20BridgeWithdrawETHToIntegrationTest is uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; - bytes memory predictedPayload = - abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); vm.expectCall( address(mockAxelarGateway), 0, @@ -145,8 +143,7 @@ contract ChildERC20BridgeWithdrawETHToIntegrationTest is uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; - bytes memory predictedPayload = - abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); vm.expectEmit(address(axelarAdaptor)); emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); @@ -172,4 +169,4 @@ contract ChildERC20BridgeWithdrawETHToIntegrationTest is assertEq(postTokenBal, preTokenBal - withdrawAmount); assertEq(postGasBal, preGasBal + withdrawFee, "Gas service not getting paid"); } -} \ No newline at end of file +} diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol index 775677e4a..2a861dbe6 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETH.t.sol @@ -59,7 +59,7 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I vm.prank(address(childBridge)); childToken.mint(address(this), 1000000 ether); childToken.approve(address(childBridge), 1000000 ether); - + childETHToken = ChildERC20(childBridge.childETHToken()); vm.prank(address(childBridge)); childETHToken.mint(address(this), 100 ether); @@ -127,4 +127,4 @@ contract ChildERC20BridgeWithdrawETHUnitTest is Test, IChildERC20BridgeEvents, I uint256 postBal = address(this).balance; assertEq(postBal, preBal - withdrawFee, "Fee not paid"); } -} \ No newline at end of file +} diff --git a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol index a719fe1ad..064a37a54 100644 --- a/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol +++ b/test/unit/child/withdrawals/ChildERC20BridgeWithdrawETHTo.t.sol @@ -59,7 +59,7 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, vm.prank(address(childBridge)); childToken.mint(address(this), 1000000 ether); childToken.approve(address(childBridge), 1000000 ether); - + childETHToken = ChildERC20(childBridge.childETHToken()); vm.prank(address(childBridge)); childETHToken.mint(address(this), 100 ether); @@ -100,8 +100,7 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; - bytes memory predictedPayload = - abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); vm.expectCall( address(mockAdaptor), @@ -153,4 +152,4 @@ contract ChildERC20BridgeWithdrawETHToUnitTest is Test, IChildERC20BridgeEvents, uint256 postBal = address(this).balance; assertEq(postBal, preBal - withdrawFee, "Fee not paid"); } -} \ No newline at end of file +} From 737c9c339c8e4ddb3f043b8ea2930d9b8a9530b7 Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 16:29:53 +1000 Subject: [PATCH 48/49] Add root bridge unit test --- src/root/RootERC20Bridge.sol | 2 + .../withdrawals/RootERC20BridgeWithdraw.t.sol | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index b8d420894..bdfd21609 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -358,6 +358,8 @@ contract RootERC20Bridge is address childToken; if (address(rootToken) == rootIMXToken) { childToken = NATIVE_IMX; + } else if (address(rootToken) == NATIVE_ETH) { + childToken = childETHToken; } else { childToken = rootTokenToChildToken[rootToken]; if (childToken == address(0)) { diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index abb233f9c..8cb1ae552 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -11,6 +11,7 @@ import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootERC20BridgeErrors, Utils { address constant CHILD_BRIDGE = address(3); @@ -20,6 +21,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE address constant UnmappedToken = address(0xbbb); address constant IMX_TOKEN = address(0xccc); address constant WRAPPED_ETH = address(0xddd); + address public constant NATIVE_ETH = address(0xeee); address public constant NATIVE_IMX = address(0xfff); uint256 constant mapTokenFee = 300; uint256 constant withdrawAmount = 0.5 ether; @@ -32,6 +34,8 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE MockAxelarGateway public mockAxelarGateway; MockAxelarGasService public axelarGasService; + receive() external payable {} + function setUp() public { token = new ERC20PresetMinterPauser("Test", "TST"); token.mint(address(this), 100 ether); @@ -144,6 +148,21 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE ); } + function test_onMessageReceive_TransfersETH() public { + // Give bridge some ETH + Address.sendValue(payable(rootBridge), 100 ether); + + uint256 thisPreBal = address(this).balance; + uint256 bridgePreBal = address(rootBridge).balance; + + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(address(this).balance, thisPreBal + withdrawAmount, "ETH not transferred to receiver"); + assertEq(address(rootBridge).balance, bridgePreBal - withdrawAmount, "ETH not transferred from bridge"); + } + function test_onMessageReceive_TransfersTokens_DifferentReceiver() public { address receiver = address(123456); // Need to first map the token. @@ -184,6 +203,22 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE ); } + function test_onMessageReceive_TransfersETH_DifferentReceiver() public { + address receiver = address(123456); + // Give bridge some ETH + Address.sendValue(payable(rootBridge), 100 ether); + + uint256 receiverPreBal = address(receiver).balance; + uint256 bridgePreBal = address(rootBridge).balance; + + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(address(receiver).balance, receiverPreBal + withdrawAmount, "ETH not transferred to receiver"); + assertEq(address(rootBridge).balance, bridgePreBal - withdrawAmount, "ETH not transferred from bridge"); + } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent() public { // Need to first map the token. rootBridge.mapToken(token); @@ -214,6 +249,19 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEventForETH() public { + // Give bridge some ETH + Address.sendValue(payable(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw( + NATIVE_ETH, address(rootBridge.childETHToken()), address(this), address(this), withdrawAmount + ); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { address receiver = address(123456); // Need to first map the token. @@ -241,4 +289,18 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEventForETH_DifferentReceiver() public { + address receiver = address(123456); + // Give bridge some ETH + Address.sendValue(payable(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw( + NATIVE_ETH, address(rootBridge.childETHToken()), address(this), receiver, withdrawAmount + ); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } } From 2e83a3d6e5e9671cd5851a87f1e36b001e7c660a Mon Sep 17 00:00:00 2001 From: wcgcyx Date: Wed, 8 Nov 2023 16:35:48 +1000 Subject: [PATCH 49/49] Update RootERC20BridgeWithdraw.t.sol --- .../RootERC20BridgeWithdraw.t.sol | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index dff08e970..40ed22cdb 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -18,6 +18,7 @@ import { } from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; import {Utils} from "../../../utils.t.sol"; import {WETH} from "../../../../src/test/root/WETH.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; contract RootERC20BridgeWithdrawIntegrationTest is Test, @@ -44,6 +45,8 @@ contract RootERC20BridgeWithdrawIntegrationTest is MockAxelarGateway public mockAxelarGateway; MockAxelarGasService public axelarGasService; + receive() external payable {} + function setUp() public { deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); @@ -61,6 +64,8 @@ contract RootERC20BridgeWithdrawIntegrationTest is // And give the bridge some tokens token.transfer(address(rootBridge), 100 ether); imxToken.transfer(address(rootBridge), 100 ether); + // Give bridge some ETH + Address.sendValue(payable(rootBridge), 100 ether); } function test_RevertsIf_WithdrawWithInvalidSourceChain() public { @@ -139,6 +144,24 @@ contract RootERC20BridgeWithdrawIntegrationTest is assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } + function test_withdrawETH_TransfersETH() public { + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 thisPreBal = address(this).balance; + uint256 bridgePreBal = address(rootBridge).balance; + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 thisPostBal = address(this).balance; + uint256 bridgePostBal = address(rootBridge).balance; + + assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + function test_withdraw_TransfersTokens_DifferentReceiver() public { address receiver = address(987654321); bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); @@ -177,6 +200,25 @@ contract RootERC20BridgeWithdrawIntegrationTest is assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } + function test_withdrawETH_TransfersETH_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 receiverPreBal = address(receiver).balance; + uint256 bridgePreBal = address(rootBridge).balance; + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 receiverPostBal = address(receiver).balance; + uint256 bridgePostBal = address(rootBridge).balance; + + assertEq(receiverPostBal, receiverPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + function test_withdraw_EmitsRootChainERC20WithdrawEvent() public { bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); @@ -205,6 +247,19 @@ contract RootERC20BridgeWithdrawIntegrationTest is axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } + function test_withdrawETH_EmitsRootChainERC20WithdrawEvent() public { + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + NATIVE_ETH, address(rootBridge.childETHToken()), address(this), address(this), withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + function test_withdraw_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { address receiver = address(987654321); bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); @@ -230,4 +285,18 @@ contract RootERC20BridgeWithdrawIntegrationTest is emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), receiver, withdrawAmount); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } + + function test_withdrawETH_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, NATIVE_ETH, address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + NATIVE_ETH, address(rootBridge.childETHToken()), address(this), receiver, withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } }