diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 132fda72..b8d42089 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -41,6 +41,7 @@ contract RootERC20Bridge is bytes32 public constant DEPOSIT_SIG = keccak256("DEPOSIT"); bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); address public constant NATIVE_ETH = address(0xeee); + address public constant NATIVE_IMX = address(0xfff); IRootERC20BridgeAdaptor public rootBridgeAdaptor; /// @dev Used to verify source address in messages sent from child chain. @@ -354,9 +355,14 @@ contract RootERC20Bridge is function _withdraw(bytes memory data) private { (address rootToken, address withdrawer, address receiver, uint256 amount) = abi.decode(data, (address, address, address, uint256)); - address childToken = rootTokenToChildToken[rootToken]; - if (childToken == address(0)) { - revert NotMapped(); + address childToken; + if (address(rootToken) == rootIMXToken) { + childToken = NATIVE_IMX; + } else { + childToken = rootTokenToChildToken[rootToken]; + if (childToken == address(0)) { + revert NotMapped(); + } } _executeTransfer(rootToken, childToken, withdrawer, receiver, amount); } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol index 0396c0de..61dd35c2 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawIMX.t.sol @@ -102,7 +102,7 @@ contract ChildERC20BridgeWithdrawIMXIntegrationTest is childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); } - function test_WithdrawIMX_EmitsAxelarMessageEvent() public { + function test_WithdrawIMX_EmitsAxelarMessageSentEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -110,7 +110,7 @@ contract ChildERC20BridgeWithdrawIMXIntegrationTest is abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdrawIMX{value: withdrawFee + withdrawAmount}(withdrawAmount); } diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol index 8c6e1fea..a2a09223 100644 --- a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol +++ b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol @@ -163,7 +163,7 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } - function test_withdrawIMXTo_EmitsAxelarMessageEvent() public { + function test_withdrawIMXTo_EmitsAxelarMessageSentEvent() public { uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -171,12 +171,12 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), address(this), withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(address(this), withdrawAmount); } - function test_withdrawIMXToWithDifferentAccount_EmitsAxelarMessageEvent() public { + function test_withdrawIMXToWithDifferentAccount_EmitsAxelarMessageSentEvent() public { address receiver = address(0xabcd); uint256 withdrawFee = 300; uint256 withdrawAmount = 7 ether; @@ -185,7 +185,7 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is abi.encode(WITHDRAW_SIG, ROOT_IMX_TOKEN, address(this), receiver, withdrawAmount); vm.expectEmit(address(axelarAdaptor)); - emit AxelarMessage(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); + emit AxelarMessageSent(childBridge.rootChain(), childBridge.rootERC20BridgeAdaptor(), predictedPayload); childBridge.withdrawIMXTo{value: withdrawFee + withdrawAmount}(receiver, withdrawAmount); } diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 4bd1cded..dff08e97 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -32,6 +32,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is address constant IMX_TOKEN_ADDRESS = address(0xccc); address constant NATIVE_ETH = address(0xeee); address constant WRAPPED_ETH = address(0xddd); + address public constant NATIVE_IMX = address(0xfff); uint256 constant UNLIMITED_IMX_DEPOSIT_LIMIT = 0; uint256 constant withdrawAmount = 0.5 ether; @@ -59,6 +60,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is rootBridge.mapToken{value: 1}(token); // And give the bridge some tokens token.transfer(address(rootBridge), 100 ether); + imxToken.transfer(address(rootBridge), 100 ether); } function test_RevertsIf_WithdrawWithInvalidSourceChain() public { @@ -119,6 +121,24 @@ contract RootERC20BridgeWithdrawIntegrationTest is assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } + function test_withdrawIMX_TransfersIMX() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 thisPreBal = imxToken.balanceOf(address(this)); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 thisPostBal = imxToken.balanceOf(address(this)); + uint256 bridgePostBal = imxToken.balanceOf(address(rootBridge)); + + assertEq(thisPostBal, thisPreBal + withdrawAmount, "Incorrect user balance after withdraw"); + assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); + } + function test_withdraw_TransfersTokens_DifferentReceiver() public { address receiver = address(987654321); bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); @@ -138,6 +158,25 @@ contract RootERC20BridgeWithdrawIntegrationTest is assertEq(bridgePostBal, bridgePreBal - withdrawAmount, "Incorrect bridge balance after withdraw"); } + function test_withdrawIMX_TransfersIMX_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + uint256 receiverPreBal = imxToken.balanceOf(receiver); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + + uint256 receiverPostBal = imxToken.balanceOf(receiver); + uint256 bridgePostBal = imxToken.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); @@ -155,6 +194,17 @@ contract RootERC20BridgeWithdrawIntegrationTest is axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } + function test_withdrawIMX_EmitsRootChainERC20WithdrawEvent() public { + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), address(this), withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), address(this), withdrawAmount); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } + function test_withdraw_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { address receiver = address(987654321); bytes memory data = abi.encode(WITHDRAW_SIG, address(token), address(this), receiver, withdrawAmount); @@ -168,4 +218,16 @@ contract RootERC20BridgeWithdrawIntegrationTest is ); axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); } + + function test_withdrawIMX_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { + address receiver = address(987654321); + bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN_ADDRESS, address(this), receiver, withdrawAmount); + + bytes32 commandId = bytes32("testCommandId"); + string memory sourceAddress = rootBridge.childBridgeAdaptor(); + + vm.expectEmit(); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), receiver, withdrawAmount); + axelarAdaptor.execute(commandId, CHILD_CHAIN_NAME, sourceAddress, data); + } } diff --git a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol index be1999cd..abb233f9 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -17,13 +17,16 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE address constant CHILD_BRIDGE_ADAPTOR = address(4); string CHILD_BRIDGE_ADAPTOR_STRING = Strings.toHexString(CHILD_BRIDGE_ADAPTOR); string constant CHILD_CHAIN_NAME = "test"; + address constant UnmappedToken = address(0xbbb); address constant IMX_TOKEN = address(0xccc); address constant WRAPPED_ETH = address(0xddd); + address public constant NATIVE_IMX = address(0xfff); uint256 constant mapTokenFee = 300; uint256 constant withdrawAmount = 0.5 ether; uint256 constant UNLIMITED_IMX_DEPOSIT_LIMIT = 0; ERC20PresetMinterPauser public token; + ERC20PresetMinterPauser public imxToken; RootERC20Bridge public rootBridge; MockAdaptor public mockAxelarAdaptor; MockAxelarGateway public mockAxelarGateway; @@ -33,6 +36,8 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE token = new ERC20PresetMinterPauser("Test", "TST"); token.mint(address(this), 100 ether); deployCodeTo("ERC20PresetMinterPauser.sol", abi.encode("ImmutableX", "IMX"), IMX_TOKEN); + imxToken = ERC20PresetMinterPauser(IMX_TOKEN); + imxToken.mint(address(this), 100 ether); deployCodeTo("WETH.sol", abi.encode("Wrapped ETH", "WETH"), WRAPPED_ETH); @@ -56,14 +61,14 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE } function test_RevertsIf_WithdrawWithInvalidSender() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); vm.expectRevert(NotBridgeAdaptor.selector); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } function test_RevertsIf_OnMessageReceiveWithInvalidSourceChain() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); vm.expectRevert(InvalidSourceChain.selector); @@ -71,7 +76,7 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE } function test_RevertsIf_OnMessageReceiveWithInvalidSourceAddress() public { - bytes memory data = abi.encode(WITHDRAW_SIG, IMX_TOKEN, address(this), address(this), withdrawAmount); + bytes memory data = abi.encode(WITHDRAW_SIG, token, address(this), address(this), withdrawAmount); console2.log(CHILD_CHAIN_NAME); console2.log(rootBridge.childChain()); @@ -90,15 +95,14 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE } function test_RevertsIf_OnMessageReceiveWithInvalidSignature() public { - bytes memory data = abi.encode(keccak256("RANDOM"), IMX_TOKEN, address(this), address(this), withdrawAmount); - + bytes memory data = abi.encode(keccak256("RANDOM"), token, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); vm.expectRevert(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); + bytes memory data = abi.encode(WITHDRAW_SIG, UnmappedToken, address(this), address(this), withdrawAmount); vm.prank(address(mockAxelarAdaptor)); vm.expectRevert(NotMapped.selector); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); @@ -123,6 +127,23 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE ); } + function test_onMessageReceive_TransfersIMXTokens() public { + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + uint256 thisPreBal = imxToken.balanceOf(address(this)); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq(imxToken.balanceOf(address(this)), thisPreBal + withdrawAmount, "IMX not transferred to receiver"); + assertEq( + imxToken.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "IMX not transferred from bridge" + ); + } + function test_onMessageReceive_TransfersTokens_DifferentReceiver() public { address receiver = address(123456); // Need to first map the token. @@ -143,6 +164,26 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE ); } + function test_onMessageReceive_TransfersIMXTokens_DifferentReceiver() public { + address receiver = address(123456); + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + uint256 receiverPreBal = imxToken.balanceOf(address(receiver)); + uint256 bridgePreBal = imxToken.balanceOf(address(rootBridge)); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + + assertEq( + imxToken.balanceOf(address(receiver)), receiverPreBal + withdrawAmount, "IMX not transferred to receiver" + ); + assertEq( + imxToken.balanceOf(address(rootBridge)), bridgePreBal - withdrawAmount, "IMX not transferred from bridge" + ); + } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent() public { // Need to first map the token. rootBridge.mapToken(token); @@ -162,6 +203,17 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEventForIMX() public { + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), address(this), withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), address(this), withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } + function test_onMessageReceive_EmitsRootChainERC20WithdrawEvent_DifferentReceiver() public { address receiver = address(123456); // Need to first map the token. @@ -177,4 +229,16 @@ contract RootERC20BridgeWithdrawUnitTest is Test, IRootERC20BridgeEvents, IRootE vm.prank(address(mockAxelarAdaptor)); rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); } + + function test_onMessageReceive_EmitsRootChainERC20WithdrawEventForIMX_DifferentReceiver() public { + address receiver = address(123456); + // Give bridge some IMX tokens + imxToken.transfer(address(rootBridge), 100 ether); + + bytes memory data = abi.encode(WITHDRAW_SIG, imxToken, address(this), receiver, withdrawAmount); + vm.expectEmit(); + emit RootChainERC20Withdraw(address(imxToken), NATIVE_IMX, address(this), receiver, withdrawAmount); + vm.prank(address(mockAxelarAdaptor)); + rootBridge.onMessageReceive(CHILD_CHAIN_NAME, CHILD_BRIDGE_ADAPTOR_STRING, data); + } }