diff --git a/script/DeployRootContracts.s.sol b/script/DeployRootContracts.s.sol index fee8e860c..0ef5c52bf 100644 --- a/script/DeployRootContracts.s.sol +++ b/script/DeployRootContracts.s.sol @@ -21,6 +21,7 @@ contract DeployRootContracts is Script { uint256 rootPrivateKey = vm.envUint("ROOT_PRIVATE_KEY"); string memory rootRpcUrl = vm.envString("ROOT_RPC_URL"); string memory deployEnvironment = vm.envString("ENVIRONMENT"); + address rootGateway = vm.envAddress("ROOT_GATEWAY_ADDRESS"); /** * DEPLOY ROOT CHAIN CONTRACTS @@ -36,7 +37,7 @@ contract DeployRootContracts is Script { RootERC20Bridge rootERC20BridgeImplementation = new RootERC20Bridge(); rootERC20BridgeImplementation.initialize( - address(1), address(1), "filler", address(1), address(1), address(1), 1 + address(1), address(1), "filler", address(1), address(1), address(1), "filler_child_name", 1 ); TransparentUpgradeableProxy rootERC20BridgeProxy = new TransparentUpgradeableProxy( address(rootERC20BridgeImplementation), @@ -44,10 +45,9 @@ contract DeployRootContracts is Script { "" ); - RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor(); - rootBridgeAdaptorImplementation.initialize( - address(rootERC20BridgeImplementation), "Filler", address(1), address(1) - ); + RootAxelarBridgeAdaptor rootBridgeAdaptorImplementation = new RootAxelarBridgeAdaptor(rootGateway); + rootBridgeAdaptorImplementation.initialize(address(rootERC20BridgeImplementation), "Filler", address(1)); + TransparentUpgradeableProxy rootBridgeAdaptorProxy = new TransparentUpgradeableProxy( address(rootBridgeAdaptorImplementation), address(proxyAdmin), diff --git a/script/InitializeRootContracts.s.sol b/script/InitializeRootContracts.s.sol index 8ea85a593..625345d09 100644 --- a/script/InitializeRootContracts.s.sol +++ b/script/InitializeRootContracts.s.sol @@ -24,7 +24,6 @@ struct InitializeRootContractsParams { address rootIMXToken; address rootWETHToken; string childChainName; - address rootGateway; address rootGasService; uint256 initialIMXCumulativeDepositLimit; } @@ -42,7 +41,6 @@ contract InitializeRootContracts is Script { rootIMXToken: vm.envAddress("ROOT_IMX_ADDRESS"), rootWETHToken: vm.envAddress("ROOT_WETH_ADDRESS"), childChainName: vm.envString("CHILD_CHAIN_NAME"), - rootGateway: vm.envAddress("ROOT_GATEWAY_ADDRESS"), rootGasService: vm.envAddress("ROOT_GAS_SERVICE_ADDRESS"), initialIMXCumulativeDepositLimit: vm.envUint("INITIAL_IMX_CUMULATIVE_DEPOSIT_LIMIT") }); @@ -63,13 +61,13 @@ contract InitializeRootContracts is Script { params.rootChainChildTokenTemplate, params.rootIMXToken, params.rootWETHToken, + params.childChainName, params.initialIMXCumulativeDepositLimit ); params.rootBridgeAdaptor.initialize( address(params.rootERC20Bridge), // root bridge params.childChainName, // child chain name - params.rootGateway, // axelar gateway params.rootGasService // axelar gas service ); diff --git a/src/child/ChildAxelarBridgeAdaptor.sol b/src/child/ChildAxelarBridgeAdaptor.sol index 144f6cac3..5e725257e 100644 --- a/src/child/ChildAxelarBridgeAdaptor.sol +++ b/src/child/ChildAxelarBridgeAdaptor.sol @@ -22,7 +22,6 @@ contract ChildAxelarBridgeAdaptor is /// @notice Address of bridge to relay messages to. IChildERC20Bridge public childBridge; IAxelarGasService public gasService; - string public rootBridgeAdaptor; string public rootChain; constructor(address _gateway) AxelarExecutable(_gateway) {} @@ -39,7 +38,6 @@ contract ChildAxelarBridgeAdaptor is childBridge = IChildERC20Bridge(_childBridge); rootChain = _rootChain; gasService = IAxelarGasService(_gasService); - rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); } /** @@ -54,7 +52,7 @@ contract ChildAxelarBridgeAdaptor is } // Load from storage. - string memory _rootBridgeAdaptor = rootBridgeAdaptor; + string memory _rootBridgeAdaptor = childBridge.rootERC20BridgeAdaptor(); string memory _rootChain = rootChain; gasService.payNativeGasForContractCall{value: msg.value}( @@ -62,7 +60,7 @@ contract ChildAxelarBridgeAdaptor is ); gateway.callContract(_rootChain, _rootBridgeAdaptor, payload); - emit AxelarMessage(_rootChain, _rootBridgeAdaptor, payload); + emit AxelarMessageSent(_rootChain, _rootBridgeAdaptor, payload); } /** @@ -73,6 +71,7 @@ contract ChildAxelarBridgeAdaptor is internal override { + emit AdaptorExecute(sourceChain_, sourceAddress_, payload_); childBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_); } } diff --git a/src/child/ChildERC20Bridge.sol b/src/child/ChildERC20Bridge.sol index 9a9e6212e..d4eae07a4 100644 --- a/src/child/ChildERC20Bridge.sol +++ b/src/child/ChildERC20Bridge.sol @@ -109,12 +109,13 @@ contract ChildERC20Bridge is if (!Strings.equal(messageSourceChain, rootChain)) { revert InvalidSourceChain(); } - if (!Strings.equal(sourceAddress, rootERC20BridgeAdaptor)) { revert InvalidSourceAddress(); } - if (data.length == 0) { - revert InvalidData(); + if (data.length <= 32) { + // Data must always be greater than 32. + // 32 bytes for the signature, and at least some information for the payload + revert InvalidData("Data too short"); } if (bytes32(data[:32]) == MAP_TOKEN_SIG) { @@ -122,7 +123,7 @@ contract ChildERC20Bridge is } else if (bytes32(data[:32]) == DEPOSIT_SIG) { _deposit(data[32:]); } else { - revert InvalidData(); + revert InvalidData("Unsupported action signature"); } } diff --git a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol index f7cd63330..cca42edcf 100644 --- a/src/interfaces/child/IChildAxelarBridgeAdaptor.sol +++ b/src/interfaces/child/IChildAxelarBridgeAdaptor.sol @@ -12,5 +12,7 @@ interface IChildAxelarBridgeAdaptorErrors { interface IChildAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the root chain. - event AxelarMessage(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); + event AxelarMessageSent(string indexed rootChain, string indexed rootBridgeAdaptor, bytes indexed payload); + /// @notice Emitted when an Axelar message is received from the root chain. + event AdaptorExecute(string sourceChain, string sourceAddress_, bytes payload_); } diff --git a/src/interfaces/child/IChildERC20Bridge.sol b/src/interfaces/child/IChildERC20Bridge.sol index 087daa893..aaf4195d7 100644 --- a/src/interfaces/child/IChildERC20Bridge.sol +++ b/src/interfaces/child/IChildERC20Bridge.sol @@ -73,7 +73,7 @@ interface IChildERC20BridgeErrors { /// @notice Error when a message is given to the bridge from an address not the designated bridge adaptor. error NotBridgeAdaptor(); /// @notice Error when the message's payload is not valid. - error InvalidData(); + error InvalidData(string reason); /// @notice Error when the message's source chain is not valid. error InvalidSourceChain(); /// @notice Error when the source chain's message sender is not a recognised address. diff --git a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol index d134c9410..e1cddac17 100644 --- a/src/interfaces/root/IRootAxelarBridgeAdaptor.sol +++ b/src/interfaces/root/IRootAxelarBridgeAdaptor.sol @@ -14,5 +14,7 @@ interface IRootAxelarBridgeAdaptorErrors { interface IRootAxelarBridgeAdaptorEvents { /// @notice Emitted when an Axelar message is sent to the child chain. - event AxelarMessage(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); + event AxelarMessageSent(string indexed childChain, string indexed childBridgeAdaptor, bytes indexed payload); + /// @notice Emitted when an Axelar message is received from the child chain. + event AdaptorExecute(string sourceChain, string sourceAddress_, bytes payload_); } diff --git a/src/interfaces/root/IRootERC20Bridge.sol b/src/interfaces/root/IRootERC20Bridge.sol index 1e3741fc0..51d235d05 100644 --- a/src/interfaces/root/IRootERC20Bridge.sol +++ b/src/interfaces/root/IRootERC20Bridge.sol @@ -5,6 +5,14 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER interface IRootERC20Bridge { function childBridgeAdaptor() external view returns (string memory); + /** + * @notice Receives a bridge message from child chain, parsing the message type then executing. + * @param sourceChain The chain the message originated from. + * @param sourceAddress The address the message originated from. + * @param data The data payload of the message. + */ + function onMessageReceive(string calldata sourceChain, string calldata sourceAddress, bytes calldata data) + external; /** * @notice Initiate sending a mapToken message to the child chain. @@ -64,6 +72,14 @@ interface IRootERC20BridgeEvents { address indexed receiver, uint256 amount ); + + event RootChainERC20Withdraw( + address indexed rootToken, + address indexed childToken, + address withdrawer, + address indexed receiver, + uint256 amount + ); } interface IRootERC20BridgeErrors { @@ -73,6 +89,8 @@ interface IRootERC20BridgeErrors { error ZeroAmount(); /// @notice Error when a zero address is given when not valid. error ZeroAddress(); + /// @notice Error when the child chain name is invalid. + error InvalidChildChain(); /// @notice Error when a token is already mapped. error AlreadyMapped(); /// @notice Error when a token is not mapped when it should be. @@ -87,6 +105,14 @@ interface IRootERC20BridgeErrors { error BalanceInvariantCheckFailed(uint256 actualBalance, uint256 expectedBalance); /// @notice Error when the given child chain bridge adaptor is invalid. error InvalidChildERC20BridgeAdaptor(); + /// @notice Error when a message received has invalid data. + error InvalidData(string reason); + /// @notice Error when a message received has invalid source address. + error InvalidSourceAddress(); + /// @notice Error when a message received has invalid source chain. + error InvalidSourceChain(); + /// @notice Error when caller is not the root bridge adaptor but should be. + error NotBridgeAdaptor(); /// @notice Error when the total IMX deposit limit is exceeded error ImxDepositLimitExceeded(); /// @notice Error when the IMX deposit limit is set below the amount of IMX already deposited diff --git a/src/root/RootAxelarBridgeAdaptor.sol b/src/root/RootAxelarBridgeAdaptor.sol index 8c769959d..e603dadba 100644 --- a/src/root/RootAxelarBridgeAdaptor.sol +++ b/src/root/RootAxelarBridgeAdaptor.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache 2.0 pragma solidity ^0.8.21; +import {AxelarExecutable} from "@axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -20,6 +21,7 @@ import {IRootERC20Bridge} from "../interfaces/root/IRootERC20Bridge.sol"; * @notice RootAxelarBridgeAdaptor is a bridge adaptor that allows the RootERC20Bridge to communicate with the Axelar Gateway. */ contract RootAxelarBridgeAdaptor is + AxelarExecutable, Initializable, IRootERC20BridgeAdaptor, IRootAxelarBridgeAdaptorEvents, @@ -27,34 +29,30 @@ contract RootAxelarBridgeAdaptor is { using SafeERC20 for IERC20Metadata; - address public rootBridge; + IRootERC20Bridge public rootBridge; string public childBridgeAdaptor; string public childChain; - IAxelarGateway public axelarGateway; IAxelarGasService public gasService; mapping(uint256 => string) public chainIdToChainName; + constructor(address _gateway) AxelarExecutable(_gateway) {} + /** * @notice Initilization function for RootAxelarBridgeAdaptor. * @param _rootBridge Address of root bridge contract. * @param _childChain Name of child chain. - * @param _axelarGateway Address of Axelar Gateway contract. * @param _gasService Address of Axelar Gas Service contract. */ - function initialize(address _rootBridge, string memory _childChain, address _axelarGateway, address _gasService) - public - initializer - { - if (_rootBridge == address(0) || _axelarGateway == address(0) || _gasService == address(0)) { + function initialize(address _rootBridge, string memory _childChain, address _gasService) public initializer { + if (_rootBridge == address(0) || _gasService == address(0)) { revert ZeroAddresses(); } if (bytes(_childChain).length == 0) { revert InvalidChildChain(); } - rootBridge = _rootBridge; + rootBridge = IRootERC20Bridge(_rootBridge); childChain = _childChain; - axelarGateway = IAxelarGateway(_axelarGateway); gasService = IAxelarGasService(_gasService); } @@ -66,7 +64,7 @@ contract RootAxelarBridgeAdaptor is if (msg.value == 0) { revert NoGas(); } - if (msg.sender != rootBridge) { + if (msg.sender != address(rootBridge)) { revert CallerNotBridge(); } @@ -79,7 +77,18 @@ contract RootAxelarBridgeAdaptor is address(this), _childChain, _childBridgeAdaptor, payload, refundRecipient ); - axelarGateway.callContract(_childChain, _childBridgeAdaptor, payload); - emit AxelarMessage(_childChain, _childBridgeAdaptor, payload); + gateway.callContract(_childChain, _childBridgeAdaptor, payload); + emit AxelarMessageSent(_childChain, _childBridgeAdaptor, payload); + } + + /** + * @dev This function is called by the parent `AxelarExecutable` contract to execute the payload. + */ + function _execute(string calldata sourceChain_, string calldata sourceAddress_, bytes calldata payload_) + internal + override + { + emit AdaptorExecute(sourceChain_, sourceAddress_, payload_); + rootBridge.onMessageReceive(sourceChain_, sourceAddress_, payload_); } } diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 62f55cfaa..132fda725 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -7,6 +7,7 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol"; import {IRootERC20Bridge, IERC20Metadata} from "../interfaces/root/IRootERC20Bridge.sol"; import {IRootERC20BridgeEvents, IRootERC20BridgeErrors} from "../interfaces/root/IRootERC20Bridge.sol"; @@ -38,6 +39,7 @@ contract RootERC20Bridge is uint256 public constant NO_DEPOSIT_LIMIT = 0; bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address public constant NATIVE_ETH = address(0xeee); IRootERC20BridgeAdaptor public rootBridgeAdaptor; @@ -53,6 +55,8 @@ contract RootERC20Bridge is address public childETHToken; /// @dev The address of the wETH ERC20 token on L1. address public rootWETHToken; + /// @dev The name of the chain that this bridge is connected to. + string public childChain; /// @dev The maximum cumulative amount of IMX that can be deposited into the bridge. /// @dev A limit of zero indicates unlimited. uint256 public imxCumulativeDepositLimit; @@ -65,6 +69,7 @@ contract RootERC20Bridge is * @param newChildTokenTemplate Address of child token template to clone. * @param newRootIMXToken Address of ERC20 IMX on the root chain. * @param newRootWETHToken Address of ERC20 WETH on the root chain. + * @param newChildChain Name of child chain. * @param newImxCumulativeDepositLimit The cumulative IMX deposit limit. * @dev Can only be called once. */ @@ -75,6 +80,7 @@ contract RootERC20Bridge is address newChildTokenTemplate, address newRootIMXToken, address newRootWETHToken, + string memory newChildChain, uint256 newImxCumulativeDepositLimit ) public initializer { if ( @@ -86,6 +92,10 @@ contract RootERC20Bridge is if (bytes(newChildBridgeAdaptor).length == 0) { revert InvalidChildERC20BridgeAdaptor(); } + if (bytes(newChildChain).length == 0) { + revert InvalidChildChain(); + } + childERC20Bridge = newChildERC20Bridge; childTokenTemplate = newChildTokenTemplate; rootIMXToken = newRootIMXToken; @@ -95,6 +105,7 @@ contract RootERC20Bridge is ); rootBridgeAdaptor = IRootERC20BridgeAdaptor(newRootBridgeAdaptor); childBridgeAdaptor = newChildBridgeAdaptor; + childChain = newChildChain; imxCumulativeDepositLimit = newImxCumulativeDepositLimit; } @@ -135,6 +146,37 @@ contract RootERC20Bridge is */ receive() external payable {} + /** + * @inheritdoc IRootERC20Bridge + * @dev This is only callable by the root chain bridge adaptor. + * @dev Validates `sourceAddress` is the child chain's bridgeAdaptor. + */ + function onMessageReceive(string calldata messageSourceChain, string calldata sourceAddress, bytes calldata data) + external + override + { + if (msg.sender != address(rootBridgeAdaptor)) { + revert NotBridgeAdaptor(); + } + if (!Strings.equal(messageSourceChain, childChain)) { + revert InvalidSourceChain(); + } + if (!Strings.equal(sourceAddress, childBridgeAdaptor)) { + revert InvalidSourceAddress(); + } + if (data.length <= 32) { + // Data must always be greater than 32. + // 32 bytes for the signature, and at least some information for the payload + revert InvalidData("Data too short"); + } + + if (bytes32(data[:32]) == WITHDRAW_SIG) { + _withdraw(data[32:]); + } else { + revert InvalidData("Unsupported action signature"); + } + } + /** * @inheritdoc IRootERC20Bridge * @dev TODO when this becomes part of the deposit flow on a token's first bridge, this logic will need to be mostly moved into an internal function. @@ -308,4 +350,32 @@ contract RootERC20Bridge is emit ChildChainERC20Deposit(address(rootToken), childToken, msg.sender, receiver, amount); } } + + function _withdraw(bytes memory data) private { + (address rootToken, address withdrawer, address receiver, uint256 amount) = + abi.decode(data, (address, address, address, uint256)); + address childToken = rootTokenToChildToken[rootToken]; + if (childToken == address(0)) { + revert NotMapped(); + } + _executeTransfer(rootToken, childToken, withdrawer, receiver, amount); + } + + function _executeTransfer( + address rootToken, + address childToken, + address withdrawer, + address receiver, + uint256 amount + ) internal { + // TODO when withdrawing ETH/WETH, this next section will also need to check for the withdrawal of WETH (i.e. rootToken == NATIVE_ETH || rootToken == CHILD_WETH) + // Tests for this NATIVE_ETH branch not yet written. This should come as part of that PR. + if (rootToken == NATIVE_ETH) { + Address.sendValue(payable(receiver), amount); + } else { + IERC20Metadata(rootToken).safeTransfer(receiver, amount); + } + // slither-disable-next-line reentrancy-events + emit RootChainERC20Withdraw(rootToken, childToken, withdrawer, receiver, amount); + } } diff --git a/src/test/child/ChildERC20FailOnBurn.sol b/src/test/child/ChildERC20FailOnBurn.sol index f287319ca..5fd11ad86 100644 --- a/src/test/child/ChildERC20FailOnBurn.sol +++ b/src/test/child/ChildERC20FailOnBurn.sol @@ -12,7 +12,7 @@ import "../../child/ChildERC20.sol"; */ // solhint-disable reason-string contract ChildERC20FailOnBurn is ChildERC20 { - function burn(address account, uint256 amount) public virtual override returns (bool) { + function burn(address, /*account*/ uint256 /*amount*/ ) public virtual override returns (bool) { return false; } } diff --git a/src/test/root/MockAxelarGateway.sol b/src/test/root/MockAxelarGateway.sol index 756a61a62..8b16342a2 100644 --- a/src/test/root/MockAxelarGateway.sol +++ b/src/test/root/MockAxelarGateway.sol @@ -4,4 +4,8 @@ pragma solidity ^0.8.21; // @dev A contract for ensuring the Axelar Gateway is called correctly during unit tests. contract MockAxelarGateway { function callContract(string memory childChain, string memory childBridgeAdaptor, bytes memory payload) external {} + + function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) { + return true; + } } diff --git a/src/test/root/StubRootBridge.sol b/src/test/root/StubRootBridge.sol index da4081155..e8fa980c0 100644 --- a/src/test/root/StubRootBridge.sol +++ b/src/test/root/StubRootBridge.sol @@ -7,4 +7,6 @@ contract StubRootBridge { function childBridgeAdaptor() external pure returns (string memory) { return Strings.toHexString(address(9999)); } + + function onMessageReceive(string calldata, string calldata, bytes calldata) external {} } diff --git a/test/integration/child/ChildAxelarBridge.t.sol b/test/integration/child/ChildAxelarBridge.t.sol index c73f41a68..bf8e0da28 100644 --- a/test/integration/child/ChildAxelarBridge.t.sol +++ b/test/integration/child/ChildAxelarBridge.t.sol @@ -83,7 +83,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil bytes32 commandId = bytes32("testCommandId"); bytes memory payload = abi.encode("invalid payload"); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); } @@ -109,7 +109,7 @@ contract ChildERC20BridgeIntegrationTest is Test, IChildERC20BridgeEvents, IChil bytes32 commandId = bytes32("testCommandId"); bytes memory payload = ""; - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); childAxelarBridgeAdaptor.execute(commandId, ROOT_CHAIN_NAME, ROOT_ADAPTOR_ADDRESS, payload); } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol index e52abd117..5b098ac91 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdraw.t.sol @@ -106,14 +106,14 @@ contract ChildERC20BridgeWithdrawIntegrationTest is childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); } - function test_withdraw_emits_AxelarMessageEvent() public { + function test_withdraw_emits_AxelarMessageSentEvent() public { ChildERC20 childToken = ChildERC20(childBridge.rootTokenToChildToken(rootToken)); bytes memory predictedPayload = abi.encode(WITHDRAW_SIG, rootToken, address(this), address(this), withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdraw{value: withdrawFee}(childToken, withdrawAmount); } diff --git a/test/integration/root/RootERC20Bridge.t.sol b/test/integration/root/RootERC20Bridge.t.sol index 9728d5959..3a767d299 100644 --- a/test/integration/root/RootERC20Bridge.t.sol +++ b/test/integration/root/RootERC20Bridge.t.sol @@ -51,7 +51,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, Strings.toHexString(CHILD_BRIDGE_ADAPTOR), payload); vm.expectEmit(true, true, false, false, address(rootBridge)); emit L1TokenMapped(address(token), childToken); @@ -113,7 +113,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx console2.logBytes(predictedPayload); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit NativeEthDeposit( address(NATIVE_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount @@ -169,7 +169,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(IMX_TOKEN_ADDRESS, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit IMXDeposit(address(IMX_TOKEN_ADDRESS), address(this), address(this), tokenAmount); @@ -226,7 +226,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(WRAPPED_ETH, rootBridge, mapTokenFee, depositFee, tokenAmount, false); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit WETHDeposit(address(WRAPPED_ETH), rootBridge.childETHToken(), address(this), address(this), tokenAmount); vm.expectCall( @@ -284,7 +284,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDeposit(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, true); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit ChildChainERC20Deposit(address(token), childToken, address(this), address(this), tokenAmount); @@ -340,7 +340,7 @@ contract RootERC20BridgeIntegrationTest is Test, IRootERC20BridgeEvents, IRootAx setupDepositTo(address(token), rootBridge, mapTokenFee, depositFee, tokenAmount, recipient, true); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptorString, predictedPayload); vm.expectEmit(address(rootBridge)); emit ChildChainERC20Deposit(address(token), childToken, address(this), recipient, tokenAmount); diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol new file mode 100644 index 000000000..4bd1cded8 --- /dev/null +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import { + RootERC20Bridge, + IRootERC20BridgeEvents, + IERC20Metadata, + IRootERC20BridgeErrors +} from "../../../../src/root/RootERC20Bridge.sol"; +import { + RootAxelarBridgeAdaptor, IRootAxelarBridgeAdaptorEvents +} from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; +import {WETH} from "../../../../src/test/root/WETH.sol"; + +contract RootERC20BridgeWithdrawIntegrationTest is + Test, + IRootERC20BridgeErrors, + IRootERC20BridgeEvents, + IRootAxelarBridgeAdaptorEvents, + Utils +{ + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string constant CHILD_CHAIN_NAME = "CHILD"; + address constant IMX_TOKEN_ADDRESS = address(0xccc); + address constant NATIVE_ETH = address(0xeee); + address constant WRAPPED_ETH = address(0xddd); + uint256 constant UNLIMITED_IMX_DEPOSIT_LIMIT = 0; + + uint256 constant withdrawAmount = 0.5 ether; + + ERC20PresetMinterPauser public token; + ERC20PresetMinterPauser public imxToken; + RootERC20Bridge public rootBridge; + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); + + (imxToken, token, rootBridge, axelarAdaptor, mockAxelarGateway, axelarGasService) = rootIntegrationSetup( + CHILD_BRIDGE, + CHILD_BRIDGE_ADAPTOR, + CHILD_CHAIN_NAME, + IMX_TOKEN_ADDRESS, + WRAPPED_ETH, + UNLIMITED_IMX_DEPOSIT_LIMIT + ); + + // Need to first map the token. + rootBridge.mapToken{value: 1}(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + } + + function test_RevertsIf_WithdrawWithInvalidSourceChain() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectRevert(InvalidSourceChain.selector); + axelarAdaptor.execute(commandId, "INVALID", sourceAddress, data); + } + + function test_RevertsIf_WithdrawWithInvalidSourceAddress() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = Strings.toHexString(address(123)); + + vm.expectRevert(InvalidSourceAddress.selector); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_RevertsIf_MessageWithEmptyData() public { + bytes memory data; + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_RevertsIf_MessageWithInvalidSignature() public { + bytes memory data = abi.encode("INVALID_SIG", address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_withdraw_TransfersTokens() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 thisPreBal = token.balanceOf(address(this)); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 thisPostBal = token.balanceOf(address(this)); + uint256 bridgePostBal = token.balanceOf(address(rootBridge)); + + assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + + function test_withdraw_TransfersTokens_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 receiverPreBal = token.balanceOf(receiver); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 receiverPostBal = token.balanceOf(receiver); + uint256 bridgePostBal = token.balanceOf(address(rootBridge)); + + assertEq(receiverPostBal, receiverPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + + function test_withdraw_EmitsRootChainERC20WithdrawEvent() public { + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(token), + rootBridge.rootTokenToChildToken(address(token)), + address(this), + address(this), + withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + + function test_withdraw_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), receiver, withdrawAmount + ); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } +} diff --git a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol index 5cdf8e049..148445c60 100644 --- a/test/unit/child/ChildAxelarBridgeAdaptor.t.sol +++ b/test/unit/child/ChildAxelarBridgeAdaptor.t.sol @@ -47,7 +47,7 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro newAdaptor.initialize("root", address(0), address(mockChildAxelarGasService)); } - function test_Execute() public { + function test_Execute_CallsBridge() public { bytes32 commandId = bytes32("testCommandId"); string memory sourceChain = "test"; string memory sourceAddress = Strings.toHexString(address(123)); @@ -61,6 +61,18 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); } + function test_Execute_EmitsAdaptorExecuteEvent() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectEmit(); + emit AdaptorExecute(sourceChain, sourceAddress, payload); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } + function test_sendMessage_CallsGasService() public { address refundRecipient = address(123); bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); @@ -103,12 +115,12 @@ contract ChildAxelarBridgeAdaptorUnitTest is Test, IChildAxelarBridgeAdaptorErro axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } - function test_sendMessage_EmitsAxelarMessageEvent() public { + function test_sendMessage_EmitsAxelarMessageSentEvent() public { bytes memory payload = abi.encode(WITHDRAW_SIG, address(token), address(this), address(999), 11111); uint256 callValue = 300; vm.expectEmit(); - emit AxelarMessage(ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload); + emit AxelarMessageSent(ROOT_CHAIN_NAME, mockChildERC20Bridge.rootERC20BridgeAdaptor(), payload); vm.deal(address(mockChildERC20Bridge), callValue); vm.prank(address(mockChildERC20Bridge)); diff --git a/test/unit/child/ChildERC20Bridge.t.sol b/test/unit/child/ChildERC20Bridge.t.sol index 7bb37a263..72694bcac 100644 --- a/test/unit/child/ChildERC20Bridge.t.sol +++ b/test/unit/child/ChildERC20Bridge.t.sol @@ -168,7 +168,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B function test_RevertIf_onMessageReceiveCalledWithDataLengthZero() public { bytes memory data = ""; - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); } @@ -176,7 +176,7 @@ contract ChildERC20BridgeUnitTest is Test, IChildERC20BridgeEvents, IChildERC20B bytes memory data = abi.encode("FAKEDATA", address(rootToken), rootToken.name(), rootToken.symbol(), rootToken.decimals()); - vm.expectRevert(InvalidData.selector); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); childBridge.onMessageReceive(ROOT_CHAIN_NAME, ROOT_BRIDGE_ADAPTOR, data); } diff --git a/test/unit/root/RootAxelarBridgeAdaptor.t.sol b/test/unit/root/RootAxelarBridgeAdaptor.t.sol index 2374409f2..76281fce0 100644 --- a/test/unit/root/RootAxelarBridgeAdaptor.t.sol +++ b/test/unit/root/RootAxelarBridgeAdaptor.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {MockAxelarGateway} from "../../../src/test/root/MockAxelarGateway.sol"; import {MockAxelarGasService} from "../../../src/test/root/MockAxelarGasService.sol"; @@ -32,30 +33,54 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR stubRootBridge = new StubRootBridge(); childBridgeAdaptor = stubRootBridge.childBridgeAdaptor(); - axelarAdaptor = new RootAxelarBridgeAdaptor(); - axelarAdaptor.initialize( - address(stubRootBridge), CHILD_CHAIN_NAME, address(mockAxelarGateway), address(axelarGasService) - ); + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + axelarAdaptor.initialize(address(stubRootBridge), CHILD_CHAIN_NAME, address(axelarGasService)); vm.deal(address(stubRootBridge), 99999999999); } function test_Constructor() public { - assertEq(axelarAdaptor.rootBridge(), address(stubRootBridge), "rootBridge not set"); + assertEq(address(axelarAdaptor.rootBridge()), address(stubRootBridge), "rootBridge not set"); assertEq(axelarAdaptor.childChain(), CHILD_CHAIN_NAME, "childChain not set"); - assertEq(address(axelarAdaptor.axelarGateway()), address(mockAxelarGateway), "axelarGateway not set"); + assertEq(address(axelarAdaptor.gateway()), address(mockAxelarGateway), "axelarGateway not set"); assertEq(address(axelarAdaptor.gasService()), address(axelarGasService), "axelarGasService not set"); } function test_RevertWhen_InitializerGivenZeroAddress() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(ZeroAddresses.selector); - newAdaptor.initialize(address(0), CHILD_CHAIN_NAME, address(mockAxelarGateway), address(axelarGasService)); + newAdaptor.initialize(address(0), CHILD_CHAIN_NAME, address(axelarGasService)); } function test_RevertWhen_ConstructorGivenEmptyChildChainName() public { - RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(); + RootAxelarBridgeAdaptor newAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); vm.expectRevert(InvalidChildChain.selector); - newAdaptor.initialize(address(this), "", address(mockAxelarGateway), address(axelarGasService)); + newAdaptor.initialize(address(this), "", address(axelarGasService)); + } + + function test_Execute_CallsBridge() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectCall( + address(stubRootBridge), + abi.encodeWithSelector(stubRootBridge.onMessageReceive.selector, sourceChain, sourceAddress, payload) + ); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } + + function test_Execute_EmitsAdaptorExecuteEvent() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectEmit(); + emit AdaptorExecute(sourceChain, sourceAddress, payload); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); } /// @dev For this unit test we just want to make sure the correct functions are called on the Axelar Gateway and Gas Service. @@ -96,12 +121,12 @@ contract RootAxelarBridgeAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IR axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } - function test_sendMessage_EmitsAxelarMessageEvent() public { + function test_sendMessage_EmitsAxelarMessageSentEvent() public { bytes memory payload = abi.encode(MAP_TOKEN_SIG, address(token), token.name(), token.symbol(), token.decimals()); uint256 callValue = 300; vm.expectEmit(true, true, true, false, address(axelarAdaptor)); - emit AxelarMessage(CHILD_CHAIN_NAME, childBridgeAdaptor, payload); + emit AxelarMessageSent(CHILD_CHAIN_NAME, childBridgeAdaptor, payload); vm.prank(address(stubRootBridge)); axelarAdaptor.sendMessage{value: callValue}(payload, address(123)); } diff --git a/test/unit/root/RootERC20Bridge.t.sol b/test/unit/root/RootERC20Bridge.t.sol index abc5174e7..e4c3de3e9 100644 --- a/test/unit/root/RootERC20Bridge.t.sol +++ b/test/unit/root/RootERC20Bridge.t.sol @@ -55,6 +55,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(token), IMX_TOKEN, WRAPPED_ETH, + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -67,6 +68,11 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid assertEq(address(rootBridge.rootBridgeAdaptor()), address(mockAxelarAdaptor), "bridgeAdaptor not set"); assertEq(rootBridge.childERC20Bridge(), CHILD_BRIDGE, "childERC20Bridge not set"); assertEq(rootBridge.childTokenTemplate(), address(token), "childTokenTemplate not set"); + assert(Strings.equal(rootBridge.childChain(), CHILD_CHAIN_NAME)); + assert(Strings.equal(CHILD_BRIDGE_ADAPTOR_STRING, rootBridge.childBridgeAdaptor())); + assertEq(address(token), rootBridge.childTokenTemplate(), "childTokenTemplate not set"); + assertEq(rootBridge.rootIMXToken(), IMX_TOKEN, "rootIMXToken not set"); + assertEq(rootBridge.rootWETHToken(), WRAPPED_ETH, "rootWETHToken not set"); } function test_RevertIfInitializeTwice() public { @@ -78,6 +84,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(token), IMX_TOKEN, WRAPPED_ETH, + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -92,6 +99,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(1), address(1), + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -106,6 +114,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(1), address(1), + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -113,7 +122,9 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_InitializeWithEmptyChildAdapter() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(InvalidChildERC20BridgeAdaptor.selector); - bridge.initialize(address(1), address(1), "", address(1), address(1), address(1), UNLIMITED_IMX_DEPOSITS); + bridge.initialize( + address(1), address(1), "", address(1), address(1), address(1), CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS + ); } function test_RevertIf_InitializeWithAZeroAddressTokenTemplate() public { @@ -126,6 +137,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(0), address(1), address(1), + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -140,6 +152,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(0), address(1), + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -154,6 +167,7 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid address(1), address(1), address(0), + CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS ); } @@ -161,7 +175,24 @@ contract RootERC20BridgeUnitTest is Test, IRootERC20BridgeEvents, IRootERC20Brid function test_RevertIf_InitializeWithAZeroAddressAll() public { RootERC20Bridge bridge = new RootERC20Bridge(); vm.expectRevert(ZeroAddress.selector); - bridge.initialize(address(0), address(0), "", address(0), address(0), address(0), UNLIMITED_IMX_DEPOSITS); + bridge.initialize( + address(0), address(0), "", address(0), address(0), address(0), CHILD_CHAIN_NAME, UNLIMITED_IMX_DEPOSITS + ); + } + + function test_RevertIf_InitializeWithEmptyChildName() public { + RootERC20Bridge bridge = new RootERC20Bridge(); + vm.expectRevert(InvalidChildChain.selector); + bridge.initialize( + address(1), + address(1), + CHILD_BRIDGE_ADAPTOR_STRING, + address(1), + address(1), + address(1), + "", + UNLIMITED_IMX_DEPOSITS + ); } /** diff --git a/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol b/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol new file mode 100644 index 000000000..6b997c686 --- /dev/null +++ b/test/unit/root/withdrawals/RootAxelarBridgeAdaptorWithdraw.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import { + RootAxelarBridgeAdaptor, + IRootAxelarBridgeAdaptorEvents, + IRootAxelarBridgeAdaptorErrors +} from "../../../../src/root/RootAxelarBridgeAdaptor.sol"; +import {StubRootBridge} from "../../../../src/test/root/StubRootBridge.sol"; + +contract RootAxelarBridgeWithdrawAdaptorTest is Test, IRootAxelarBridgeAdaptorEvents, IRootAxelarBridgeAdaptorErrors { + address constant CHILD_BRIDGE = address(3); + string public childBridgeAdaptor; + string constant CHILD_CHAIN_NAME = "test"; + bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); + + ERC20PresetMinterPauser public token; + RootAxelarBridgeAdaptor public axelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + StubRootBridge public stubRootBridge; + + function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + stubRootBridge = new StubRootBridge(); + childBridgeAdaptor = stubRootBridge.childBridgeAdaptor(); + + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); + axelarAdaptor.initialize(address(stubRootBridge), CHILD_CHAIN_NAME, address(axelarGasService)); + vm.deal(address(stubRootBridge), 99999999999); + } + + function test_execute_callsBridge() public { + bytes32 commandId = bytes32("testCommandId"); + string memory sourceChain = "test"; + string memory sourceAddress = Strings.toHexString(address(123)); + bytes memory payload = abi.encodePacked("payload"); + + // We expect to call the bridge's onMessageReceive function. + vm.expectCall( + address(stubRootBridge), + abi.encodeWithSelector(stubRootBridge.onMessageReceive.selector, sourceChain, sourceAddress, payload) + ); + axelarAdaptor.execute(commandId, sourceChain, sourceAddress, payload); + } +} diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol new file mode 100644 index 000000000..be1999cdc --- /dev/null +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + RootERC20Bridge, IRootERC20BridgeEvents, IRootERC20BridgeErrors +} from "../../../../src/root/RootERC20Bridge.sol"; +import {MockAxelarGateway} from "../../../../src/test/root/MockAxelarGateway.sol"; +import {MockAxelarGasService} from "../../../../src/test/root/MockAxelarGasService.sol"; +import {MockAdaptor} from "../../../../src/test/root/MockAdaptor.sol"; +import {Utils} from "../../../utils.t.sol"; + +contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootERC20BridgeErrors, Utils { + address constant CHILD_BRIDGE = address(3); + address constant CHILD_BRIDGE_ADAPTOR = address(4); + string CHILD_BRIDGE_ADAPTOR_STRING = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); + string constant CHILD_CHAIN_NAME = "test"; + address constant IMX_TOKEN = address(0xccc); + address constant WRAPPED_ETH = address(0xddd); + uint256 constant mapTokenFee = 300; + uint256 constant withdrawAmount = 0.5 ether; + uint256 constant UNLIMITED_IMX_DEPOSIT_LIMIT = 0; + + ERC20PresetMinterPauser public token; + RootERC20Bridge public rootBridge; + MockAdaptor public mockAxelarAdaptor; + MockAxelarGateway public mockAxelarGateway; + MockAxelarGasService public axelarGasService; + + function setUp() public { + token = new ERC20PresetMinterPauser("Test", "TST"); + token.mint(address(this), 100 ether); + deployCodeTo("ERC20PresetMinterPauser.sol", abi.encode("ImmutableX", "IMX"), IMX_TOKEN); + + deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); + + rootBridge = new RootERC20Bridge(); + mockAxelarGateway = new MockAxelarGateway(); + axelarGasService = new MockAxelarGasService(); + + mockAxelarAdaptor = new MockAdaptor(); + + // The specific ERC20 token template does not matter for these unit tests + rootBridge.initialize( + address(mockAxelarAdaptor), + CHILD_BRIDGE, + CHILD_BRIDGE_ADAPTOR_STRING, + address(token), + IMX_TOKEN, + WRAPPED_ETH, + CHILD_CHAIN_NAME, + UNLIMITED_IMX_DEPOSIT_LIMIT + ); + } + + function test_RevertsIf_WithdrawWithInvalidSender() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.expectRevert(NotBridgeAdaptor.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithInvalidSourceChain() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(InvalidSourceChain.selector); + rootBridge.onMessageReceive("ding_dong", CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithInvalidSourceAddress() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + + console2.log(CHILD_CHAIN_NAME); + console2.log(rootBridge.childChain()); + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(InvalidSourceAddress.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, "DING_DONG", data); + } + + function test_RevertsIf_OnMessageReceiveWithZeroDataLength() public { + bytes memory data; + + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Data too short")); + + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithInvalidSignature() public { + bytes memory data = abi.encode(keccak256("RANDOM"), IMX_TOKEN, address(this), address(this), withdrawAmount); + + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(abi.encodeWithSelector(InvalidData.selector, "Unsupported action signature")); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_RevertsIf_OnMessageReceiveWithUnmappedToken() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + vm.expectRevert(NotMapped.selector); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_onMessageReceive_TransfersTokens() public { + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + uint256 thisPreBal = token.balanceOf(address(this)); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(token.balanceOf(address(this)), thisPreBal + withdrawAmount, "Tokens not transferred to receiver"); + assertEq( + token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge" + ); + } + + function test_onMessageReceive_TransfersTokens_DifferentReceiver() public { + address receiver = address(123456); + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + uint256 receiverPreBal = token.balanceOf(receiver); + uint256 bridgePreBal = token.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(token.balanceOf(receiver), receiverPreBal + withdrawAmount, "Tokens not transferred to receiver"); + assertEq( + token.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "Tokens not transferred from bridge" + ); + } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent() public { + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(token), + rootBridge.rootTokenToChildToken(address(token)), + address(this), + address(this), + withdrawAmount + ); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(123456); + // Need to first map the token. + rootBridge.mapToken(token); + // And give the bridge some tokens + token.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), receiver, withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw( + address(token), rootBridge.rootTokenToChildToken(address(token)), address(this), receiver, withdrawAmount + ); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } +} diff --git a/test/utils.t.sol b/test/utils.t.sol index 514fa45c0..fb3b0c2eb 100644 --- a/test/utils.t.sol +++ b/test/utils.t.sol @@ -87,7 +87,7 @@ contract Utils is Test { mockAxelarGateway = new MockAxelarGateway(); axelarGasService = new MockAxelarGasService(); - axelarAdaptor = new RootAxelarBridgeAdaptor(); + axelarAdaptor = new RootAxelarBridgeAdaptor(address(mockAxelarGateway)); rootBridge.initialize( address(axelarAdaptor), @@ -96,12 +96,11 @@ contract Utils is Test { address(token), imxTokenAddress, wethTokenAddress, + "CHILD", imxCumulativeDepositLimit ); - axelarAdaptor.initialize( - address(rootBridge), childBridgeName, address(mockAxelarGateway), address(axelarGasService) - ); + axelarAdaptor.initialize(address(rootBridge), childBridgeName, address(axelarGasService)); } function setupDeposit(