From 13e66f1fefa8f2951bbf66522a04836ef11900b9 Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Wed, 1 Nov 2023 11:03:23 +1100 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 ff6b92de888a24ab7907fe8bbc33e83498b6a37e Mon Sep 17 00:00:00 2001 From: Benjamin Patch Date: Fri, 3 Nov 2023 15:50:59 +1100 Subject: [PATCH 9/9] 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"); - // } }