diff --git a/src/root/RootERC20Bridge.sol b/src/root/RootERC20Bridge.sol index 475010ea..1a815c40 100644 --- a/src/root/RootERC20Bridge.sol +++ b/src/root/RootERC20Bridge.sol @@ -48,6 +48,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. @@ -378,9 +379,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 04f18e1f..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; diff --git a/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol b/test/integration/child/withdrawals/ChildAxelarBridgeWithdrawToIMX.t.sol index 2dde96f7..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; @@ -176,7 +176,7 @@ contract ChildERC20BridgewithdrawIMXToIntegrationTest is 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; diff --git a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol index 39a53a41..0246364c 100644 --- a/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol +++ b/test/integration/root/withdrawals.t.sol/RootERC20BridgeWithdraw.t.sol @@ -33,6 +33,7 @@ contract RootERC20BridgeWithdrawIntegrationTest is address constant NATIVE_ETH = address(0xeee); address constant WRAPPED_ETH = address(0xddd); uint256 constant UNLIMITED_DEPOSIT_LIMIT = 0; + address public constant NATIVE_IMX = address(0xfff); uint256 constant withdrawAmount = 0.5 ether; @@ -66,6 +67,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 { @@ -126,6 +128,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); @@ -145,6 +165,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); @@ -162,6 +201,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); @@ -175,4 +225,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 186cbdcc..9d7a4be4 100644 --- a/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol +++ b/test/unit/root/withdrawals/RootERC20BridgeWithdraw.t.sol @@ -20,13 +20,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; @@ -36,6 +39,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); @@ -68,14 +73,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); @@ -83,7 +88,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()); @@ -102,15 +107,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); @@ -135,6 +139,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. @@ -155,6 +176,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); @@ -174,6 +215,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. @@ -189,4 +241,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); + } }