From 2c5e2738ed75a4a6aa13c5bcd63d8e89c9cffec0 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 12 Dec 2024 10:18:34 +0200 Subject: [PATCH 01/35] initial support --- contracts/scripts/DeployLocal.sol | 10 +++---- .../{FundAgent.sol => FundGateway.sol} | 13 +++------ contracts/src/Assets.sol | 27 +++++++++++++++---- contracts/src/Gateway.sol | 18 ++++++------- contracts/src/Types.sol | 1 + contracts/test/Gateway.t.sol | 2 +- .../{fund-agent.sh => fund-gateway.sh} | 10 +++---- web/packages/test/scripts/set-env.sh | 2 +- 8 files changed, 45 insertions(+), 38 deletions(-) rename contracts/scripts/{FundAgent.sol => FundGateway.sol} (66%) rename web/packages/test/scripts/{fund-agent.sh => fund-gateway.sh} (63%) diff --git a/contracts/scripts/DeployLocal.sol b/contracts/scripts/DeployLocal.sol index 4fd6be19ee..c2eba56e67 100644 --- a/contracts/scripts/DeployLocal.sol +++ b/contracts/scripts/DeployLocal.sol @@ -96,15 +96,11 @@ contract DeployLocal is Script { // Deploy WETH for testing new WETH9(); - // Fund the sovereign account for the BridgeHub parachain. Used to reward relayers + // Fund the gateway proxy contract. Used to reward relayers // of messages originating from BridgeHub - uint256 initialDeposit = vm.envUint("BRIDGE_HUB_INITIAL_DEPOSIT"); + uint256 initialDeposit = vm.envUint("GATEWAY_PROXY_INITIAL_DEPOSIT"); - address bridgeHubAgent = IGateway(address(gateway)).agentOf(bridgeHubAgentID); - address assetHubAgent = IGateway(address(gateway)).agentOf(assetHubAgentID); - - payable(bridgeHubAgent).safeNativeTransfer(initialDeposit); - payable(assetHubAgent).safeNativeTransfer(initialDeposit); + payable(gateway).safeNativeTransfer(initialDeposit); // Deploy MockGatewayV2 for testing new MockGatewayV2(); diff --git a/contracts/scripts/FundAgent.sol b/contracts/scripts/FundGateway.sol similarity index 66% rename from contracts/scripts/FundAgent.sol rename to contracts/scripts/FundGateway.sol index c25affd4c1..89e021fb9b 100644 --- a/contracts/scripts/FundAgent.sol +++ b/contracts/scripts/FundGateway.sol @@ -15,7 +15,7 @@ import {ParaID} from "../src/Types.sol"; import {SafeNativeTransfer} from "../src/utils/SafeTransfer.sol"; import {stdJson} from "forge-std/StdJson.sol"; -contract FundAgent is Script { +contract FundGateway is Script { using SafeNativeTransfer for address payable; using stdJson for string; @@ -26,17 +26,10 @@ contract FundAgent is Script { address deployer = vm.rememberKey(privateKey); vm.startBroadcast(deployer); - uint256 initialDeposit = vm.envUint("BRIDGE_HUB_INITIAL_DEPOSIT"); + uint256 initialDeposit = vm.envUint("GATEWAY_PROXY_INITIAL_DEPOSIT"); address gatewayAddress = vm.envAddress("GATEWAY_PROXY_CONTRACT"); - bytes32 bridgeHubAgentID = vm.envBytes32("BRIDGE_HUB_AGENT_ID"); - bytes32 assetHubAgentID = vm.envBytes32("ASSET_HUB_AGENT_ID"); - - address bridgeHubAgent = IGateway(gatewayAddress).agentOf(bridgeHubAgentID); - address assetHubAgent = IGateway(gatewayAddress).agentOf(assetHubAgentID); - - payable(bridgeHubAgent).safeNativeTransfer(initialDeposit); - payable(assetHubAgent).safeNativeTransfer(initialDeposit); + payable(gatewayAddress).safeNativeTransfer(initialDeposit); vm.stopBroadcast(); } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 806b11e0af..26f6919e8c 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -13,6 +13,7 @@ import {CoreStorage} from "./storage/CoreStorage.sol"; import {SubstrateTypes} from "./SubstrateTypes.sol"; import {ChannelID, ParaID, MultiAddress, Ticket, Costs} from "./Types.sol"; import {Address} from "./utils/Address.sol"; +import {SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {AgentExecutor} from "./AgentExecutor.sol"; import {Agent} from "./Agent.sol"; import {Call} from "./utils/Call.sol"; @@ -21,6 +22,7 @@ import {Token} from "./Token.sol"; /// @title Library for implementing Ethereum->Polkadot ERC20 transfers. library Assets { using Address for address; + using SafeNativeTransfer for address payable; using SafeTokenTransferFrom for IERC20; /* Errors */ @@ -110,15 +112,17 @@ library Assets { TokenInfo storage info = $.tokenRegistry[token]; - if (!info.isRegistered) { - revert TokenNotRegistered(); - } - if (info.foreignID == bytes32(0)) { + if (!info.isRegistered && token != address(0)) { + revert TokenNotRegistered(); + } return _sendNativeToken( token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount ); } else { + if (!info.isRegistered) { + revert TokenNotRegistered(); + } return _sendForeignToken( info.foreignID, token, @@ -144,7 +148,18 @@ library Assets { AssetsStorage.Layout storage $ = AssetsStorage.layout(); // Lock the funds into AssetHub's agent contract - _transferToAgent($.assetHubAgent, token, sender, amount); + if (token != address(0)) { + // ERC20 + _transferToAgent($.assetHubAgent, token, sender, amount); + ticket.etherAmount = 0; + } else { + // Native ETH + if (msg.value < amount) { + revert TokenAmountTooLow(); + } + payable($.assetHubAgent).safeNativeTransfer(amount); + ticket.etherAmount = amount; + } ticket.dest = $.assetHubParaID; ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); @@ -211,6 +226,7 @@ library Assets { ticket.dest = $.assetHubParaID; ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); + ticket.etherAmount = 0; // Construct a message payload if (destinationChain == $.assetHubParaID && destinationAddress.isAddress32()) { @@ -262,6 +278,7 @@ library Assets { ticket.dest = $.assetHubParaID; ticket.costs = _registerTokenCosts(); ticket.payload = SubstrateTypes.RegisterToken(token, $.assetHubCreateAssetFee); + ticket.etherAmount = 0; emit IGateway.TokenRegistrationSent(token); } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 8be9541cec..356cf4d58a 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -99,6 +99,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { error InvalidAgentExecutionPayload(); error InvalidConstructorParams(); error TokenNotRegistered(); + error TokenAmountTooLow(); // Message handlers can only be dispatched by the gateway itself modifier onlySelf() { @@ -258,11 +259,11 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // Add the reward to the refund amount. If the sum is more than the funds available // in the channel agent, then reduce the total amount - uint256 amount = Math.min(refund + message.reward, address(channel.agent).balance); + uint256 amount = Math.min(refund + message.reward, address(this).balance); - // Do the payment if there funds available in the agent + // Do the payment if there funds available in the gateway if (amount > _dustThreshold()) { - _transferNativeFromAgent(channel.agent, payable(msg.sender), amount); + payable(msg.sender).safeNativeTransfer(amount); } emit IGateway.InboundMessageDispatched(message.channelID, message.nonce, message.id, success); @@ -548,18 +549,17 @@ contract Gateway is IGateway, IInitializable, IUpgradable { uint256 fee = _calculateFee(ticket.costs); // Ensure the user has enough funds for this message to be accepted - if (msg.value < fee) { + uint256 totalEther = fee + ticket.etherAmount; + if (msg.value < totalEther) { revert FeePaymentToLow(); } channel.outboundNonce = channel.outboundNonce + 1; - // Deposit total fee into agent's contract - payable(channel.agent).safeNativeTransfer(fee); - + // The fee is already collected into the gateway contract // Reimburse excess fee payment - if (msg.value > fee) { - payable(msg.sender).safeNativeTransfer(msg.value - fee); + if (msg.value > totalEther) { + payable(msg.sender).safeNativeTransfer(msg.value - totalEther); } // Generate a unique ID for this message diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 6d7ab930a6..e9ae67a845 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -107,6 +107,7 @@ struct Ticket { ParaID dest; Costs costs; bytes payload; + uint128 etherAmount; } struct TokenInfo { diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 8e44e7c29c..83d880ad8b 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -996,7 +996,7 @@ contract GatewayTest is Test { vm.expectRevert(Assets.TokenNotRegistered.selector); - IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(0x0), destPara, recipientAddress32, 1, 1); + IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(0x1), destPara, recipientAddress32, 1, 1); } function testSendTokenFromNotMintedAccountWillFail() public { diff --git a/web/packages/test/scripts/fund-agent.sh b/web/packages/test/scripts/fund-gateway.sh similarity index 63% rename from web/packages/test/scripts/fund-agent.sh rename to web/packages/test/scripts/fund-gateway.sh index e094e9c483..453864f49a 100755 --- a/web/packages/test/scripts/fund-agent.sh +++ b/web/packages/test/scripts/fund-gateway.sh @@ -3,19 +3,19 @@ set -eu source scripts/set-env.sh -fund_agent() { +fund_gateway() { pushd "$contract_dir" forge script \ --rpc-url $eth_endpoint_http \ --broadcast \ -vvv \ - scripts/FundAgent.sol:FundAgent + scripts/FundGateway.sol:FundGateway popd - echo "Fund agent success!" + echo "Fund gateway success!" } if [ -z "${from_start_services:-}" ]; then - echo "Funding agent" - fund_agent + echo "Funding gateway" + fund_gateway fi diff --git a/web/packages/test/scripts/set-env.sh b/web/packages/test/scripts/set-env.sh index 1ab1d6c278..2e6ff5f8c7 100755 --- a/web/packages/test/scripts/set-env.sh +++ b/web/packages/test/scripts/set-env.sh @@ -110,7 +110,7 @@ export LOCAL_REWARD="${LOCAL_REWARD:-1000000000000}" export REMOTE_REWARD="${REMOTE_REWARD:-1000000000000000}" ## Vault -export BRIDGE_HUB_INITIAL_DEPOSIT="${ETH_BRIDGE_HUB_INITIAL_DEPOSIT:-10000000000000000000}" +export GATEWAY_PROXY_INITIAL_DEPOSIT="${GATEWAY_PROXY_INITIAL_DEPOSIT:-10000000000000000000}" export GATEWAY_STORAGE_KEY="${GATEWAY_STORAGE_KEY:-0xaed97c7854d601808b98ae43079dafb3}" export GATEWAY_PROXY_CONTRACT="${GATEWAY_PROXY_CONTRACT:-0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d}" From 3f178ef5eb54cbd57453535c90f5b6620d511d17 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Tue, 17 Dec 2024 22:52:00 +0200 Subject: [PATCH 02/35] add error --- contracts/src/Assets.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 26f6919e8c..2dbb00e00f 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -36,6 +36,7 @@ library Assets { error TokenAlreadyRegistered(); error TokenMintFailed(); error TokenTransferFailed(); + error TokenAmountTooLow(); function isTokenRegistered(address token) external view returns (bool) { return AssetsStorage.layout().tokenRegistry[token].isRegistered; From 5887da784745a446ba55c6fc0977eca5636da9eb Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 00:01:27 +0200 Subject: [PATCH 03/35] fixed tests --- contracts/test/Gateway.t.sol | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 83d880ad8b..c992c553f4 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -192,8 +192,6 @@ contract GatewayTest is Test { * Message Verification */ function testSubmitHappyPath() public { - deal(assetHubAgent, 50 ether); - (Command command, bytes memory params) = makeCreateAgentCommand(); // Expect the gateway to emit `InboundMessageDispatched` @@ -209,8 +207,6 @@ contract GatewayTest is Test { } function testSubmitFailInvalidNonce() public { - deal(assetHubAgent, 50 ether); - (Command command, bytes memory params) = makeCreateAgentCommand(); hoax(relayer, 1 ether); @@ -243,8 +239,6 @@ contract GatewayTest is Test { } function testSubmitFailInvalidProof() public { - deal(assetHubAgent, 50 ether); - (Command command, bytes memory params) = makeCreateAgentCommand(); MockGateway(address(gateway)).setCommitmentsAreVerified(false); @@ -263,14 +257,15 @@ contract GatewayTest is Test { */ // Message relayer should be rewarded from the agent for a channel - function testRelayerRewardedFromAgent() public { + function testRelayerRewardedFromGateway() public { (Command command, bytes memory params) = makeCreateAgentCommand(); vm.txGasPrice(10 gwei); hoax(relayer, 1 ether); - deal(assetHubAgent, 50 ether); + deal(address(gateway), 50 ether); uint256 relayerBalanceBefore = address(relayer).balance; + uint256 gatewayBalanceBefore = address(address(gateway)).balance; uint256 agentBalanceBefore = address(assetHubAgent).balance; uint256 startGas = gasleft(); @@ -283,19 +278,22 @@ contract GatewayTest is Test { uint256 estimatedActualRefundAmount = (startGas - endGas) * tx.gasprice; assertLt(estimatedActualRefundAmount, maxRefund); - // Check that agent balance decreased and relayer balance increases - assertLt(address(assetHubAgent).balance, agentBalanceBefore); + // Agents do not pay reward+refund so no balance should change. + assertEq(address(assetHubAgent).balance, agentBalanceBefore); + // Relayer balance has increased + assertLt(address(gateway).balance, gatewayBalanceBefore); + // Relayer balance has increased assertGt(relayer.balance, relayerBalanceBefore); // The total amount paid to the relayer - uint256 totalPaid = agentBalanceBefore - address(assetHubAgent).balance; + uint256 totalPaid = gatewayBalanceBefore - address(gateway).balance; // Since we know that the actual refund amount is less than the max refund, // the total amount paid to the relayer is less. assertLt(totalPaid, maxRefund + reward); } - // In this case, the agent has no funds to reward the relayer + // In this case, the gateway has no funds to reward the relayer function testRelayerNotRewarded() public { (Command command, bytes memory params) = makeCreateAgentCommand(); @@ -773,8 +771,6 @@ contract GatewayTest is Test { } function testCreateAgentWithNotEnoughGas() public { - deal(assetHubAgent, 50 ether); - (Command command, bytes memory params) = makeCreateAgentCommand(); hoax(relayer, 1 ether); From 4677fe79c9bbfbfd4000546ea79fd3d513ff6e58 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Tue, 17 Dec 2024 23:46:57 +0200 Subject: [PATCH 04/35] test legacy and new unlock messages --- contracts/test/Gateway.t.sol | 85 +++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index c992c553f4..00e8863f42 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -161,6 +161,27 @@ contract GatewayTest is Test { return (Command.CreateAgent, abi.encode((keccak256("6666")))); } + function makeLegacyUnlockWethCommand(bytes32 agentID, address token_, address recipient, uint128 amount) + public + view + returns (Command, bytes memory) + { + bytes memory payload = abi.encode(token_, recipient, amount); + AgentExecuteParams memory params = + AgentExecuteParams({agentID: agentID, payload: abi.encode(AgentExecuteCommand.TransferToken, payload)}); + return (Command.AgentExecute, abi.encode(params)); + } + + function makeUnlockWethCommand(bytes32 agentID, address token_, address recipient, uint128 amount) + public + view + returns (Command, bytes memory) + { + TransferNativeTokenParams memory params = + TransferNativeTokenParams({agentID: agentID, token: token_, recipient: recipient, amount: amount}); + return (Command.TransferNativeToken, abi.encode(params)); + } + function makeMockProof() public pure returns (Verification.Proof memory) { return Verification.Proof({ header: Verification.ParachainHeader({ @@ -195,7 +216,7 @@ contract GatewayTest is Test { (Command command, bytes memory params) = makeCreateAgentCommand(); // Expect the gateway to emit `InboundMessageDispatched` - vm.expectEmit(true, false, false, false); + vm.expectEmit(); emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); hoax(relayer, 1 ether); @@ -206,6 +227,68 @@ contract GatewayTest is Test { ); } + function testLegacyUnlockWethHappyPath() public { + address recipient = makeAddr("test_recipeint"); + uint128 amount = 1; + + hoax(assetHubAgent, amount); + token.deposit{value: amount}(); + + (Command command, bytes memory params) = makeLegacyUnlockWethCommand(assetHubAgentID, address(token), recipient, amount); + + assertEq(token.balanceOf(assetHubAgent), amount); + assertEq(token.balanceOf(recipient), 0); + + // Expect WETH.Transfer event. + vm.expectEmit(); + emit WETH9.Transfer(assetHubAgent, recipient, amount); + + // Expect the gateway to emit `InboundMessageDispatched` + vm.expectEmit(); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + + assertEq(token.balanceOf(assetHubAgent), 0); + assertEq(token.balanceOf(recipient), amount); + } + + function testUnlockWethHappyPath() public { + address recipient = makeAddr("test_recipeint"); + uint128 amount = 1; + + hoax(assetHubAgent, amount); + token.deposit{value: amount}(); + + (Command command, bytes memory params) = makeUnlockWethCommand(assetHubAgentID, address(token), recipient, amount); + + assertEq(token.balanceOf(assetHubAgent), amount); + assertEq(token.balanceOf(recipient), 0); + + // Expect WETH.Transfer event. + vm.expectEmit(); + emit WETH9.Transfer(assetHubAgent, recipient, amount); + + // Expect the gateway to emit `InboundMessageDispatched` + vm.expectEmit(true, false, false, true); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + + assertEq(token.balanceOf(assetHubAgent), 0); + assertEq(token.balanceOf(recipient), amount); + } + function testSubmitFailInvalidNonce() public { (Command command, bytes memory params) = makeCreateAgentCommand(); From 13b59fbff8955a07717da604dedaccd50b1bbd89 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 00:07:51 +0200 Subject: [PATCH 05/35] fix spelling --- contracts/src/Gateway.sol | 4 ++-- contracts/test/Gateway.t.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 356cf4d58a..e72174f779 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -88,7 +88,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { error InvalidProof(); error InvalidNonce(); error NotEnoughGas(); - error FeePaymentToLow(); + error FeePaymentTooLow(); error Unauthorized(); error Disabled(); error AgentAlreadyCreated(); @@ -551,7 +551,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // Ensure the user has enough funds for this message to be accepted uint256 totalEther = fee + ticket.etherAmount; if (msg.value < totalEther) { - revert FeePaymentToLow(); + revert FeePaymentTooLow(); } channel.outboundNonce = channel.outboundNonce + 1; diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 00e8863f42..9611b91276 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -427,7 +427,7 @@ contract GatewayTest is Test { hoax(user); token.approve(address(gateway), 1); - vm.expectRevert(Gateway.FeePaymentToLow.selector); + vm.expectRevert(Gateway.FeePaymentTooLow.selector); hoax(user, 2 ether); IGateway(address(gateway)).sendToken{value: 0.002 ether}( address(token), ParaID.wrap(0), recipientAddress32, 1, 1 From 6d6f3fce9687ee496630a75b454e7ecfcef71205 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 00:18:38 +0200 Subject: [PATCH 06/35] test registration is blocked --- contracts/test/Gateway.t.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 9611b91276..a1b80fc3a8 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -1136,4 +1136,10 @@ contract GatewayTest is Test { address(token), paraID, recipientAddress32, destinationFee, amount ); } + + function testRegisterTokenWithEthWillReturnInvalidToken() public { + uint256 fee = IGateway(address(gateway)).quoteRegisterTokenFee(); + vm.expectRevert(Assets.InvalidToken.selector); + IGateway(address(gateway)).registerToken{value: fee}(address(0)); + } } From 299c19750338c8f351f945fb4a33f4fc82515379 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 00:42:38 +0200 Subject: [PATCH 07/35] fix inbound messages --- contracts/src/Assets.sol | 7 +++- contracts/test/Gateway.t.sol | 69 ++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 2dbb00e00f..df9ec9642b 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -317,7 +317,12 @@ library Assets { function transferNativeToken(address executor, address agent, address token, address recipient, uint128 amount) external { - bytes memory call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount)); + bytes memory call; + if (token != address(0)) { + call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount)); + } else { + call = abi.encodeCall(AgentExecutor.transferNative, (payable(recipient), amount)); + } (bool success,) = Agent(payable(agent)).invoke(executor, call); if (!success) { revert TokenTransferFailed(); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index a1b80fc3a8..4f4485099b 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -161,7 +161,7 @@ contract GatewayTest is Test { return (Command.CreateAgent, abi.encode((keccak256("6666")))); } - function makeLegacyUnlockWethCommand(bytes32 agentID, address token_, address recipient, uint128 amount) + function makeLegacyUnlockTokenCommand(bytes32 agentID, address token_, address recipient, uint128 amount) public view returns (Command, bytes memory) @@ -172,7 +172,7 @@ contract GatewayTest is Test { return (Command.AgentExecute, abi.encode(params)); } - function makeUnlockWethCommand(bytes32 agentID, address token_, address recipient, uint128 amount) + function makeUnlockTokenCommand(bytes32 agentID, address token_, address recipient, uint128 amount) public view returns (Command, bytes memory) @@ -234,7 +234,8 @@ contract GatewayTest is Test { hoax(assetHubAgent, amount); token.deposit{value: amount}(); - (Command command, bytes memory params) = makeLegacyUnlockWethCommand(assetHubAgentID, address(token), recipient, amount); + (Command command, bytes memory params) = + makeLegacyUnlockTokenCommand(assetHubAgentID, address(token), recipient, amount); assertEq(token.balanceOf(assetHubAgent), amount); assertEq(token.balanceOf(recipient), 0); @@ -261,12 +262,13 @@ contract GatewayTest is Test { function testUnlockWethHappyPath() public { address recipient = makeAddr("test_recipeint"); uint128 amount = 1; - + hoax(assetHubAgent, amount); token.deposit{value: amount}(); - (Command command, bytes memory params) = makeUnlockWethCommand(assetHubAgentID, address(token), recipient, amount); - + (Command command, bytes memory params) = + makeUnlockTokenCommand(assetHubAgentID, address(token), recipient, amount); + assertEq(token.balanceOf(assetHubAgent), amount); assertEq(token.balanceOf(recipient), 0); @@ -288,7 +290,60 @@ contract GatewayTest is Test { assertEq(token.balanceOf(assetHubAgent), 0); assertEq(token.balanceOf(recipient), amount); } - + + function testLegacyUnlockEthHappyPath() public { + address recipient = makeAddr("test_recipeint"); + uint128 amount = 1; + + deal(assetHubAgent, amount); + + (Command command, bytes memory params) = + makeLegacyUnlockTokenCommand(assetHubAgentID, address(0), recipient, amount); + + assertEq(assetHubAgent.balance, amount); + assertEq(recipient.balance, 0); + + // Expect the gateway to emit `InboundMessageDispatched` + vm.expectEmit(); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + + assertEq(assetHubAgent.balance, 0); + assertEq(recipient.balance, amount); + } + + function testUnlockEthHappyPath() public { + address recipient = makeAddr("test_recipeint"); + uint128 amount = 1; + + deal(assetHubAgent, amount); + + (Command command, bytes memory params) = makeUnlockTokenCommand(assetHubAgentID, address(0), recipient, amount); + + assertEq(assetHubAgent.balance, amount); + assertEq(recipient.balance, 0); + + // Expect the gateway to emit `InboundMessageDispatched` + vm.expectEmit(true, false, false, true); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + + assertEq(assetHubAgent.balance, 0); + assertEq(recipient.balance, amount); + } + function testSubmitFailInvalidNonce() public { (Command command, bytes memory params) = makeCreateAgentCommand(); From 5aad78d79edbca12a7f3c9b0bff86917974cd0c4 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 00:58:11 +0200 Subject: [PATCH 08/35] allow gateway proxy to receive funds --- contracts/src/Assets.sol | 2 ++ contracts/src/GatewayProxy.sol | 7 ++----- contracts/test/Gateway.t.sol | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index df9ec9642b..b5ce6831b0 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -319,8 +319,10 @@ library Assets { { bytes memory call; if (token != address(0)) { + // ERC20 call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount)); } else { + // Native ETH call = abi.encodeCall(AgentExecutor.transferNative, (payable(recipient), amount)); } (bool success,) = Agent(payable(agent)).invoke(executor, call); diff --git a/contracts/src/GatewayProxy.sol b/contracts/src/GatewayProxy.sol index a6a738e923..e3a58f0380 100644 --- a/contracts/src/GatewayProxy.sol +++ b/contracts/src/GatewayProxy.sol @@ -37,9 +37,6 @@ contract GatewayProxy is IInitializable { } } - // Prevent users from unwittingly sending ether to the gateway, as these funds - // would otherwise be lost forever. - receive() external payable { - revert NativeCurrencyNotAccepted(); - } + // Allow the Gateway proxy to receive ether in order to pay out rewards and refunds + receive() external payable {} } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 4f4485099b..c7475c3c81 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -24,7 +24,7 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {MultiAddress} from "../src/MultiAddress.sol"; import {Channel, InboundMessage, OperatingMode, ParaID, Command, ChannelID, MultiAddress} from "../src/Types.sol"; -import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; +import {SafeNativeTransfer} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {TokenLib} from "../src/TokenLib.sol"; @@ -394,6 +394,19 @@ contract GatewayTest is Test { * Fees & Rewards */ + // Test that the Gateway Proxy can receive funds to act as a wallet to pay out rewards and refunds + function testGatewayProxyCanRecieveFunds() public { + uint256 amount = 1 ether; + address deployer = makeAddr("deployer"); + hoax(deployer, amount); + + assertEq(address(gateway).balance, 0); + + SafeNativeTransfer.safeNativeTransfer(payable(gateway), amount); + + assertEq(address(gateway).balance, amount); + } + // Message relayer should be rewarded from the agent for a channel function testRelayerRewardedFromGateway() public { (Command command, bytes memory params) = makeCreateAgentCommand(); From dfcfb6c97f74291f39db3c77d523e44a3fcf382f Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 01:51:00 +0200 Subject: [PATCH 09/35] final tests and fixes --- contracts/src/Assets.sol | 2 +- contracts/test/Gateway.t.sol | 74 ++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index b5ce6831b0..c54ac08b2c 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -63,7 +63,7 @@ library Assets { ) external view returns (Costs memory costs) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); TokenInfo storage info = $.tokenRegistry[token]; - if (!info.isRegistered) { + if (!info.isRegistered && token != address(0)) { revert TokenNotRegistered(); } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index c7475c3c81..41110d5619 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -163,7 +163,7 @@ contract GatewayTest is Test { function makeLegacyUnlockTokenCommand(bytes32 agentID, address token_, address recipient, uint128 amount) public - view + pure returns (Command, bytes memory) { bytes memory payload = abi.encode(token_, recipient, amount); @@ -174,7 +174,7 @@ contract GatewayTest is Test { function makeUnlockTokenCommand(bytes32 agentID, address token_, address recipient, uint128 amount) public - view + pure returns (Command, bytes memory) { TransferNativeTokenParams memory params = @@ -459,8 +459,7 @@ contract GatewayTest is Test { assertEq(relayer.balance, 1 ether); } - // Users should pay fees to send outbound messages - function testUserPaysFees() public { + function testSendingWethWithFeeSucceeds() public { // Create a mock user address user = makeAddr("user"); deal(address(token), user, 1); @@ -481,6 +480,73 @@ contract GatewayTest is Test { assertEq(user.balance, 0); } + function testSendingEthWithAmountAndFeeSucceeds() public { + // Create a mock user + address user = makeAddr("user"); + uint128 amount = 1; + ParaID paraID = ParaID.wrap(1000); + + uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, 1)); + + vm.expectEmit(); + emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); + vm.expectEmit(true, false, false, false); + emit IGateway.OutboundMessageAccepted(paraID.into(), 1, messageID, hex""); + hoax(user, amount + fee); + IGateway(address(gateway)).sendToken{value: amount + fee}(address(0), paraID, recipientAddress32, 1, amount); + + assertEq(user.balance, 0); + } + + function testSendingEthWithAmountFeeAndExtraSucceedsWithRefund() public { + // Create a mock user + address user = makeAddr("user"); + uint128 amount = 1 ether; + uint128 extra = 2 ether; + ParaID paraID = ParaID.wrap(1000); + + uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, 1)); + + vm.expectEmit(); + emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); + vm.expectEmit(true, false, false, false); + emit IGateway.OutboundMessageAccepted(paraID.into(), 1, messageID, hex""); + hoax(user, amount + fee + extra); + IGateway(address(gateway)).sendToken{value: amount + fee + extra}( + address(0), paraID, recipientAddress32, 1, amount + ); + + assertEq(user.balance, extra); + } + + function testSendingEthWithoutFeeFails() public { + // Create a mock user + address user = makeAddr("user"); + uint128 amount = 1; + ParaID paraID = ParaID.wrap(1000); + + uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, 1)); + + vm.expectEmit(); + emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); + vm.expectRevert(Gateway.FeePaymentTooLow.selector); + hoax(user, amount + fee); + IGateway(address(gateway)).sendToken{value: amount}(address(0), paraID, recipientAddress32, 1, amount); + } + + function testSendingEthWithoutAmountFails() public { + // Create a mock user + address user = makeAddr("user"); + uint128 amount = 1 ether; + ParaID paraID = ParaID.wrap(1000); + + uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, amount)); + + vm.expectRevert(Gateway.TokenAmountTooLow.selector); + hoax(user, amount + fee); + IGateway(address(gateway)).sendToken{value: amount - 1}(address(0), paraID, recipientAddress32, 1, amount); + } + // User doesn't have enough funds to send message function testUserDoesNotProvideEnoughFees() public { // register token first From 3c0d826499931eeb679bb5ef873caf13e5f1bb42 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 01:51:21 +0200 Subject: [PATCH 10/35] warnings --- contracts/test/Bitfield.t.sol | 1 - contracts/test/mocks/BitfieldWrapper.sol | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/Bitfield.t.sol b/contracts/test/Bitfield.t.sol index d0713a3c81..7409f98e34 100644 --- a/contracts/test/Bitfield.t.sol +++ b/contracts/test/Bitfield.t.sol @@ -16,7 +16,6 @@ contract BitfieldTest is Test { string memory json = vm.readFile(string.concat(vm.projectRoot(), "/test/data/beefy-validator-set.json")); uint32 setSize = uint32(json.readUint(".validatorSetSize")); - bytes32 root = json.readBytes32(".validatorRoot"); uint256[] memory bitSetArray = json.readUintArray(".participants"); uint256[] memory initialBitField = bw.createBitfield(bitSetArray, setSize); diff --git a/contracts/test/mocks/BitfieldWrapper.sol b/contracts/test/mocks/BitfieldWrapper.sol index 523b2edae2..9993dc8b0a 100644 --- a/contracts/test/mocks/BitfieldWrapper.sol +++ b/contracts/test/mocks/BitfieldWrapper.sol @@ -14,6 +14,7 @@ contract BitfieldWrapper { function subsample(uint256 seed, uint256[] memory prior, uint256 n, uint256 length) public + pure returns (uint256[] memory bitfield) { return Bitfield.subsample(seed, prior, n, length); From 12124592be8c31254239c63080ab68acd01ad6e4 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 18 Dec 2024 22:53:49 +0200 Subject: [PATCH 11/35] PR feedback --- contracts/src/Assets.sol | 8 ++++---- contracts/src/Gateway.sol | 4 ++-- contracts/src/Types.sol | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index c54ac08b2c..7a599bc557 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -152,14 +152,14 @@ library Assets { if (token != address(0)) { // ERC20 _transferToAgent($.assetHubAgent, token, sender, amount); - ticket.etherAmount = 0; + ticket.value = 0; } else { // Native ETH if (msg.value < amount) { revert TokenAmountTooLow(); } payable($.assetHubAgent).safeNativeTransfer(amount); - ticket.etherAmount = amount; + ticket.value = amount; } ticket.dest = $.assetHubParaID; @@ -227,7 +227,7 @@ library Assets { ticket.dest = $.assetHubParaID; ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); - ticket.etherAmount = 0; + ticket.value = 0; // Construct a message payload if (destinationChain == $.assetHubParaID && destinationAddress.isAddress32()) { @@ -279,7 +279,7 @@ library Assets { ticket.dest = $.assetHubParaID; ticket.costs = _registerTokenCosts(); ticket.payload = SubstrateTypes.RegisterToken(token, $.assetHubCreateAssetFee); - ticket.etherAmount = 0; + ticket.value = 0; emit IGateway.TokenRegistrationSent(token); } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index e72174f779..bf3bb42ae6 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -549,7 +549,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { uint256 fee = _calculateFee(ticket.costs); // Ensure the user has enough funds for this message to be accepted - uint256 totalEther = fee + ticket.etherAmount; + uint256 totalEther = fee + ticket.value; if (msg.value < totalEther) { revert FeePaymentTooLow(); } @@ -558,7 +558,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // The fee is already collected into the gateway contract // Reimburse excess fee payment - if (msg.value > totalEther) { + if (msg.value > totalEther && (msg.value - totalEther) > _dustThreshold()) { payable(msg.sender).safeNativeTransfer(msg.value - totalEther); } diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index e9ae67a845..7986eaeacf 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -107,7 +107,7 @@ struct Ticket { ParaID dest; Costs costs; bytes payload; - uint128 etherAmount; + uint128 value; } struct TokenInfo { From e75906ace71623c0aa5c209fd458e9d88f8a39fc Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 19 Dec 2024 01:08:26 +0200 Subject: [PATCH 12/35] migrate ether on upgrade --- contracts/src/Agent.sol | 3 +-- contracts/src/AgentExecutor.sol | 7 ++++++- contracts/src/Gateway.sol | 11 ++++++++++ contracts/src/GatewayProxy.sol | 7 +++++-- contracts/src/interfaces/IGateway.sol | 10 +++++++++ contracts/src/storage/CoreStorage.sol | 2 ++ contracts/src/upgrades/Gateway202410.sol | 15 ++++++++++++- ...kUpgrade.t.sol => ForkUpgrade202410.t.sol} | 21 +++++++++++++++++-- contracts/test/Gateway.t.sol | 3 +++ 9 files changed, 71 insertions(+), 8 deletions(-) rename contracts/test/{ForkUpgrade.t.sol => ForkUpgrade202410.t.sol} (78%) diff --git a/contracts/src/Agent.sol b/contracts/src/Agent.sol index 25bb014e35..01e89b2c78 100644 --- a/contracts/src/Agent.sol +++ b/contracts/src/Agent.sol @@ -20,8 +20,7 @@ contract Agent { } /// @dev Agents can receive ether permissionlessly. - /// This is important, as agents for top-level parachains also act as sovereign accounts from which message relayers - /// are rewarded. + /// This is important, as agents are used to lock ether. receive() external payable {} /// @dev Allow the gateway to invoke some code within the context of this agent diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index f201ab3cc8..654d712f1c 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -6,6 +6,7 @@ import {AgentExecuteCommand, ParaID} from "./Types.sol"; import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; +import {IGateway} from "./interfaces/IGateway.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. @@ -16,11 +17,15 @@ contract AgentExecutor { /// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`, /// as the gateway needs to control an agent's ether balance directly. - /// function transferNative(address payable recipient, uint256 amount) external { recipient.safeNativeTransfer(amount); } + /// @dev Transfer ether to Gateway. Used once off for migration purposes. Can be removed after version 1. + function transferNativeToGateway(address payable gateway, uint256 amount) external { + IGateway(gateway).depositEther{value: amount}(); + } + /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. function transferToken(address token, address recipient, uint128 amount) external { _transferToken(token, recipient, amount); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index bf3bb42ae6..d304fc1dd0 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -299,6 +299,17 @@ contract Gateway is IGateway, IInitializable, IUpgradable { return ERC1967.load(); } + function version() public view returns (uint64) { + return CoreStorage.layout().version; + } + + /** + * Fee management + */ + function depositEther() external payable { + emit EtherDeposited(msg.sender, msg.value); + } + /** * Handlers */ diff --git a/contracts/src/GatewayProxy.sol b/contracts/src/GatewayProxy.sol index e3a58f0380..a6a738e923 100644 --- a/contracts/src/GatewayProxy.sol +++ b/contracts/src/GatewayProxy.sol @@ -37,6 +37,9 @@ contract GatewayProxy is IInitializable { } } - // Allow the Gateway proxy to receive ether in order to pay out rewards and refunds - receive() external payable {} + // Prevent users from unwittingly sending ether to the gateway, as these funds + // would otherwise be lost forever. + receive() external payable { + revert NativeCurrencyNotAccepted(); + } } diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index b2703d3144..7cfaf5e5f5 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -38,6 +38,9 @@ interface IGateway { // Emitted when foreign token from polkadot registed event ForeignTokenRegistered(bytes32 indexed tokenID, address token); + // Emitted when ether is deposited + event EtherDeposited(address who, uint256 amount); + /** * Getters */ @@ -53,6 +56,13 @@ interface IGateway { function implementation() external view returns (address); + function version() external view returns (uint64); + + /** + * Fee management + */ + function depositEther() external payable; + /** * Messaging */ diff --git a/contracts/src/storage/CoreStorage.sol b/contracts/src/storage/CoreStorage.sol index 35e6ec03c2..a8a7931b1b 100644 --- a/contracts/src/storage/CoreStorage.sol +++ b/contracts/src/storage/CoreStorage.sol @@ -14,6 +14,8 @@ library CoreStorage { mapping(bytes32 agentID => address) agents; // Agent addresses mapping(address agent => bytes32 agentID) agentAddresses; + // Version of the Gateway Implementation + uint64 version; } bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.core"); diff --git a/contracts/src/upgrades/Gateway202410.sol b/contracts/src/upgrades/Gateway202410.sol index 655ee06c87..084fc13b61 100644 --- a/contracts/src/upgrades/Gateway202410.sol +++ b/contracts/src/upgrades/Gateway202410.sol @@ -25,10 +25,23 @@ contract Gateway202410 is Gateway { {} // Override parent initializer to prevent re-initialization of storage. - function initialize(bytes memory) external view override { + function initialize(bytes memory) external override { // Ensure that arbitrary users cannot initialize storage in this logic contract. if (ERC1967.load() == address(0)) { revert Unauthorized(); } + + // We expect version 0, deploying version 1. + CoreStorage.Layout storage $ = CoreStorage.layout(); + if ($.version != 0) { + revert Unauthorized(); + } + $.version = 1; + + // migrate asset hub agent + address agent = _ensureAgent(hex"81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79"); + bytes memory call = + abi.encodeCall(AgentExecutor.transferNativeToGateway, (payable(address(this)), agent.balance)); + _invokeOnAgent(agent, call); } } diff --git a/contracts/test/ForkUpgrade.t.sol b/contracts/test/ForkUpgrade202410.t.sol similarity index 78% rename from contracts/test/ForkUpgrade.t.sol rename to contracts/test/ForkUpgrade202410.t.sol index 226d7f0621..d23a6f7696 100644 --- a/contracts/test/ForkUpgrade.t.sol +++ b/contracts/test/ForkUpgrade202410.t.sol @@ -16,6 +16,7 @@ contract ForkUpgradeTest is Test { address private constant GatewayProxy = 0x27ca963C279c93801941e1eB8799c23f407d68e7; address private constant BeefyClient = 0x6eD05bAa904df3DE117EcFa638d4CB84e1B8A00C; bytes32 private constant BridgeHubAgent = 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314; + bytes32 private constant AssetHubAgent = 0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79; function setUp() public { vm.createSelectFork("https://rpc.tenderly.co/fork/b77e07b8-ad6d-4e83-b5be-30a2001964aa", 20645700); @@ -33,10 +34,20 @@ contract ForkUpgradeTest is Test { UpgradeParams memory params = UpgradeParams({impl: address(newLogic), implCodeHash: address(newLogic).codehash, initParams: bytes("")}); - vm.expectEmit(true, false, false, false); + Gateway gateway = Gateway(GatewayProxy); + + // Check pre-migration of ETH from Asset Hub agent + assertGt(IGateway(GatewayProxy).agentOf(AssetHubAgent).balance, 0); + // Check pre-migration of ETH to Gateway + assertEq(address(GatewayProxy).balance, 0); + + vm.expectEmit(); + emit IGateway.EtherDeposited(gateway.agentOf(AssetHubAgent), 587928061927368450); + + vm.expectEmit(); emit IUpgradable.Upgraded(address(newLogic)); - Gateway(GatewayProxy).upgrade(abi.encode(params)); + gateway.upgrade(abi.encode(params)); } function checkLegacyToken() public { @@ -60,6 +71,12 @@ contract ForkUpgradeTest is Test { } function testSanityCheck() public { + // Check that the version is correctly set. + assertEq(IGateway(GatewayProxy).version(), 1); + // Check migration of ETH from Asset Hub agent + assertEq(IGateway(GatewayProxy).agentOf(AssetHubAgent).balance, 0); + // Check migration of ETH to Gateway + assertGt(address(GatewayProxy).balance, 0); // Check AH channel nonces as expected (uint64 inbound, uint64 outbound) = IGateway(GatewayProxy).channelNoncesOf( ChannelID.wrap(0xc173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 41110d5619..2b871289db 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -402,8 +402,11 @@ contract GatewayTest is Test { assertEq(address(gateway).balance, 0); + vm.expectRevert(GatewayProxy.NativeCurrencyNotAccepted.selector); SafeNativeTransfer.safeNativeTransfer(payable(gateway), amount); + IGateway(address(gateway)).depositEther{value: amount}(); + assertEq(address(gateway).balance, amount); } From 182d6ed83927304b1ea983faf3eb0cef5ba860f5 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 19 Dec 2024 01:33:37 +0200 Subject: [PATCH 13/35] fix scripts --- contracts/scripts/DeployLocal.sol | 2 +- contracts/scripts/FundGateway.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/scripts/DeployLocal.sol b/contracts/scripts/DeployLocal.sol index c2eba56e67..d7529a90ad 100644 --- a/contracts/scripts/DeployLocal.sol +++ b/contracts/scripts/DeployLocal.sol @@ -100,7 +100,7 @@ contract DeployLocal is Script { // of messages originating from BridgeHub uint256 initialDeposit = vm.envUint("GATEWAY_PROXY_INITIAL_DEPOSIT"); - payable(gateway).safeNativeTransfer(initialDeposit); + IGateway(address(gateway)).depositEther{value: initialDeposit}(); // Deploy MockGatewayV2 for testing new MockGatewayV2(); diff --git a/contracts/scripts/FundGateway.sol b/contracts/scripts/FundGateway.sol index 89e021fb9b..26c2dda979 100644 --- a/contracts/scripts/FundGateway.sol +++ b/contracts/scripts/FundGateway.sol @@ -29,7 +29,7 @@ contract FundGateway is Script { uint256 initialDeposit = vm.envUint("GATEWAY_PROXY_INITIAL_DEPOSIT"); address gatewayAddress = vm.envAddress("GATEWAY_PROXY_CONTRACT"); - payable(gatewayAddress).safeNativeTransfer(initialDeposit); + IGateway(address(gatewayAddress)).depositEther{value: initialDeposit}(); vm.stopBroadcast(); } From 61c52b0027224fb4660db70e304056fb39ea7490 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 19 Dec 2024 01:36:33 +0200 Subject: [PATCH 14/35] update bindings --- relayer/contracts/gateway.go | 189 ++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/relayer/contracts/gateway.go b/relayer/contracts/gateway.go index 1a4eba075a..a81d5266ed 100644 --- a/relayer/contracts/gateway.go +++ b/relayer/contracts/gateway.go @@ -91,7 +91,7 @@ type VerificationProof struct { // GatewayMetaData contains all meta data concerning the Gateway contract. var GatewayMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"function\",\"name\":\"agentOf\",\"inputs\":[{\"name\":\"agentID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"channelNoncesOf\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"internalType\":\"ChannelID\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"channelOperatingModeOf\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"internalType\":\"ChannelID\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumOperatingMode\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"implementation\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isTokenRegistered\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"operatingMode\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumOperatingMode\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"pricingParameters\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"UD60x18\"},{\"name\":\"\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"queryForeignTokenID\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"quoteRegisterTokenFee\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"quoteSendTokenFee\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"destinationChain\",\"type\":\"uint32\",\"internalType\":\"ParaID\"},{\"name\":\"destinationFee\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"registerToken\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"sendToken\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"destinationChain\",\"type\":\"uint32\",\"internalType\":\"ParaID\"},{\"name\":\"destinationAddress\",\"type\":\"tuple\",\"internalType\":\"structMultiAddress\",\"components\":[{\"name\":\"kind\",\"type\":\"uint8\",\"internalType\":\"enumKind\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"destinationFee\",\"type\":\"uint128\",\"internalType\":\"uint128\"},{\"name\":\"amount\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"submitV1\",\"inputs\":[{\"name\":\"message\",\"type\":\"tuple\",\"internalType\":\"structInboundMessage\",\"components\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"internalType\":\"ChannelID\"},{\"name\":\"nonce\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"command\",\"type\":\"uint8\",\"internalType\":\"enumCommand\"},{\"name\":\"params\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"maxDispatchGas\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"maxFeePerGas\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"reward\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"id\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"name\":\"leafProof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"},{\"name\":\"headerProof\",\"type\":\"tuple\",\"internalType\":\"structVerification.Proof\",\"components\":[{\"name\":\"header\",\"type\":\"tuple\",\"internalType\":\"structVerification.ParachainHeader\",\"components\":[{\"name\":\"parentHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"number\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"stateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"extrinsicsRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"digestItems\",\"type\":\"tuple[]\",\"internalType\":\"structVerification.DigestItem[]\",\"components\":[{\"name\":\"kind\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"consensusEngineID\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]}]},{\"name\":\"headProof\",\"type\":\"tuple\",\"internalType\":\"structVerification.HeadProof\",\"components\":[{\"name\":\"pos\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"width\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"proof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"}]},{\"name\":\"leafPartial\",\"type\":\"tuple\",\"internalType\":\"structVerification.MMRLeafPartial\",\"components\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentNumber\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"parentHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"nextAuthoritySetID\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"nextAuthoritySetLen\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"nextAuthoritySetRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"name\":\"leafProof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"},{\"name\":\"leafProofOrder\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"AgentCreated\",\"inputs\":[{\"name\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"agent\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"AgentFundsWithdrawn\",\"inputs\":[{\"name\":\"agentID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"recipient\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ChannelCreated\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ChannelUpdated\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ForeignTokenRegistered\",\"inputs\":[{\"name\":\"tokenID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"token\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"InboundMessageDispatched\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"},{\"name\":\"nonce\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"},{\"name\":\"messageID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"success\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OperatingModeChanged\",\"inputs\":[{\"name\":\"mode\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"enumOperatingMode\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OutboundMessageAccepted\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"},{\"name\":\"nonce\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"},{\"name\":\"messageID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"payload\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"PricingParametersChanged\",\"inputs\":[],\"anonymous\":false},{\"type\":\"event\",\"name\":\"TokenRegistrationSent\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"TokenSent\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"destinationChain\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"ParaID\"},{\"name\":\"destinationAddress\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structMultiAddress\",\"components\":[{\"name\":\"kind\",\"type\":\"uint8\",\"internalType\":\"enumKind\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"amount\",\"type\":\"uint128\",\"indexed\":false,\"internalType\":\"uint128\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"TokenTransferFeesChanged\",\"inputs\":[],\"anonymous\":false}]", + ABI: "[{\"type\":\"function\",\"name\":\"agentOf\",\"inputs\":[{\"name\":\"agentID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"channelNoncesOf\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"internalType\":\"ChannelID\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"channelOperatingModeOf\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"internalType\":\"ChannelID\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumOperatingMode\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"depositEther\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"implementation\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isTokenRegistered\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"operatingMode\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumOperatingMode\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"pricingParameters\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"UD60x18\"},{\"name\":\"\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"queryForeignTokenID\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"quoteRegisterTokenFee\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"quoteSendTokenFee\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"destinationChain\",\"type\":\"uint32\",\"internalType\":\"ParaID\"},{\"name\":\"destinationFee\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"registerToken\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"sendToken\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"destinationChain\",\"type\":\"uint32\",\"internalType\":\"ParaID\"},{\"name\":\"destinationAddress\",\"type\":\"tuple\",\"internalType\":\"structMultiAddress\",\"components\":[{\"name\":\"kind\",\"type\":\"uint8\",\"internalType\":\"enumKind\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"destinationFee\",\"type\":\"uint128\",\"internalType\":\"uint128\"},{\"name\":\"amount\",\"type\":\"uint128\",\"internalType\":\"uint128\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"submitV1\",\"inputs\":[{\"name\":\"message\",\"type\":\"tuple\",\"internalType\":\"structInboundMessage\",\"components\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"internalType\":\"ChannelID\"},{\"name\":\"nonce\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"command\",\"type\":\"uint8\",\"internalType\":\"enumCommand\"},{\"name\":\"params\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"maxDispatchGas\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"maxFeePerGas\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"reward\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"id\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"name\":\"leafProof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"},{\"name\":\"headerProof\",\"type\":\"tuple\",\"internalType\":\"structVerification.Proof\",\"components\":[{\"name\":\"header\",\"type\":\"tuple\",\"internalType\":\"structVerification.ParachainHeader\",\"components\":[{\"name\":\"parentHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"number\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"stateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"extrinsicsRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"digestItems\",\"type\":\"tuple[]\",\"internalType\":\"structVerification.DigestItem[]\",\"components\":[{\"name\":\"kind\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"consensusEngineID\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]}]},{\"name\":\"headProof\",\"type\":\"tuple\",\"internalType\":\"structVerification.HeadProof\",\"components\":[{\"name\":\"pos\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"width\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"proof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"}]},{\"name\":\"leafPartial\",\"type\":\"tuple\",\"internalType\":\"structVerification.MMRLeafPartial\",\"components\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentNumber\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"parentHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"nextAuthoritySetID\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"nextAuthoritySetLen\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"nextAuthoritySetRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"name\":\"leafProof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"},{\"name\":\"leafProofOrder\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"version\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"AgentCreated\",\"inputs\":[{\"name\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"agent\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"AgentFundsWithdrawn\",\"inputs\":[{\"name\":\"agentID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"recipient\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ChannelCreated\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ChannelUpdated\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"EtherDeposited\",\"inputs\":[{\"name\":\"who\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ForeignTokenRegistered\",\"inputs\":[{\"name\":\"tokenID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"token\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"InboundMessageDispatched\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"},{\"name\":\"nonce\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"},{\"name\":\"messageID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"success\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OperatingModeChanged\",\"inputs\":[{\"name\":\"mode\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"enumOperatingMode\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OutboundMessageAccepted\",\"inputs\":[{\"name\":\"channelID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"ChannelID\"},{\"name\":\"nonce\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"},{\"name\":\"messageID\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"payload\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"PricingParametersChanged\",\"inputs\":[],\"anonymous\":false},{\"type\":\"event\",\"name\":\"TokenRegistrationSent\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"TokenSent\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"destinationChain\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"ParaID\"},{\"name\":\"destinationAddress\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structMultiAddress\",\"components\":[{\"name\":\"kind\",\"type\":\"uint8\",\"internalType\":\"enumKind\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"amount\",\"type\":\"uint128\",\"indexed\":false,\"internalType\":\"uint128\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"TokenTransferFeesChanged\",\"inputs\":[],\"anonymous\":false}]", } // GatewayABI is the input ABI used to generate the binding from. @@ -552,6 +552,58 @@ func (_Gateway *GatewayCallerSession) QuoteSendTokenFee(token common.Address, de return _Gateway.Contract.QuoteSendTokenFee(&_Gateway.CallOpts, token, destinationChain, destinationFee) } +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(uint64) +func (_Gateway *GatewayCaller) Version(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _Gateway.contract.Call(opts, &out, "version") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(uint64) +func (_Gateway *GatewaySession) Version() (uint64, error) { + return _Gateway.Contract.Version(&_Gateway.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(uint64) +func (_Gateway *GatewayCallerSession) Version() (uint64, error) { + return _Gateway.Contract.Version(&_Gateway.CallOpts) +} + +// DepositEther is a paid mutator transaction binding the contract method 0x98ea5fca. +// +// Solidity: function depositEther() payable returns() +func (_Gateway *GatewayTransactor) DepositEther(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Gateway.contract.Transact(opts, "depositEther") +} + +// DepositEther is a paid mutator transaction binding the contract method 0x98ea5fca. +// +// Solidity: function depositEther() payable returns() +func (_Gateway *GatewaySession) DepositEther() (*types.Transaction, error) { + return _Gateway.Contract.DepositEther(&_Gateway.TransactOpts) +} + +// DepositEther is a paid mutator transaction binding the contract method 0x98ea5fca. +// +// Solidity: function depositEther() payable returns() +func (_Gateway *GatewayTransactorSession) DepositEther() (*types.Transaction, error) { + return _Gateway.Contract.DepositEther(&_Gateway.TransactOpts) +} + // RegisterToken is a paid mutator transaction binding the contract method 0x09824a80. // // Solidity: function registerToken(address token) payable returns() @@ -1192,6 +1244,141 @@ func (_Gateway *GatewayFilterer) ParseChannelUpdated(log types.Log) (*GatewayCha return event, nil } +// GatewayEtherDepositedIterator is returned from FilterEtherDeposited and is used to iterate over the raw logs and unpacked data for EtherDeposited events raised by the Gateway contract. +type GatewayEtherDepositedIterator struct { + Event *GatewayEtherDeposited // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *GatewayEtherDepositedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(GatewayEtherDeposited) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(GatewayEtherDeposited) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *GatewayEtherDepositedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *GatewayEtherDepositedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// GatewayEtherDeposited represents a EtherDeposited event raised by the Gateway contract. +type GatewayEtherDeposited struct { + Who common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterEtherDeposited is a free log retrieval operation binding the contract event 0x939e51ac2fd009b158d6344f7e68a83d8d18d9b0cc88cf514aac6aaa9cad2a18. +// +// Solidity: event EtherDeposited(address who, uint256 amount) +func (_Gateway *GatewayFilterer) FilterEtherDeposited(opts *bind.FilterOpts) (*GatewayEtherDepositedIterator, error) { + + logs, sub, err := _Gateway.contract.FilterLogs(opts, "EtherDeposited") + if err != nil { + return nil, err + } + return &GatewayEtherDepositedIterator{contract: _Gateway.contract, event: "EtherDeposited", logs: logs, sub: sub}, nil +} + +// WatchEtherDeposited is a free log subscription operation binding the contract event 0x939e51ac2fd009b158d6344f7e68a83d8d18d9b0cc88cf514aac6aaa9cad2a18. +// +// Solidity: event EtherDeposited(address who, uint256 amount) +func (_Gateway *GatewayFilterer) WatchEtherDeposited(opts *bind.WatchOpts, sink chan<- *GatewayEtherDeposited) (event.Subscription, error) { + + logs, sub, err := _Gateway.contract.WatchLogs(opts, "EtherDeposited") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(GatewayEtherDeposited) + if err := _Gateway.contract.UnpackLog(event, "EtherDeposited", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseEtherDeposited is a log parse operation binding the contract event 0x939e51ac2fd009b158d6344f7e68a83d8d18d9b0cc88cf514aac6aaa9cad2a18. +// +// Solidity: event EtherDeposited(address who, uint256 amount) +func (_Gateway *GatewayFilterer) ParseEtherDeposited(log types.Log) (*GatewayEtherDeposited, error) { + event := new(GatewayEtherDeposited) + if err := _Gateway.contract.UnpackLog(event, "EtherDeposited", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // GatewayForeignTokenRegisteredIterator is returned from FilterForeignTokenRegistered and is used to iterate over the raw logs and unpacked data for ForeignTokenRegistered events raised by the Gateway contract. type GatewayForeignTokenRegisteredIterator struct { Event *GatewayForeignTokenRegistered // Event containing the contract specifics and raw log From db453759bccc92179cef0d6850198ae2977e4e03 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 19 Dec 2024 03:50:15 +0200 Subject: [PATCH 15/35] e2e tests --- smoketest/run-tests.sh | 4 + smoketest/tests/send_native_eth.rs | 106 +++++++++++++++ smoketest/tests/transfer_native_eth.rs | 127 ++++++++++++++++++ .../test/scripts/configure-substrate.sh | 8 ++ 4 files changed, 245 insertions(+) create mode 100644 smoketest/tests/send_native_eth.rs create mode 100644 smoketest/tests/transfer_native_eth.rs diff --git a/smoketest/run-tests.sh b/smoketest/run-tests.sh index 58158ade7f..5a905a06a3 100755 --- a/smoketest/run-tests.sh +++ b/smoketest/run-tests.sh @@ -5,6 +5,10 @@ set -xe cargo test --no-run tests=( + # Native ETH + send_native_eth + transfer_native_eth + # ERC20 Tests register_token send_token diff --git a/smoketest/tests/send_native_eth.rs b/smoketest/tests/send_native_eth.rs new file mode 100644 index 0000000000..37271af934 --- /dev/null +++ b/smoketest/tests/send_native_eth.rs @@ -0,0 +1,106 @@ +use ethers::{ + core::types::{Address, U256}, + utils::parse_units, +}; +use futures::StreamExt; +use snowbridge_smoketest::{ + constants::*, + contracts::i_gateway, + helper::{initial_clients, print_event_log_for_unit_tests}, + parachains::assethub::api::{ + foreign_assets::events::Issued, + runtime_types::{ + staging_xcm::v3::multilocation::MultiLocation, + xcm::v3::{ + junction::{Junction::GlobalConsensus, NetworkId}, + junctions::Junctions::X1, + }, + }, + }, +}; +use subxt::{ext::codec::Encode, utils::AccountId32}; + +#[tokio::test] +async fn send_native_eth() { + let test_clients = initial_clients().await.expect("initialize clients"); + let ethereum_client = *(test_clients.ethereum_signed_client.clone()); + let assethub = *(test_clients.asset_hub_client.clone()); + + let gateway_addr: Address = (*GATEWAY_PROXY_CONTRACT).into(); + let gateway = i_gateway::IGateway::new(gateway_addr, ethereum_client.clone()); + + let eth_address: Address = [0; 20].into(); + + let destination_fee = 0; + let fee = gateway + .quote_send_token_fee(eth_address, ASSET_HUB_PARA_ID, destination_fee) + .call() + .await + .unwrap(); + + let value = parse_units("1", "ether").unwrap(); + // Lock tokens into vault + let amount: u128 = U256::from(value).low_u128(); + let receipt = gateway + .send_token( + eth_address, + ASSET_HUB_PARA_ID, + i_gateway::MultiAddress { kind: 1, data: (*SUBSTRATE_RECEIVER).into() }, + destination_fee, + amount, + ) + .value(fee + amount) + .send() + .await + .unwrap() + .await + .unwrap() + .unwrap(); + + println!( + "receipt transaction hash: {:#?}, transaction block: {:#?}", + hex::encode(receipt.transaction_hash), + receipt.block_number + ); + + // Log for OutboundMessageAccepted + let outbound_message_accepted_log = receipt.logs.last().unwrap(); + + // print log for unit tests + print_event_log_for_unit_tests(outbound_message_accepted_log); + + assert_eq!(receipt.status.unwrap().as_u64(), 1u64); + + let wait_for_blocks = (*WAIT_PERIOD) as usize; + let mut blocks = assethub + .blocks() + .subscribe_finalized() + .await + .expect("block subscription") + .take(wait_for_blocks); + + let expected_asset_id: MultiLocation = MultiLocation { + parents: 2, + interior: X1(GlobalConsensus(NetworkId::Ethereum { chain_id: ETHEREUM_CHAIN_ID })), + }; + let expected_owner: AccountId32 = (*SUBSTRATE_RECEIVER).into(); + + let mut issued_event_found = false; + while let Some(Ok(block)) = blocks.next().await { + println!("Polling assethub block {} for issued event.", block.number()); + + let events = block.events().await.unwrap(); + for issued in events.find::() { + println!("Created event found in assethub block {}.", block.number()); + let issued = issued.unwrap(); + assert_eq!(issued.asset_id.encode(), expected_asset_id.encode()); + assert_eq!(issued.owner, expected_owner); + assert_eq!(issued.amount, amount); + issued_event_found = true; + } + if issued_event_found { + break + } + } + assert!(issued_event_found) +} diff --git a/smoketest/tests/transfer_native_eth.rs b/smoketest/tests/transfer_native_eth.rs new file mode 100644 index 0000000000..ebc6f0c824 --- /dev/null +++ b/smoketest/tests/transfer_native_eth.rs @@ -0,0 +1,127 @@ +use assethub::api::polkadot_xcm::calls::TransactionApi; +use ethers::{ + prelude::Middleware, + providers::{Provider, Ws}, + types::Address, +}; +use futures::StreamExt; +use snowbridge_smoketest::{ + constants::*, + contracts::i_gateway::{IGateway, InboundMessageDispatchedFilter}, + helper::AssetHubConfig, + parachains::assethub::{ + self, + api::{ + polkadot_xcm::events::Sent, + runtime_types::{ + staging_xcm::v3::multilocation::MultiLocation, + xcm::{ + v3::{ + junction::{Junction, NetworkId}, + junctions::Junctions, + multiasset::{AssetId, Fungibility, MultiAsset, MultiAssets}, + }, + VersionedAssets, VersionedLocation, + }, + }, + }, + }, +}; +use std::{str::FromStr, sync::Arc, time::Duration}; +use subxt::OnlineClient; +use subxt_signer::{sr25519, SecretUri}; + +#[tokio::test] +async fn transfer_native_eth() { + let ethereum_provider = Provider::::connect((*ETHEREUM_API).to_string()) + .await + .unwrap() + .interval(Duration::from_millis(10u64)); + + let ethereum_client = Arc::new(ethereum_provider); + + let gateway_addr: Address = (*GATEWAY_PROXY_CONTRACT).into(); + let gateway = IGateway::new(gateway_addr, ethereum_client.clone()); + + let assethub: OnlineClient = + OnlineClient::from_url((*ASSET_HUB_WS_URL).to_string()).await.unwrap(); + + let amount: u128 = 1_000_000_000; + let assets = VersionedAssets::V3(MultiAssets(vec![MultiAsset { + id: AssetId::Concrete(MultiLocation { + parents: 2, + interior: Junctions::X1(Junction::GlobalConsensus(NetworkId::Ethereum { + chain_id: ETHEREUM_CHAIN_ID, + })), + }), + fun: Fungibility::Fungible(amount), + }])); + + let destination = VersionedLocation::V3(MultiLocation { + parents: 2, + interior: Junctions::X1(Junction::GlobalConsensus(NetworkId::Ethereum { + chain_id: ETHEREUM_CHAIN_ID, + })), + }); + + let beneficiary = VersionedLocation::V3(MultiLocation { + parents: 0, + interior: Junctions::X1(Junction::AccountKey20 { + network: None, + key: (*ETHEREUM_RECEIVER).into(), + }), + }); + + let suri = SecretUri::from_str(&SUBSTRATE_KEY).expect("Parse SURI"); + + let signer = sr25519::Keypair::from_uri(&suri).expect("valid keypair"); + + let token_transfer_call = + TransactionApi.reserve_transfer_assets(destination, beneficiary, assets, 0); + + let events = assethub + .tx() + .sign_and_submit_then_watch_default(&token_transfer_call, &signer) + .await + .expect("call success") + .wait_for_finalized_success() + .await + .expect("sucessful call"); + + let message_id = events + .find_first::() + .expect("xcm sent") + .expect("xcm sent found") + .message_id; + + let receiver: Address = (*ETHEREUM_RECEIVER).into(); + let balance_before = ethereum_client.get_balance(receiver, None).await.expect("fetch balance"); + + let wait_for_blocks = 500; + let mut stream = ethereum_client.subscribe_blocks().await.unwrap().take(wait_for_blocks); + + let mut transfer_event_found = false; + while let Some(block) = stream.next().await { + println!("Polling ethereum block {:?} for transfer event", block.number.unwrap()); + if let Ok(transfers) = gateway + .event::() + .at_block_hash(block.hash.unwrap()) + .query() + .await + { + for transfer in transfers { + if transfer.message_id.eq(&message_id) { + println!("Transfer event found at ethereum block {:?}", block.number.unwrap()); + assert!(transfer.success, "delivered successfully"); + transfer_event_found = true; + } + } + } + if transfer_event_found { + break + } + } + assert!(transfer_event_found); + let balance_after = ethereum_client.get_balance(receiver, None).await.expect("fetch balance"); + assert_eq!(balance_before, balance_after + amount) +} diff --git a/web/packages/test/scripts/configure-substrate.sh b/web/packages/test/scripts/configure-substrate.sh index 582006fe8d..25641e999b 100755 --- a/web/packages/test/scripts/configure-substrate.sh +++ b/web/packages/test/scripts/configure-substrate.sh @@ -83,6 +83,13 @@ config_xcm_version() { send_governance_transact_from_relaychain $ASSET_HUB_PARAID "$call" } +register_native_eth() { + # Registers Eth and makes it sufficient + # https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:12144#/extrinsics/decode/0x3501020109079edaa80200ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d0104 + local call="0x3501020109079edaa80200ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d0104" + send_governance_transact_from_relaychain $ASSET_HUB_PARAID "$call" +} + configure_substrate() { set_gateway fund_accounts @@ -90,6 +97,7 @@ configure_substrate() { config_xcm_version wait_beacon_chain_ready config_beacon_checkpoint + register_native_eth } if [ -z "${from_start_services:-}" ]; then From a44d09378e1ec3b155fff9fb044edb8685a8a7a3 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 19 Dec 2024 03:52:27 +0200 Subject: [PATCH 16/35] fix assert --- smoketest/tests/transfer_native_eth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoketest/tests/transfer_native_eth.rs b/smoketest/tests/transfer_native_eth.rs index ebc6f0c824..871f95a481 100644 --- a/smoketest/tests/transfer_native_eth.rs +++ b/smoketest/tests/transfer_native_eth.rs @@ -123,5 +123,5 @@ async fn transfer_native_eth() { } assert!(transfer_event_found); let balance_after = ethereum_client.get_balance(receiver, None).await.expect("fetch balance"); - assert_eq!(balance_before, balance_after + amount) + assert_eq!(balance_before + amount, balance_after) } From 9b6739835d674ba94641c7238e735bc43de295b9 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 19 Dec 2024 11:55:49 +0200 Subject: [PATCH 17/35] duplicated error removal --- contracts/src/Assets.sol | 3 +-- contracts/src/Gateway.sol | 1 - contracts/test/Gateway.t.sol | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 7a599bc557..242c26e2ca 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -36,7 +36,6 @@ library Assets { error TokenAlreadyRegistered(); error TokenMintFailed(); error TokenTransferFailed(); - error TokenAmountTooLow(); function isTokenRegistered(address token) external view returns (bool) { return AssetsStorage.layout().tokenRegistry[token].isRegistered; @@ -156,7 +155,7 @@ library Assets { } else { // Native ETH if (msg.value < amount) { - revert TokenAmountTooLow(); + revert InvalidAmount(); } payable($.assetHubAgent).safeNativeTransfer(amount); ticket.value = amount; diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index d304fc1dd0..42247d896f 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -99,7 +99,6 @@ contract Gateway is IGateway, IInitializable, IUpgradable { error InvalidAgentExecutionPayload(); error InvalidConstructorParams(); error TokenNotRegistered(); - error TokenAmountTooLow(); // Message handlers can only be dispatched by the gateway itself modifier onlySelf() { diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 2b871289db..76d5925242 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -545,7 +545,7 @@ contract GatewayTest is Test { uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, amount)); - vm.expectRevert(Gateway.TokenAmountTooLow.selector); + vm.expectRevert(Assets.InvalidAmount.selector); hoax(user, amount + fee); IGateway(address(gateway)).sendToken{value: amount - 1}(address(0), paraID, recipientAddress32, 1, amount); } From 2b9173e61fa4c37d7b6086f873631ff983e8a9b6 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Fri, 20 Dec 2024 13:59:52 +0200 Subject: [PATCH 18/35] fix test fixture generator --- relayer/cmd/generate_beacon_data.go | 21 +- relayer/templates/beacon-fixtures.mustache | 270 ++++++++++++++++++++ relayer/templates/inbound-fixtures.mustache | 95 +++++++ 3 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 relayer/templates/beacon-fixtures.mustache create mode 100644 relayer/templates/inbound-fixtures.mustache diff --git a/relayer/cmd/generate_beacon_data.go b/relayer/cmd/generate_beacon_data.go index 06202d84e5..4e28887873 100644 --- a/relayer/cmd/generate_beacon_data.go +++ b/relayer/cmd/generate_beacon_data.go @@ -83,7 +83,7 @@ func generateInboundFixtureCmd() *cobra.Command { } cmd.Flags().String("beacon-config", "/tmp/snowbridge/beacon-relay.json", "Path to the beacon relay config") - cmd.Flags().String("execution-config", "/tmp/snowbridge/execution-relay-asset-hub.json", "Path to the beacon relay config") + cmd.Flags().String("execution-config", "/tmp/snowbridge/execution-relay-asset-hub-0.json", "Path to the beacon relay config") cmd.Flags().Uint32("nonce", 1, "Nonce of the inbound message") cmd.Flags().String("test_case", "register_token", "Inbound test case") return cmd @@ -206,7 +206,7 @@ func generateBeaconTestFixture(cmd *cobra.Command, _ []string) error { client := api.NewBeaconClient(conf.Source.Beacon.Endpoint, conf.Source.Beacon.StateEndpoint) s := syncer.New(client, &store, p) - viper.SetConfigFile("/tmp/snowbridge/execution-relay-asset-hub.json") + viper.SetConfigFile("/tmp/snowbridge/execution-relay-asset-hub-0.json") if err = viper.ReadInConfig(); err != nil { return err @@ -390,13 +390,16 @@ func generateBeaconTestFixture(cmd *cobra.Command, _ []string) error { log.WithFields(log.Fields{ "location": pathToInboundQueueFixtureData, }).Info("writing result file") - err = writeRawDataFile(fmt.Sprintf("%s", pathToInboundQueueFixtureData), rendered) + err = writeRawDataFile(pathToInboundQueueFixtureData, rendered) if err != nil { return err } // Generate test fixture in next period (require waiting a long time) waitUntilNextPeriod, err := cmd.Flags().GetBool("wait_until_next_period") + if err != nil { + return fmt.Errorf("could not parse flag wait_until_next_period: %w", err) + } if waitUntilNextPeriod { log.Info("waiting finalized_update in next period (5 hours later), be patient and wait...") for { @@ -537,11 +540,6 @@ func generateExecutionUpdate(cmd *cobra.Command, _ []string) error { return nil } -func generateInboundTestFixture(ctx context.Context, beaconEndpoint string) error { - - return nil -} - func getEthereumEvent(ctx context.Context, gatewayContract *contracts.Gateway, channelID executionConf.ChannelID, nonce uint32) (*contracts.GatewayOutboundMessageAccepted, error) { maxBlockNumber := uint64(10000) @@ -613,6 +611,9 @@ func getBeaconBlockContainingExecutionHeader(s syncer.Syncer, messageBlockNumber log.WithField("beaconHeaderSlot", beaconHeaderSlot).Info("getting beacon block by slot") beaconBlock, blockNumber, err = getBeaconBlockAndBlockNumber(s, beaconHeaderSlot) + if err != nil { + return api.BeaconBlockResponse{}, 0, err + } } return beaconBlock, blockNumber, nil @@ -813,7 +814,7 @@ func generateInboundFixture(cmd *cobra.Command, _ []string) error { if err != nil { return err } - if testCase != "register_token" && testCase != "send_token" && testCase != "send_token_to_penpal" { + if testCase != "register_token" && testCase != "send_token" && testCase != "send_token_to_penpal" && testCase != "send_native_eth" { return fmt.Errorf("invalid test case: %s", testCase) } @@ -830,7 +831,7 @@ func generateInboundFixture(cmd *cobra.Command, _ []string) error { } pathToInboundQueueFixtureTestCaseData := fmt.Sprintf(pathToInboundQueueFixtureTestCaseData, testCase) - err = writeRawDataFile(fmt.Sprintf("%s", pathToInboundQueueFixtureTestCaseData), rendered) + err = writeRawDataFile(pathToInboundQueueFixtureTestCaseData, rendered) if err != nil { return err } diff --git a/relayer/templates/beacon-fixtures.mustache b/relayer/templates/beacon-fixtures.mustache new file mode 100644 index 0000000000..5942be0563 --- /dev/null +++ b/relayer/templates/beacon-fixtures.mustache @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +// Generated, do not edit! +// See README.md for instructions to generate +#![cfg_attr(not(feature = "std"), no_std)] + +use hex_literal::hex; +use snowbridge_beacon_primitives::{ + types::deneb, AncestryProof, BeaconHeader, ExecutionProof, NextSyncCommitteeUpdate, + SyncAggregate, SyncCommittee, VersionedExecutionPayloadHeader, +}; +use snowbridge_core::inbound::{InboundQueueFixture, Log, Message, Proof}; +use sp_core::U256; +use sp_std::{boxed::Box, vec}; + +const SC_SIZE: usize = 512; +const SC_BITS_SIZE: usize = 64; +type CheckpointUpdate = snowbridge_beacon_primitives::CheckpointUpdate; +type Update = snowbridge_beacon_primitives::Update; + + +pub fn make_checkpoint() -> Box { + Box::new(CheckpointUpdate { + header: BeaconHeader { + slot: {{CheckpointUpdate.Header.Slot}}, + proposer_index: {{CheckpointUpdate.Header.ProposerIndex}}, + parent_root: hex!("{{CheckpointUpdate.Header.ParentRoot}}").into(), + state_root: hex!("{{CheckpointUpdate.Header.StateRoot}}").into(), + body_root: hex!("{{CheckpointUpdate.Header.BodyRoot}}").into(), + }, + current_sync_committee: SyncCommittee { + pubkeys: [ + {{#CheckpointUpdate.CurrentSyncCommittee.Pubkeys}} + hex!("{{.}}").into(), + {{/CheckpointUpdate.CurrentSyncCommittee.Pubkeys}} + ], + aggregate_pubkey: hex!("{{CheckpointUpdate.CurrentSyncCommittee.AggregatePubkey}}").into(), + }, + current_sync_committee_branch: vec![ + {{#CheckpointUpdate.CurrentSyncCommitteeBranch}} + hex!("{{.}}").into(), + {{/CheckpointUpdate.CurrentSyncCommitteeBranch}} + ], + validators_root: hex!("{{CheckpointUpdate.ValidatorsRoot}}").into(), + block_roots_root: hex!("{{CheckpointUpdate.BlockRootsRoot}}").into(), + block_roots_branch: vec![ + {{#CheckpointUpdate.BlockRootsBranch}} + hex!("{{.}}").into(), + {{/CheckpointUpdate.BlockRootsBranch}} + ], + }) +} + +pub fn make_sync_committee_update() -> Box { + Box::new(Update { + attested_header: BeaconHeader { + slot: {{SyncCommitteeUpdate.AttestedHeader.Slot}}, + proposer_index: {{SyncCommitteeUpdate.AttestedHeader.ProposerIndex}}, + parent_root: hex!("{{SyncCommitteeUpdate.AttestedHeader.ParentRoot}}").into(), + state_root: hex!("{{SyncCommitteeUpdate.AttestedHeader.StateRoot}}").into(), + body_root: hex!("{{SyncCommitteeUpdate.AttestedHeader.BodyRoot}}").into(), + }, + sync_aggregate: SyncAggregate{ + sync_committee_bits: hex!("{{SyncCommitteeUpdate.SyncAggregate.SyncCommitteeBits}}"), + sync_committee_signature: hex!("{{SyncCommitteeUpdate.SyncAggregate.SyncCommitteeSignature}}").into(), + }, + signature_slot: {{SyncCommitteeUpdate.SignatureSlot}}, + next_sync_committee_update: Some(NextSyncCommitteeUpdate { + next_sync_committee: SyncCommittee { + pubkeys: [ + {{#SyncCommitteeUpdate.NextSyncCommitteeUpdate.NextSyncCommittee.Pubkeys}} + hex!("{{.}}").into(), + {{/SyncCommitteeUpdate.NextSyncCommitteeUpdate.NextSyncCommittee.Pubkeys}} + ], + aggregate_pubkey: hex!("{{SyncCommitteeUpdate.NextSyncCommitteeUpdate.NextSyncCommittee.AggregatePubkey}}").into(), + }, + next_sync_committee_branch: vec![ + {{#SyncCommitteeUpdate.NextSyncCommitteeUpdate.NextSyncCommitteeBranch}} + hex!("{{.}}").into(), + {{/SyncCommitteeUpdate.NextSyncCommitteeUpdate.NextSyncCommitteeBranch}} + ], + }), + finalized_header: BeaconHeader{ + slot: {{SyncCommitteeUpdate.FinalizedHeader.Slot}}, + proposer_index: {{SyncCommitteeUpdate.FinalizedHeader.ProposerIndex}}, + parent_root: hex!("{{SyncCommitteeUpdate.FinalizedHeader.ParentRoot}}").into(), + state_root: hex!("{{SyncCommitteeUpdate.FinalizedHeader.StateRoot}}").into(), + body_root: hex!("{{SyncCommitteeUpdate.FinalizedHeader.BodyRoot}}").into(), + }, + finality_branch: vec![ + {{#SyncCommitteeUpdate.FinalityBranch}} + hex!("{{.}}").into(), + {{/SyncCommitteeUpdate.FinalityBranch}} + ], + block_roots_root: hex!("{{SyncCommitteeUpdate.BlockRootsRoot}}").into(), + block_roots_branch: vec![ + {{#SyncCommitteeUpdate.BlockRootsBranch}} + hex!("{{.}}").into(), + {{/SyncCommitteeUpdate.BlockRootsBranch}} + ], + }) +} + +pub fn make_finalized_header_update() -> Box { + Box::new(Update { + attested_header: BeaconHeader { + slot: {{FinalizedHeaderUpdate.AttestedHeader.Slot}}, + proposer_index: {{FinalizedHeaderUpdate.AttestedHeader.ProposerIndex}}, + parent_root: hex!("{{FinalizedHeaderUpdate.AttestedHeader.ParentRoot}}").into(), + state_root: hex!("{{FinalizedHeaderUpdate.AttestedHeader.StateRoot}}").into(), + body_root: hex!("{{FinalizedHeaderUpdate.AttestedHeader.BodyRoot}}").into(), + }, + sync_aggregate: SyncAggregate{ + sync_committee_bits: hex!("{{FinalizedHeaderUpdate.SyncAggregate.SyncCommitteeBits}}"), + sync_committee_signature: hex!("{{FinalizedHeaderUpdate.SyncAggregate.SyncCommitteeSignature}}").into(), + }, + signature_slot: {{FinalizedHeaderUpdate.SignatureSlot}}, + next_sync_committee_update: None, + finalized_header: BeaconHeader { + slot: {{FinalizedHeaderUpdate.FinalizedHeader.Slot}}, + proposer_index: {{FinalizedHeaderUpdate.FinalizedHeader.ProposerIndex}}, + parent_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.ParentRoot}}").into(), + state_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.StateRoot}}").into(), + body_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.BodyRoot}}").into(), + }, + finality_branch: vec![ + {{#FinalizedHeaderUpdate.FinalityBranch}} + hex!("{{.}}").into(), + {{/FinalizedHeaderUpdate.FinalityBranch}} + ], + block_roots_root: hex!("{{FinalizedHeaderUpdate.BlockRootsRoot}}").into(), + block_roots_branch: vec![ + {{#FinalizedHeaderUpdate.BlockRootsBranch}} + hex!("{{.}}").into(), + {{/FinalizedHeaderUpdate.BlockRootsBranch}} + ] + }) +} + +pub fn make_execution_proof() -> Box { + Box::new(ExecutionProof { + header: BeaconHeader { + slot: {{HeaderUpdate.Header.Slot}}, + proposer_index: {{HeaderUpdate.Header.ProposerIndex}}, + parent_root: hex!("{{HeaderUpdate.Header.ParentRoot}}").into(), + state_root: hex!("{{HeaderUpdate.Header.StateRoot}}").into(), + body_root: hex!("{{HeaderUpdate.Header.BodyRoot}}").into(), + }, + {{#HeaderUpdate.AncestryProof}} + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + {{#HeaderUpdate.AncestryProof.HeaderBranch}} + hex!("{{.}}").into(), + {{/HeaderUpdate.AncestryProof.HeaderBranch}} + ], + finalized_block_root: hex!("{{HeaderUpdate.AncestryProof.FinalizedBlockRoot}}").into(), + }), + {{/HeaderUpdate.AncestryProof}} + {{^HeaderUpdate.AncestryProof}} + ancestry_proof: None, + {{/HeaderUpdate.AncestryProof}} + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ParentHash}}").into(), + fee_recipient: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.FeeRecipient}}").into(), + state_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.StateRoot}}").into(), + receipts_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ReceiptsRoot}}").into(), + logs_bloom: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.LogsBloom}}").into(), + prev_randao: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.PrevRandao}}").into(), + block_number: {{HeaderUpdate.ExecutionHeader.Deneb.BlockNumber}}, + gas_limit: {{HeaderUpdate.ExecutionHeader.Deneb.GasLimit}}, + gas_used: {{HeaderUpdate.ExecutionHeader.Deneb.GasUsed}}, + timestamp: {{HeaderUpdate.ExecutionHeader.Deneb.Timestamp}}, + extra_data: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ExtraData}}").into(), + base_fee_per_gas: U256::from({{HeaderUpdate.ExecutionHeader.Deneb.BaseFeePerGas}}u64), + block_hash: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.BlockHash}}").into(), + transactions_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.TransactionsRoot}}").into(), + withdrawals_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.WithdrawalsRoot}}").into(), + blob_gas_used: {{HeaderUpdate.ExecutionHeader.Deneb.BlobGasUsed}}, + excess_blob_gas: {{HeaderUpdate.ExecutionHeader.Deneb.ExcessBlobGas}}, + }), + execution_branch: vec![ + {{#HeaderUpdate.ExecutionBranch}} + hex!("{{.}}").into(), + {{/HeaderUpdate.ExecutionBranch}} + ], + }) +} + +pub fn make_inbound_fixture() -> InboundQueueFixture { + InboundQueueFixture { + message: Message { + event_log: Log { + address: hex!("{{InboundMessage.EventLog.Address}}").into(), + topics: vec![ + {{#InboundMessage.EventLog.Topics}} + hex!("{{.}}").into(), + {{/InboundMessage.EventLog.Topics}} + ], + data: hex!("{{InboundMessage.EventLog.Data}}").into(), + }, + proof: Proof { + block_hash: hex!("{{InboundMessage.Proof.BlockHash}}").into(), + tx_index: {{InboundMessage.Proof.TxIndex}}, + receipt_proof: (vec![ + {{#InboundMessage.Proof.ReceiptProof.Keys}} + hex!("{{.}}").to_vec(), + {{/InboundMessage.Proof.ReceiptProof.Keys}} + ], vec![ + {{#InboundMessage.Proof.ReceiptProof.Values}} + hex!("{{.}}").to_vec(), + {{/InboundMessage.Proof.ReceiptProof.Values}} + ]), + execution_proof: ExecutionProof { + header: BeaconHeader { + slot: {{HeaderUpdate.Header.Slot}}, + proposer_index: {{HeaderUpdate.Header.ProposerIndex}}, + parent_root: hex!("{{HeaderUpdate.Header.ParentRoot}}").into(), + state_root: hex!("{{HeaderUpdate.Header.StateRoot}}").into(), + body_root: hex!("{{HeaderUpdate.Header.BodyRoot}}").into(), + }, + {{#HeaderUpdate.AncestryProof}} + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + {{#HeaderUpdate.AncestryProof.HeaderBranch}} + hex!("{{.}}").into(), + {{/HeaderUpdate.AncestryProof.HeaderBranch}} + ], + finalized_block_root: hex!("{{HeaderUpdate.AncestryProof.FinalizedBlockRoot}}").into(), + }), + {{/HeaderUpdate.AncestryProof}} + {{^HeaderUpdate.AncestryProof}} + ancestry_proof: None, + {{/HeaderUpdate.AncestryProof}} + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ParentHash}}").into(), + fee_recipient: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.FeeRecipient}}").into(), + state_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.StateRoot}}").into(), + receipts_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ReceiptsRoot}}").into(), + logs_bloom: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.LogsBloom}}").into(), + prev_randao: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.PrevRandao}}").into(), + block_number: {{HeaderUpdate.ExecutionHeader.Deneb.BlockNumber}}, + gas_limit: {{HeaderUpdate.ExecutionHeader.Deneb.GasLimit}}, + gas_used: {{HeaderUpdate.ExecutionHeader.Deneb.GasUsed}}, + timestamp: {{HeaderUpdate.ExecutionHeader.Deneb.Timestamp}}, + extra_data: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ExtraData}}").into(), + base_fee_per_gas: U256::from({{HeaderUpdate.ExecutionHeader.Deneb.BaseFeePerGas}}u64), + block_hash: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.BlockHash}}").into(), + transactions_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.TransactionsRoot}}").into(), + withdrawals_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.WithdrawalsRoot}}").into(), + blob_gas_used: {{HeaderUpdate.ExecutionHeader.Deneb.BlobGasUsed}}, + excess_blob_gas: {{HeaderUpdate.ExecutionHeader.Deneb.ExcessBlobGas}}, + }), + execution_branch: vec![ + {{#HeaderUpdate.ExecutionBranch}} + hex!("{{.}}").into(), + {{/HeaderUpdate.ExecutionBranch}} + ], + } + }, + }, + finalized_header: BeaconHeader { + slot: {{FinalizedHeaderUpdate.FinalizedHeader.Slot}}, + proposer_index: {{FinalizedHeaderUpdate.FinalizedHeader.ProposerIndex}}, + parent_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.ParentRoot}}").into(), + state_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.StateRoot}}").into(), + body_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.BodyRoot}}").into(), + }, + block_roots_root: hex!("{{FinalizedHeaderUpdate.BlockRootsRoot}}").into(), + } +} diff --git a/relayer/templates/inbound-fixtures.mustache b/relayer/templates/inbound-fixtures.mustache new file mode 100644 index 0000000000..b35a263fda --- /dev/null +++ b/relayer/templates/inbound-fixtures.mustache @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +// Generated, do not edit! +// See ethereum client README.md for instructions to generate + +use hex_literal::hex; +use snowbridge_beacon_primitives::{ +types::deneb, AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader, +}; +use snowbridge_core::inbound::{InboundQueueFixture, Log, Message, Proof}; +use sp_core::U256; +use sp_std::vec; + +pub fn make_{{TestCase}}_message() -> InboundQueueFixture { + InboundQueueFixture { + message: Message { + event_log: Log { + address: hex!("{{InboundMessage.EventLog.Address}}").into(), + topics: vec![ + {{#InboundMessage.EventLog.Topics}} + hex!("{{.}}").into(), + {{/InboundMessage.EventLog.Topics}} + ], + data: hex!("{{InboundMessage.EventLog.Data}}").into(), + }, + proof: Proof { + block_hash: hex!("{{InboundMessage.Proof.BlockHash}}").into(), + tx_index: {{InboundMessage.Proof.TxIndex}}, + receipt_proof: (vec![ + {{#InboundMessage.Proof.ReceiptProof.Keys}} + hex!("{{.}}").to_vec(), + {{/InboundMessage.Proof.ReceiptProof.Keys}} + ], vec![ + {{#InboundMessage.Proof.ReceiptProof.Values}} + hex!("{{.}}").to_vec(), + {{/InboundMessage.Proof.ReceiptProof.Values}} + ]), + execution_proof: ExecutionProof { + header: BeaconHeader { + slot: {{HeaderUpdate.Header.Slot}}, + proposer_index: {{HeaderUpdate.Header.ProposerIndex}}, + parent_root: hex!("{{HeaderUpdate.Header.ParentRoot}}").into(), + state_root: hex!("{{HeaderUpdate.Header.StateRoot}}").into(), + body_root: hex!("{{HeaderUpdate.Header.BodyRoot}}").into(), + }, + {{#HeaderUpdate.AncestryProof}} + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + {{#HeaderUpdate.AncestryProof.HeaderBranch}} + hex!("{{.}}").into(), + {{/HeaderUpdate.AncestryProof.HeaderBranch}} + ], + finalized_block_root: hex!("{{HeaderUpdate.AncestryProof.FinalizedBlockRoot}}").into(), + }), + {{/HeaderUpdate.AncestryProof}} + {{^HeaderUpdate.AncestryProof}} + ancestry_proof: None, + {{/HeaderUpdate.AncestryProof}} + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ParentHash}}").into(), + fee_recipient: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.FeeRecipient}}").into(), + state_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.StateRoot}}").into(), + receipts_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ReceiptsRoot}}").into(), + logs_bloom: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.LogsBloom}}").into(), + prev_randao: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.PrevRandao}}").into(), + block_number: {{HeaderUpdate.ExecutionHeader.Deneb.BlockNumber}}, + gas_limit: {{HeaderUpdate.ExecutionHeader.Deneb.GasLimit}}, + gas_used: {{HeaderUpdate.ExecutionHeader.Deneb.GasUsed}}, + timestamp: {{HeaderUpdate.ExecutionHeader.Deneb.Timestamp}}, + extra_data: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.ExtraData}}").into(), + base_fee_per_gas: U256::from({{HeaderUpdate.ExecutionHeader.Deneb.BaseFeePerGas}}u64), + block_hash: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.BlockHash}}").into(), + transactions_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.TransactionsRoot}}").into(), + withdrawals_root: hex!("{{HeaderUpdate.ExecutionHeader.Deneb.WithdrawalsRoot}}").into(), + blob_gas_used: {{HeaderUpdate.ExecutionHeader.Deneb.BlobGasUsed}}, + excess_blob_gas: {{HeaderUpdate.ExecutionHeader.Deneb.ExcessBlobGas}}, + }), + execution_branch: vec![ + {{#HeaderUpdate.ExecutionBranch}} + hex!("{{.}}").into(), + {{/HeaderUpdate.ExecutionBranch}} + ], + } + }, + }, + finalized_header: BeaconHeader { + slot: {{FinalizedHeaderUpdate.FinalizedHeader.Slot}}, + proposer_index: {{FinalizedHeaderUpdate.FinalizedHeader.ProposerIndex}}, + parent_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.ParentRoot}}").into(), + state_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.StateRoot}}").into(), + body_root: hex!("{{FinalizedHeaderUpdate.FinalizedHeader.BodyRoot}}").into(), + }, + block_roots_root: hex!("{{FinalizedHeaderUpdate.BlockRootsRoot}}").into(), + } +} From e20986995bbb066c8c134a0ea820e2dc88ecbbbd Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Fri, 20 Dec 2024 20:00:46 +0200 Subject: [PATCH 19/35] template fixes --- relayer/cmd/generate_beacon_data.go | 4 ++-- relayer/templates/inbound-fixtures.mustache | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/relayer/cmd/generate_beacon_data.go b/relayer/cmd/generate_beacon_data.go index 4e28887873..7b9f11c010 100644 --- a/relayer/cmd/generate_beacon_data.go +++ b/relayer/cmd/generate_beacon_data.go @@ -105,9 +105,9 @@ type InboundFixture struct { const ( pathToBeaconTestFixtureFiles = "polkadot-sdk/bridges/snowbridge/pallets/ethereum-client/tests/fixtures" - pathToInboundQueueFixtureTemplate = "polkadot-sdk/bridges/snowbridge/templates/beacon-fixtures.mustache" + pathToInboundQueueFixtureTemplate = "relayer/templates/beacon-fixtures.mustache" pathToInboundQueueFixtureData = "polkadot-sdk/bridges/snowbridge/pallets/ethereum-client/fixtures/src/lib.rs" - pathToInboundQueueFixtureTestCaseTemplate = "polkadot-sdk/bridges/snowbridge/templates/inbound-fixtures.mustache" + pathToInboundQueueFixtureTestCaseTemplate = "relayer/templates/inbound-fixtures.mustache" pathToInboundQueueFixtureTestCaseData = "polkadot-sdk/bridges/snowbridge/pallets/inbound-queue/fixtures/src/%s.rs" ) diff --git a/relayer/templates/inbound-fixtures.mustache b/relayer/templates/inbound-fixtures.mustache index b35a263fda..84f6a7abfe 100644 --- a/relayer/templates/inbound-fixtures.mustache +++ b/relayer/templates/inbound-fixtures.mustache @@ -24,8 +24,6 @@ pub fn make_{{TestCase}}_message() -> InboundQueueFixture { data: hex!("{{InboundMessage.EventLog.Data}}").into(), }, proof: Proof { - block_hash: hex!("{{InboundMessage.Proof.BlockHash}}").into(), - tx_index: {{InboundMessage.Proof.TxIndex}}, receipt_proof: (vec![ {{#InboundMessage.Proof.ReceiptProof.Keys}} hex!("{{.}}").to_vec(), From bb55b2a1ddc5a7b4c30abcb98af724b3b08a8853 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Sat, 21 Dec 2024 22:07:30 +0200 Subject: [PATCH 20/35] contract address changes --- web/packages/api/src/environment.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/packages/api/src/environment.ts b/web/packages/api/src/environment.ts index 8f4616f432..12ce63dc67 100644 --- a/web/packages/api/src/environment.ts +++ b/web/packages/api/src/environment.ts @@ -65,7 +65,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { erc20tokensReceivable: [ { id: "WETH", - address: "0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d", + address: "0x774667629726ec1FaBEbCEc0D9139bD1C8f72a23", minimumTransferAmount: 15_000_000_000_000n, }, ], @@ -86,7 +86,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { erc20tokensReceivable: [ { id: "WETH", - address: "0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d", + address: "0x774667629726ec1FaBEbCEc0D9139bD1C8f72a23", minimumTransferAmount: 15_000_000_000_000n, }, ], @@ -107,7 +107,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { erc20tokensReceivable: [ { id: "WETH", - address: "0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d", + address: "0x774667629726ec1FaBEbCEc0D9139bD1C8f72a23", minimumTransferAmount: 1n, }, ], @@ -120,8 +120,8 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { ASSET_HUB_URL: "ws://127.0.0.1:12144", BRIDGE_HUB_URL: "ws://127.0.0.1:11144", PARACHAINS: ["ws://127.0.0.1:13144"], - GATEWAY_CONTRACT: "0xEDa338E4dC46038493b885327842fD3E301CaB39", - BEEFY_CONTRACT: "0x992B9df075935E522EC7950F37eC8557e86f6fdb", + GATEWAY_CONTRACT: "0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d", + BEEFY_CONTRACT: "0x2ffa5ecdbe006d30397c7636d3e015eee251369f", ASSET_HUB_PARAID: 1000, BRIDGE_HUB_PARAID: 1002, PRIMARY_GOVERNANCE_CHANNEL_ID: From 7359dbaf08550fcaf563db390cd9dd1c21f26aad Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Tue, 24 Dec 2024 21:22:04 +0200 Subject: [PATCH 21/35] contract address change --- smoketest/tests/upgrade_gateway.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoketest/tests/upgrade_gateway.rs b/smoketest/tests/upgrade_gateway.rs index c6a4046c46..e896b2c29c 100644 --- a/smoketest/tests/upgrade_gateway.rs +++ b/smoketest/tests/upgrade_gateway.rs @@ -42,7 +42,7 @@ use subxt::{ OnlineClient, PolkadotConfig, }; -const GATEWAY_V2_ADDRESS: [u8; 20] = hex!("ee9170abfbf9421ad6dd07f6bdec9d89f2b581e0"); +const GATEWAY_V2_ADDRESS: [u8; 20] = hex!("f8f7758fbcefd546eaeff7de24aff666b6228e73"); #[tokio::test] async fn upgrade_gateway() { From 366c457d3c9c339f493e0b115a2b185dff5b9165 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 8 Jan 2025 00:30:46 +0200 Subject: [PATCH 22/35] Ignore AgentTransferFromNative bridge command (#1359) * do not dispatch transfer from native command * test to make sure the behavior is off * fmt * naming --- contracts/src/Gateway.sol | 23 ++----------- contracts/src/interfaces/IGateway.sol | 3 -- contracts/test/Gateway.t.sol | 49 ++++++++++++++++----------- contracts/test/mocks/MockGateway.sol | 4 --- 4 files changed, 32 insertions(+), 47 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 42247d896f..fbd2bb16da 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -38,7 +38,6 @@ import { CreateChannelParams, UpdateChannelParams, SetOperatingModeParams, - TransferNativeFromAgentParams, SetTokenTransferFeesParams, SetPricingParametersParams, RegisterForeignTokenParams, @@ -215,10 +214,8 @@ contract Gateway is IGateway, IInitializable, IUpgradable { success = false; } } else if (message.command == Command.TransferNativeFromAgent) { - try Gateway(this).transferNativeFromAgent{gas: maxDispatchGas}(message.params) {} - catch { - success = false; - } + // Disable this bridge command because now native Ether can be locked in an agent. + success = true; } else if (message.command == Command.Upgrade) { try Gateway(this).upgrade{gas: maxDispatchGas}(message.params) {} catch { @@ -400,16 +397,6 @@ contract Gateway is IGateway, IInitializable, IUpgradable { emit OperatingModeChanged(params.mode); } - // @dev Transfer funds from an agent to a recipient account - function transferNativeFromAgent(bytes calldata data) external onlySelf { - TransferNativeFromAgentParams memory params = abi.decode(data, (TransferNativeFromAgentParams)); - - address agent = _ensureAgent(params.agentID); - - _transferNativeFromAgent(agent, payable(params.recipient), params.amount); - emit AgentFundsWithdrawn(params.agentID, params.recipient, params.amount); - } - // @dev Set token fees of the gateway function setTokenTransferFees(bytes calldata data) external onlySelf { AssetsStorage.Layout storage $ = AssetsStorage.layout(); @@ -609,12 +596,6 @@ contract Gateway is IGateway, IInitializable, IUpgradable { return Call.verifyResult(success, returndata); } - /// @dev Transfer ether from an agent - function _transferNativeFromAgent(address agent, address payable recipient, uint256 amount) internal { - bytes memory call = abi.encodeCall(AgentExecutor.transferNative, (recipient, amount)); - _invokeOnAgent(agent, call); - } - /// @dev Define the dust threshold as the minimum cost to transfer ether between accounts function _dustThreshold() internal view returns (uint256) { return 21000 * tx.gasprice; diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 7cfaf5e5f5..dc684c8780 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -32,9 +32,6 @@ interface IGateway { // Emitted when pricing params updated event PricingParametersChanged(); - // Emitted when funds are withdrawn from an agent - event AgentFundsWithdrawn(bytes32 indexed agentID, address indexed recipient, uint256 amount); - // Emitted when foreign token from polkadot registed event ForeignTokenRegistered(bytes32 indexed tokenID, address token); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 76d5925242..9909110ee5 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -182,6 +182,16 @@ contract GatewayTest is Test { return (Command.TransferNativeToken, abi.encode(params)); } + function makeTransferNativeFromAgentCommand(bytes32 agentID, address recipient, uint128 amount) + public + pure + returns (Command, bytes memory) + { + TransferNativeFromAgentParams memory params = + TransferNativeFromAgentParams({agentID: agentID, recipient: recipient, amount: amount}); + return (Command.TransferNativeFromAgent, abi.encode(params)); + } + function makeMockProof() public pure returns (Verification.Proof memory) { return Verification.Proof({ header: Verification.ParachainHeader({ @@ -590,14 +600,6 @@ contract GatewayTest is Test { MockGateway(address(gateway)).transferNativeTokenPublic(encodedParams); } - function testAgentExecutionBadOrigin() public { - TransferNativeFromAgentParams memory params = - TransferNativeFromAgentParams({agentID: bytes32(0), recipient: address(this), amount: 1}); - - vm.expectRevert(Gateway.AgentDoesNotExist.selector); - MockGateway(address(gateway)).transferNativeFromAgentPublic(abi.encode(params)); - } - function testAgentExecutionBadPayload() public { AgentExecuteParams memory params = AgentExecuteParams({agentID: assetHubAgentID, payload: ""}); @@ -759,18 +761,30 @@ contract GatewayTest is Test { assertEq(uint256(mode), 1); } - function testWithdrawAgentFunds() public { - deal(assetHubAgent, 50 ether); + function testWithdrawAgentFundIsIgnored() public { + address recipient = makeAddr("test_recipeint"); + uint128 amount = 1; + + deal(assetHubAgent, amount); - address recipient = makeAddr("recipient"); + (Command command, bytes memory params) = makeTransferNativeFromAgentCommand(assetHubAgentID, recipient, amount); - bytes memory params = - abi.encode(TransferNativeFromAgentParams({agentID: assetHubAgentID, recipient: recipient, amount: 3 ether})); + assertEq(address(assetHubAgent).balance, amount); + assertEq(recipient.balance, 0); - MockGateway(address(gateway)).transferNativeFromAgentPublic(params); + // Expect the gateway to emit `InboundMessageDispatched` + vm.expectEmit(); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); - assertEq(assetHubAgent.balance, 47 ether); - assertEq(recipient.balance, 3 ether); + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + + assertEq(address(assetHubAgent).balance, amount); + assertEq(recipient.balance, 0); } /** @@ -962,9 +976,6 @@ contract GatewayTest is Test { vm.expectRevert(Gateway.Unauthorized.selector); Gateway(address(gateway)).upgrade(""); - - vm.expectRevert(Gateway.Unauthorized.selector); - Gateway(address(gateway)).transferNativeFromAgent(""); } function testGetters() public { diff --git a/contracts/test/mocks/MockGateway.sol b/contracts/test/mocks/MockGateway.sol index 5f078f06e8..c88c818096 100644 --- a/contracts/test/mocks/MockGateway.sol +++ b/contracts/test/mocks/MockGateway.sol @@ -47,10 +47,6 @@ contract MockGateway is Gateway { this.setOperatingMode(params); } - function transferNativeFromAgentPublic(bytes calldata params) external { - this.transferNativeFromAgent(params); - } - function setCommitmentsAreVerified(bool value) external { commitmentsAreVerified = value; } From 3ac9646629c73604defc5a09c666d9ec73507c40 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Fri, 10 Jan 2025 11:18:27 +0200 Subject: [PATCH 23/35] fmt --- contracts/test/Gateway.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 9909110ee5..57861151ea 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -1284,7 +1284,7 @@ contract GatewayTest is Test { address(token), paraID, recipientAddress32, destinationFee, amount ); } - + function testRegisterTokenWithEthWillReturnInvalidToken() public { uint256 fee = IGateway(address(gateway)).quoteRegisterTokenFee(); vm.expectRevert(Assets.InvalidToken.selector); From aaa8139a88f758b6a4dd7a08cb2c4c4a9cd36474 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Fri, 10 Jan 2025 11:19:59 +0200 Subject: [PATCH 24/35] merge conflicts --- contracts/test/Gateway.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 57861151ea..91aed20693 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -24,7 +24,7 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {MultiAddress} from "../src/MultiAddress.sol"; import {Channel, InboundMessage, OperatingMode, ParaID, Command, ChannelID, MultiAddress} from "../src/Types.sol"; -import {SafeNativeTransfer} from "../src/utils/SafeTransfer.sol"; +import {NativeTransferFailed, SafeNativeTransfer} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {TokenLib} from "../src/TokenLib.sol"; From a12492bda51b96f7cc7547ebfb6a3771a7f386f5 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 15:06:41 +0200 Subject: [PATCH 25/35] Rename and remove parameter --- contracts/src/AgentExecutor.sol | 4 ++-- contracts/src/upgrades/Gateway202410.sol | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 654d712f1c..e326bffc00 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -22,8 +22,8 @@ contract AgentExecutor { } /// @dev Transfer ether to Gateway. Used once off for migration purposes. Can be removed after version 1. - function transferNativeToGateway(address payable gateway, uint256 amount) external { - IGateway(gateway).depositEther{value: amount}(); + function transferEtherToGateway(uint256 amount) external { + IGateway(msg.sender).depositEther{value: amount}(); } /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. diff --git a/contracts/src/upgrades/Gateway202410.sol b/contracts/src/upgrades/Gateway202410.sol index 084fc13b61..06b43b02c0 100644 --- a/contracts/src/upgrades/Gateway202410.sol +++ b/contracts/src/upgrades/Gateway202410.sol @@ -40,8 +40,7 @@ contract Gateway202410 is Gateway { // migrate asset hub agent address agent = _ensureAgent(hex"81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79"); - bytes memory call = - abi.encodeCall(AgentExecutor.transferNativeToGateway, (payable(address(this)), agent.balance)); + bytes memory call = abi.encodeCall(AgentExecutor.transferEtherToGateway, (agent.balance)); _invokeOnAgent(agent, call); } } From a14966348ef7d30bd109ab6bac2df5a3bdb15c64 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 15:29:57 +0200 Subject: [PATCH 26/35] added sendEth sanity check --- contracts/test/ForkUpgrade202410.t.sol | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/contracts/test/ForkUpgrade202410.t.sol b/contracts/test/ForkUpgrade202410.t.sol index d23a6f7696..b4f59fe9a6 100644 --- a/contracts/test/ForkUpgrade202410.t.sol +++ b/contracts/test/ForkUpgrade202410.t.sol @@ -11,6 +11,7 @@ import {Gateway202410} from "../src/upgrades/Gateway202410.sol"; import {AgentExecutor} from "../src/AgentExecutor.sol"; import {UpgradeParams, SetOperatingModeParams, OperatingMode, RegisterForeignTokenParams} from "../src/Params.sol"; import {ChannelID, ParaID, OperatingMode, TokenInfo} from "../src/Types.sol"; +import {MultiAddress, multiAddressFromBytes32} from "../src/MultiAddress.sol"; contract ForkUpgradeTest is Test { address private constant GatewayProxy = 0x27ca963C279c93801941e1eB8799c23f407d68e7; @@ -70,6 +71,36 @@ contract ForkUpgradeTest is Test { assertEq(IGateway(GatewayProxy).queryForeignTokenID(0x70D9d338A6b17957B16836a90192BD8CDAe0b53d), dotId); } + function checkSendingEthWithAmountAndFeeSucceeds() public { + // Create a mock user + address user = makeAddr("user"); + uint128 amount = 1; + ParaID paraID = ParaID.wrap(1000); + MultiAddress memory recipientAddress32 = multiAddressFromBytes32(keccak256("recipient")); + + uint128 fee = uint128(IGateway(GatewayProxy).quoteSendTokenFee(address(0), paraID, 1)); + + vm.expectEmit(); + emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); + + uint64 nonce = 173; + bytes32 messageID = keccak256(abi.encodePacked(paraID.into(), nonce)); + + vm.expectEmit(); + emit IGateway.OutboundMessageAccepted( + paraID.into(), + nonce, + messageID, + hex"00010000000000000001000000000000000000000000000000000000000000811085f5b5d1b29598e73ca51de3d712f5d3103ad50e22dc1f4d3ff1559d51150100000000000000000000000000000000ca9a3b000000000000000000000000" + ); + + deal(user, amount + fee); + vm.startPrank(user); + IGateway(GatewayProxy).sendToken{value: amount + fee}(address(0), paraID, recipientAddress32, 1, amount); + + assertEq(user.balance, 0); + } + function testSanityCheck() public { // Check that the version is correctly set. assertEq(IGateway(GatewayProxy).version(), 1); @@ -87,5 +118,7 @@ contract ForkUpgradeTest is Test { registerForeignToken(); // Check legacy ethereum token not affected checkLegacyToken(); + // Check sending of ether works + checkSendingEthWithAmountAndFeeSucceeds(); } } From 566f2fbb0b21bffe361a1b5720fc8224a68987f7 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 15:44:41 +0200 Subject: [PATCH 27/35] register ether on contruction and migration --- contracts/src/Assets.sol | 2 +- contracts/src/Gateway.sol | 3 +++ contracts/src/upgrades/Gateway202410.sol | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 242c26e2ca..9668eca1f4 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -62,7 +62,7 @@ library Assets { ) external view returns (Costs memory costs) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); TokenInfo storage info = $.tokenRegistry[token]; - if (!info.isRegistered && token != address(0)) { + if (!info.isRegistered) { revert TokenNotRegistered(); } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index fbd2bb16da..13b6c9e672 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -17,6 +17,7 @@ import { Command, MultiAddress, Ticket, + TokenInfo, Costs, AgentExecuteCommand } from "./Types.sol"; @@ -699,6 +700,8 @@ contract Gateway is IGateway, IInitializable, IUpgradable { assets.registerTokenFee = config.registerTokenFee; assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee; assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee; + TokenInfo storage nativeEther = assets.tokenRegistry[address(0)]; + nativeEther.isRegistered = true; // Initialize operator storage OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); diff --git a/contracts/src/upgrades/Gateway202410.sol b/contracts/src/upgrades/Gateway202410.sol index 06b43b02c0..b62418f2b6 100644 --- a/contracts/src/upgrades/Gateway202410.sol +++ b/contracts/src/upgrades/Gateway202410.sol @@ -42,5 +42,10 @@ contract Gateway202410 is Gateway { address agent = _ensureAgent(hex"81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79"); bytes memory call = abi.encodeCall(AgentExecutor.transferEtherToGateway, (agent.balance)); _invokeOnAgent(agent, call); + + // Register native Ether + AssetsStorage.Layout storage assets = AssetsStorage.layout(); + TokenInfo storage nativeEther = assets.tokenRegistry[address(0)]; + nativeEther.isRegistered = true; } } From 6ed71403a1b7c4799575c35b0c941b6c49fdaa28 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 15:48:05 +0200 Subject: [PATCH 28/35] remove extra branches --- contracts/src/Assets.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 9668eca1f4..599b19fdd7 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -112,17 +112,15 @@ library Assets { TokenInfo storage info = $.tokenRegistry[token]; + if (!info.isRegistered) { + revert TokenNotRegistered(); + } + if (info.foreignID == bytes32(0)) { - if (!info.isRegistered && token != address(0)) { - revert TokenNotRegistered(); - } return _sendNativeToken( token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount ); } else { - if (!info.isRegistered) { - revert TokenNotRegistered(); - } return _sendForeignToken( info.foreignID, token, From f86a6d8d61856396b0cd4a247929940faba3ccb7 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 15:53:36 +0200 Subject: [PATCH 29/35] simplify math --- contracts/src/Gateway.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 13b6c9e672..e0f2f19518 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -556,8 +556,9 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // The fee is already collected into the gateway contract // Reimburse excess fee payment - if (msg.value > totalEther && (msg.value - totalEther) > _dustThreshold()) { - payable(msg.sender).safeNativeTransfer(msg.value - totalEther); + uint256 extraPayment = (msg.value - totalEther); + if (extraPayment > _dustThreshold()) { + payable(msg.sender).safeNativeTransfer(extraPayment); } // Generate a unique ID for this message From bd1548ec508cdb87ef69fcd0cdd24c6ec3364371 Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 16:07:53 +0200 Subject: [PATCH 30/35] separate branches for ether and native tokens --- contracts/src/Assets.sol | 88 ++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 599b19fdd7..9a53643d72 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -116,7 +116,11 @@ library Assets { revert TokenNotRegistered(); } - if (info.foreignID == bytes32(0)) { + if (token == address(0)) { + return _sendEther( + sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount + ); + } else if (info.foreignID == bytes32(0)) { return _sendNativeToken( token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount ); @@ -134,8 +138,8 @@ library Assets { } } - function _sendNativeToken( - address token, + // @dev Transfer ERC20(Ethereum-native) tokens to Polkadot + function _sendEther( address sender, ParaID destinationChain, MultiAddress calldata destinationAddress, @@ -145,19 +149,77 @@ library Assets { ) internal returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - // Lock the funds into AssetHub's agent contract - if (token != address(0)) { - // ERC20 - _transferToAgent($.assetHubAgent, token, sender, amount); - ticket.value = 0; + // Lock Native Ether into AssetHub's agent contract + if (msg.value < amount) { + revert InvalidAmount(); + } + payable($.assetHubAgent).safeNativeTransfer(amount); + ticket.value = amount; + + ticket.dest = $.assetHubParaID; + ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); + + address token = address(0); + // Construct a message payload + if (destinationChain == $.assetHubParaID) { + // The funds will be minted into the receiver's account on AssetHub + if (destinationAddress.isAddress32()) { + // The receiver has a 32-byte account ID + ticket.payload = SubstrateTypes.SendTokenToAssetHubAddress32( + token, destinationAddress.asAddress32(), $.assetHubReserveTransferFee, amount + ); + } else { + // AssetHub does not support 20-byte account IDs + revert Unsupported(); + } } else { - // Native ETH - if (msg.value < amount) { - revert InvalidAmount(); + if (destinationChainFee == 0) { + revert InvalidDestinationFee(); + } + // The funds will be minted into sovereign account of the destination parachain on AssetHub, + // and then reserve-transferred to the receiver's account on the destination parachain. + if (destinationAddress.isAddress32()) { + // The receiver has a 32-byte account ID + ticket.payload = SubstrateTypes.SendTokenToAddress32( + token, + destinationChain, + destinationAddress.asAddress32(), + $.assetHubReserveTransferFee, + destinationChainFee, + amount + ); + } else if (destinationAddress.isAddress20()) { + // The receiver has a 20-byte account ID + ticket.payload = SubstrateTypes.SendTokenToAddress20( + token, + destinationChain, + destinationAddress.asAddress20(), + $.assetHubReserveTransferFee, + destinationChainFee, + amount + ); + } else { + revert Unsupported(); } - payable($.assetHubAgent).safeNativeTransfer(amount); - ticket.value = amount; } + emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); + } + + // @dev Transfer ERC20(Ethereum-native) tokens to Polkadot + function _sendNativeToken( + address token, + address sender, + ParaID destinationChain, + MultiAddress calldata destinationAddress, + uint128 destinationChainFee, + uint128 maxDestinationChainFee, + uint128 amount + ) internal returns (Ticket memory ticket) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + + // Lock the ERC20 into AssetHub's agent contract + _transferToAgent($.assetHubAgent, token, sender, amount); + ticket.value = 0; ticket.dest = $.assetHubParaID; ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); From e45c9ee9f455b7b263b2a22e9838dd669cc7bd3f Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 16 Jan 2025 16:14:19 +0200 Subject: [PATCH 31/35] fix comments --- contracts/src/Assets.sol | 2 +- contracts/src/Gateway.sol | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 9a53643d72..f27fa1bdd7 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -138,7 +138,7 @@ library Assets { } } - // @dev Transfer ERC20(Ethereum-native) tokens to Polkadot + // @dev Transfer Ether tokens to Polkadot function _sendEther( address sender, ParaID destinationChain, diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index e0f2f19518..3cbd092ab1 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -556,9 +556,9 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // The fee is already collected into the gateway contract // Reimburse excess fee payment - uint256 extraPayment = (msg.value - totalEther); - if (extraPayment > _dustThreshold()) { - payable(msg.sender).safeNativeTransfer(extraPayment); + uint256 excessFee = (msg.value - totalEther); + if (excessFee > _dustThreshold()) { + payable(msg.sender).safeNativeTransfer(excessFee); } // Generate a unique ID for this message @@ -701,6 +701,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { assets.registerTokenFee = config.registerTokenFee; assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee; assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee; + // Register native Ether TokenInfo storage nativeEther = assets.tokenRegistry[address(0)]; nativeEther.isRegistered = true; From 2699ebe4be0ad837ddc673bb94b9cbc1233b1d9d Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Fri, 17 Jan 2025 15:06:57 +0200 Subject: [PATCH 32/35] factor out emitting of sendToken --- contracts/src/Assets.sol | 119 ++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 65 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index f27fa1bdd7..906dd839c0 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -138,35 +138,27 @@ library Assets { } } - // @dev Transfer Ether tokens to Polkadot - function _sendEther( + function _emitSendNativeTokenTicket( + address token, address sender, + ParaID assetHubParaID, ParaID destinationChain, MultiAddress calldata destinationAddress, + uint128 assetHubReserveTransferFee, uint128 destinationChainFee, uint128 maxDestinationChainFee, uint128 amount ) internal returns (Ticket memory ticket) { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - - // Lock Native Ether into AssetHub's agent contract - if (msg.value < amount) { - revert InvalidAmount(); - } - payable($.assetHubAgent).safeNativeTransfer(amount); - ticket.value = amount; - - ticket.dest = $.assetHubParaID; + ticket.dest = assetHubParaID; ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); - address token = address(0); // Construct a message payload - if (destinationChain == $.assetHubParaID) { + if (destinationChain == assetHubParaID) { // The funds will be minted into the receiver's account on AssetHub if (destinationAddress.isAddress32()) { // The receiver has a 32-byte account ID ticket.payload = SubstrateTypes.SendTokenToAssetHubAddress32( - token, destinationAddress.asAddress32(), $.assetHubReserveTransferFee, amount + token, destinationAddress.asAddress32(), assetHubReserveTransferFee, amount ); } else { // AssetHub does not support 20-byte account IDs @@ -184,7 +176,7 @@ library Assets { token, destinationChain, destinationAddress.asAddress32(), - $.assetHubReserveTransferFee, + assetHubReserveTransferFee, destinationChainFee, amount ); @@ -194,7 +186,7 @@ library Assets { token, destinationChain, destinationAddress.asAddress20(), - $.assetHubReserveTransferFee, + assetHubReserveTransferFee, destinationChainFee, amount ); @@ -205,6 +197,38 @@ library Assets { emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); } + // @dev Transfer Ether tokens to Polkadot + function _sendEther( + address sender, + ParaID destinationChain, + MultiAddress calldata destinationAddress, + uint128 destinationChainFee, + uint128 maxDestinationChainFee, + uint128 amount + ) internal returns (Ticket memory ticket) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + + // Lock Native Ether into AssetHub's agent contract + if (msg.value < amount) { + revert InvalidAmount(); + } + + ticket = _emitSendNativeTokenTicket( + address(0), + sender, + $.assetHubParaID, + destinationChain, + destinationAddress, + $.assetHubReserveTransferFee, + destinationChainFee, + maxDestinationChainFee, + amount + ); + ticket.value = amount; + + payable($.assetHubAgent).safeNativeTransfer(amount); + } + // @dev Transfer ERC20(Ethereum-native) tokens to Polkadot function _sendNativeToken( address token, @@ -217,56 +241,21 @@ library Assets { ) internal returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - // Lock the ERC20 into AssetHub's agent contract - _transferToAgent($.assetHubAgent, token, sender, amount); + ticket = _emitSendNativeTokenTicket( + token, + sender, + $.assetHubParaID, + destinationChain, + destinationAddress, + $.assetHubReserveTransferFee, + destinationChainFee, + maxDestinationChainFee, + amount + ); ticket.value = 0; - ticket.dest = $.assetHubParaID; - ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); - - // Construct a message payload - if (destinationChain == $.assetHubParaID) { - // The funds will be minted into the receiver's account on AssetHub - if (destinationAddress.isAddress32()) { - // The receiver has a 32-byte account ID - ticket.payload = SubstrateTypes.SendTokenToAssetHubAddress32( - token, destinationAddress.asAddress32(), $.assetHubReserveTransferFee, amount - ); - } else { - // AssetHub does not support 20-byte account IDs - revert Unsupported(); - } - } else { - if (destinationChainFee == 0) { - revert InvalidDestinationFee(); - } - // The funds will be minted into sovereign account of the destination parachain on AssetHub, - // and then reserve-transferred to the receiver's account on the destination parachain. - if (destinationAddress.isAddress32()) { - // The receiver has a 32-byte account ID - ticket.payload = SubstrateTypes.SendTokenToAddress32( - token, - destinationChain, - destinationAddress.asAddress32(), - $.assetHubReserveTransferFee, - destinationChainFee, - amount - ); - } else if (destinationAddress.isAddress20()) { - // The receiver has a 20-byte account ID - ticket.payload = SubstrateTypes.SendTokenToAddress20( - token, - destinationChain, - destinationAddress.asAddress20(), - $.assetHubReserveTransferFee, - destinationChainFee, - amount - ); - } else { - revert Unsupported(); - } - } - emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); + // Lock the ERC20 into AssetHub's agent contract + _transferToAgent($.assetHubAgent, token, sender, amount); } // @dev Transfer Polkadot-native tokens back to Polkadot From ddad89faf0db1a75a54aa4246e4e577918d74ddb Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Thu, 23 Jan 2025 00:12:11 +0200 Subject: [PATCH 33/35] comment --- contracts/src/Assets.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 906dd839c0..52aa148fb1 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -197,7 +197,7 @@ library Assets { emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); } - // @dev Transfer Ether tokens to Polkadot + // @dev Transfer Ether to Polkadot function _sendEther( address sender, ParaID destinationChain, @@ -208,7 +208,6 @@ library Assets { ) internal returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - // Lock Native Ether into AssetHub's agent contract if (msg.value < amount) { revert InvalidAmount(); } @@ -226,6 +225,7 @@ library Assets { ); ticket.value = amount; + // Lock Native Ether into AssetHub's agent contract payable($.assetHubAgent).safeNativeTransfer(amount); } From b972591b3aea88a3bbcb58a03010de3436d25534 Mon Sep 17 00:00:00 2001 From: Vincent Geddes <117534+vgeddes@users.noreply.github.com> Date: Thu, 23 Jan 2025 20:59:31 +0200 Subject: [PATCH 34/35] Refactor --- .../upgrades/polkadot/DeployGateway202502.sol | 33 +++++++ .../upgrades/westend/DeployGateway202502.sol | 33 +++++++ contracts/src/AgentExecutor.sol | 17 +--- contracts/src/Assets.sol | 93 +++++++++++++------ contracts/src/Gateway.sol | 13 +-- contracts/src/Types.sol | 1 + contracts/src/interfaces/IGateway.sol | 4 +- contracts/src/storage/CoreStorage.sol | 2 - .../{Gateway202410.sol => Gateway202502.sol} | 18 +--- ...de202410.t.sol => ForkUpgrade202502.t.sol} | 52 ++++------- 10 files changed, 161 insertions(+), 105 deletions(-) create mode 100644 contracts/scripts/upgrades/polkadot/DeployGateway202502.sol create mode 100644 contracts/scripts/upgrades/westend/DeployGateway202502.sol rename contracts/src/upgrades/{Gateway202410.sol => Gateway202502.sol} (62%) rename contracts/test/{ForkUpgrade202410.t.sol => ForkUpgrade202502.t.sol} (83%) diff --git a/contracts/scripts/upgrades/polkadot/DeployGateway202502.sol b/contracts/scripts/upgrades/polkadot/DeployGateway202502.sol new file mode 100644 index 0000000000..2c25452f17 --- /dev/null +++ b/contracts/scripts/upgrades/polkadot/DeployGateway202502.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.28; + +import {WETH9} from "canonical-weth/WETH9.sol"; +import {Script} from "forge-std/Script.sol"; +import {BeefyClient} from "../../../src/BeefyClient.sol"; +import {AgentExecutor} from "../../../src/AgentExecutor.sol"; +import {ParaID} from "../../../src/Types.sol"; +import {Gateway202502} from "../../../src/upgrades/Gateway202502.sol"; +import {SafeNativeTransfer} from "../../../src/utils/SafeTransfer.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {UD60x18, ud60x18} from "prb/math/src/UD60x18.sol"; + +contract DeployGateway202502 is Script { + address public constant BEEFY_CLIENT_ADDRESS = 0x6eD05bAa904df3DE117EcFa638d4CB84e1B8A00C; + + function run() public { + vm.startBroadcast(); + + AgentExecutor executor = new AgentExecutor(); + Gateway202502 gatewayLogic = new Gateway202502( + BEEFY_CLIENT_ADDRESS, + address(executor), + ParaID.wrap(1002), + 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314, + 10, + 20_000_000_000 // 2 DOT + ); + + vm.stopBroadcast(); + } +} diff --git a/contracts/scripts/upgrades/westend/DeployGateway202502.sol b/contracts/scripts/upgrades/westend/DeployGateway202502.sol new file mode 100644 index 0000000000..7146762719 --- /dev/null +++ b/contracts/scripts/upgrades/westend/DeployGateway202502.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.28; + +import {WETH9} from "canonical-weth/WETH9.sol"; +import {Script} from "forge-std/Script.sol"; +import {BeefyClient} from "../../../src/BeefyClient.sol"; +import {AgentExecutor} from "../../../src/AgentExecutor.sol"; +import {ParaID} from "../../../src/Types.sol"; +import {Gateway202502} from "../../../src/upgrades/Gateway202502.sol"; +import {SafeNativeTransfer} from "../../../src/utils/SafeTransfer.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {UD60x18, ud60x18} from "prb/math/src/UD60x18.sol"; + +contract DeployGateway202502 is Script { + address public constant BEEFY_CLIENT_ADDRESS = 0x6DFaD3D73A28c48E4F4c616ECda80885b415283a; + + function run() public { + vm.startBroadcast(); + + AgentExecutor executor = new AgentExecutor(); + Gateway202502 gatewayLogic = new Gateway202502( + BEEFY_CLIENT_ADDRESS, + address(executor), + ParaID.wrap(1002), + 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314, + 12, + 2_000_000_000_000 // 2 DOT + ); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index e326bffc00..2881ccb128 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -15,24 +15,13 @@ contract AgentExecutor { using SafeTokenTransfer for IERC20; using SafeNativeTransfer for address payable; - /// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`, - /// as the gateway needs to control an agent's ether balance directly. - function transferNative(address payable recipient, uint256 amount) external { + /// @dev Transfer ether to `recipient`. + function transferEther(address payable recipient, uint256 amount) external { recipient.safeNativeTransfer(amount); } - /// @dev Transfer ether to Gateway. Used once off for migration purposes. Can be removed after version 1. - function transferEtherToGateway(uint256 amount) external { - IGateway(msg.sender).depositEther{value: amount}(); - } - - /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. + /// @dev Transfer ERC20 to `recipient`. function transferToken(address token, address recipient, uint128 amount) external { - _transferToken(token, recipient, amount); - } - - /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. - function _transferToken(address token, address recipient, uint128 amount) internal { IERC20(token).safeTransfer(recipient, amount); } } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 52aa148fb1..ba1358ed8a 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -116,12 +116,8 @@ library Assets { revert TokenNotRegistered(); } - if (token == address(0)) { - return _sendEther( - sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount - ); - } else if (info.foreignID == bytes32(0)) { - return _sendNativeToken( + if (info.foreignID == bytes32(0)) { + return _sendNativeTokenOrEther( token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount ); } else { @@ -208,10 +204,6 @@ library Assets { ) internal returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if (msg.value < amount) { - revert InvalidAmount(); - } - ticket = _emitSendNativeTokenTicket( address(0), sender, @@ -223,14 +215,10 @@ library Assets { maxDestinationChainFee, amount ); - ticket.value = amount; - - // Lock Native Ether into AssetHub's agent contract - payable($.assetHubAgent).safeNativeTransfer(amount); } // @dev Transfer ERC20(Ethereum-native) tokens to Polkadot - function _sendNativeToken( + function _sendNativeTokenOrEther( address token, address sender, ParaID destinationChain, @@ -241,21 +229,66 @@ library Assets { ) internal returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - ticket = _emitSendNativeTokenTicket( - token, - sender, - $.assetHubParaID, - destinationChain, - destinationAddress, - $.assetHubReserveTransferFee, - destinationChainFee, - maxDestinationChainFee, - amount - ); - ticket.value = 0; + if (token != address(0)) { + // Lock ERC20 + _transferToAgent($.assetHubAgent, token, sender, amount); + } else { + // Lock Ether + if (msg.value < amount) { + revert InvalidAmount(); + } + // Lock Ether + payable($.assetHubAgent).safeNativeTransfer(amount); + ticket.value = amount; + } - // Lock the ERC20 into AssetHub's agent contract - _transferToAgent($.assetHubAgent, token, sender, amount); + ticket.dest = $.assetHubParaID; + ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); + + // Construct a message payload + if (destinationChain == $.assetHubParaID) { + // The funds will be minted into the receiver's account on AssetHub + if (destinationAddress.isAddress32()) { + // The receiver has a 32-byte account ID + ticket.payload = SubstrateTypes.SendTokenToAssetHubAddress32( + token, destinationAddress.asAddress32(), $.assetHubReserveTransferFee, amount + ); + } else { + // AssetHub does not support 20-byte account IDs + revert Unsupported(); + } + } else { + if (destinationChainFee == 0) { + revert InvalidDestinationFee(); + } + // The funds will be minted into sovereign account of the destination parachain on AssetHub, + // and then reserve-transferred to the receiver's account on the destination parachain. + if (destinationAddress.isAddress32()) { + // The receiver has a 32-byte account ID + ticket.payload = SubstrateTypes.SendTokenToAddress32( + token, + destinationChain, + destinationAddress.asAddress32(), + $.assetHubReserveTransferFee, + destinationChainFee, + amount + ); + } else if (destinationAddress.isAddress20()) { + // The receiver has a 20-byte account ID + ticket.payload = SubstrateTypes.SendTokenToAddress20( + token, + destinationChain, + destinationAddress.asAddress20(), + $.assetHubReserveTransferFee, + destinationChainFee, + amount + ); + } else { + revert Unsupported(); + } + } + + emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); } // @dev Transfer Polkadot-native tokens back to Polkadot @@ -371,7 +404,7 @@ library Assets { call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount)); } else { // Native ETH - call = abi.encodeCall(AgentExecutor.transferNative, (payable(recipient), amount)); + call = abi.encodeCall(AgentExecutor.transferEther, (payable(recipient), amount)); } (bool success,) = Agent(payable(agent)).invoke(executor, call); if (!success) { diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 3cbd092ab1..014e36b1ad 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -296,15 +296,11 @@ contract Gateway is IGateway, IInitializable, IUpgradable { return ERC1967.load(); } - function version() public view returns (uint64) { - return CoreStorage.layout().version; - } - /** * Fee management */ function depositEther() external payable { - emit EtherDeposited(msg.sender, msg.value); + emit Deposited(msg.sender, msg.value); } /** @@ -556,7 +552,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // The fee is already collected into the gateway contract // Reimburse excess fee payment - uint256 excessFee = (msg.value - totalEther); + uint256 excessFee = msg.value - totalEther; if (excessFee > _dustThreshold()) { payable(msg.sender).safeNativeTransfer(excessFee); } @@ -701,9 +697,10 @@ contract Gateway is IGateway, IInitializable, IUpgradable { assets.registerTokenFee = config.registerTokenFee; assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee; assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee; + // Register native Ether - TokenInfo storage nativeEther = assets.tokenRegistry[address(0)]; - nativeEther.isRegistered = true; + TokenInfo storage etherTokenInfo = assets.tokenRegistry[address(0)]; + etherTokenInfo.isRegistered = true; // Initialize operator storage OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 7986eaeacf..f57add21f7 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -107,6 +107,7 @@ struct Ticket { ParaID dest; Costs costs; bytes payload; + // amount of native ether to be sent uint128 value; } diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index dc684c8780..20f2ffef4a 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -36,7 +36,7 @@ interface IGateway { event ForeignTokenRegistered(bytes32 indexed tokenID, address token); // Emitted when ether is deposited - event EtherDeposited(address who, uint256 amount); + event Deposited(address sender, uint256 amount); /** * Getters @@ -53,8 +53,6 @@ interface IGateway { function implementation() external view returns (address); - function version() external view returns (uint64); - /** * Fee management */ diff --git a/contracts/src/storage/CoreStorage.sol b/contracts/src/storage/CoreStorage.sol index a8a7931b1b..35e6ec03c2 100644 --- a/contracts/src/storage/CoreStorage.sol +++ b/contracts/src/storage/CoreStorage.sol @@ -14,8 +14,6 @@ library CoreStorage { mapping(bytes32 agentID => address) agents; // Agent addresses mapping(address agent => bytes32 agentID) agentAddresses; - // Version of the Gateway Implementation - uint64 version; } bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.core"); diff --git a/contracts/src/upgrades/Gateway202410.sol b/contracts/src/upgrades/Gateway202502.sol similarity index 62% rename from contracts/src/upgrades/Gateway202410.sol rename to contracts/src/upgrades/Gateway202502.sol index b62418f2b6..a41beb35cb 100644 --- a/contracts/src/upgrades/Gateway202410.sol +++ b/contracts/src/upgrades/Gateway202502.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.28; import "../Gateway.sol"; // New `Gateway` logic contract for the `GatewayProxy` deployed on mainnet -contract Gateway202410 is Gateway { +contract Gateway202502 is Gateway { constructor( address beefyClient, address agentExecutor, @@ -31,21 +31,9 @@ contract Gateway202410 is Gateway { revert Unauthorized(); } - // We expect version 0, deploying version 1. - CoreStorage.Layout storage $ = CoreStorage.layout(); - if ($.version != 0) { - revert Unauthorized(); - } - $.version = 1; - - // migrate asset hub agent - address agent = _ensureAgent(hex"81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79"); - bytes memory call = abi.encodeCall(AgentExecutor.transferEtherToGateway, (agent.balance)); - _invokeOnAgent(agent, call); - // Register native Ether AssetsStorage.Layout storage assets = AssetsStorage.layout(); - TokenInfo storage nativeEther = assets.tokenRegistry[address(0)]; - nativeEther.isRegistered = true; + TokenInfo storage tokenInfo = assets.tokenRegistry[address(0)]; + tokenInfo.isRegistered = true; } } diff --git a/contracts/test/ForkUpgrade202410.t.sol b/contracts/test/ForkUpgrade202502.t.sol similarity index 83% rename from contracts/test/ForkUpgrade202410.t.sol rename to contracts/test/ForkUpgrade202502.t.sol index b4f59fe9a6..b8fce38fb3 100644 --- a/contracts/test/ForkUpgrade202410.t.sol +++ b/contracts/test/ForkUpgrade202502.t.sol @@ -7,7 +7,7 @@ import {console} from "forge-std/console.sol"; import {IUpgradable} from "../src/interfaces/IUpgradable.sol"; import {IGateway} from "../src/interfaces/IGateway.sol"; import {Gateway} from "../src/Gateway.sol"; -import {Gateway202410} from "../src/upgrades/Gateway202410.sol"; +import {Gateway202502} from "../src/upgrades/Gateway202502.sol"; import {AgentExecutor} from "../src/AgentExecutor.sol"; import {UpgradeParams, SetOperatingModeParams, OperatingMode, RegisterForeignTokenParams} from "../src/Params.sol"; import {ChannelID, ParaID, OperatingMode, TokenInfo} from "../src/Types.sol"; @@ -29,28 +29,35 @@ contract ForkUpgradeTest is Test { function forkUpgrade() public { AgentExecutor executor = new AgentExecutor(); - Gateway202410 newLogic = - new Gateway202410(BeefyClient, address(executor), ParaID.wrap(1002), BridgeHubAgent, 10, 20000000000); + Gateway202502 newLogic = + new Gateway202502(BeefyClient, address(executor), ParaID.wrap(1002), BridgeHubAgent, 10, 20000000000); UpgradeParams memory params = UpgradeParams({impl: address(newLogic), implCodeHash: address(newLogic).codehash, initParams: bytes("")}); Gateway gateway = Gateway(GatewayProxy); - // Check pre-migration of ETH from Asset Hub agent - assertGt(IGateway(GatewayProxy).agentOf(AssetHubAgent).balance, 0); - // Check pre-migration of ETH to Gateway - assertEq(address(GatewayProxy).balance, 0); - - vm.expectEmit(); - emit IGateway.EtherDeposited(gateway.agentOf(AssetHubAgent), 587928061927368450); - vm.expectEmit(); emit IUpgradable.Upgraded(address(newLogic)); gateway.upgrade(abi.encode(params)); } + function testSanityCheck() public { + // Check AH channel nonces as expected + (uint64 inbound, uint64 outbound) = IGateway(GatewayProxy).channelNoncesOf( + ChannelID.wrap(0xc173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539) + ); + assertEq(inbound, 13); + assertEq(outbound, 172); + // Register PNA + registerForeignToken(); + // Check legacy ethereum token not affected + checkLegacyToken(); + // Check sending of ether works + checkSendingEthWithAmountAndFeeSucceeds(); + } + function checkLegacyToken() public { assert(IGateway(GatewayProxy).isTokenRegistered(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); assertEq(IGateway(GatewayProxy).queryForeignTokenID(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), bytes32("")); @@ -66,7 +73,7 @@ contract ForkUpgradeTest is Test { vm.expectEmit(true, true, false, false); emit IGateway.ForeignTokenRegistered(dotId, address(0x0)); - Gateway202410(GatewayProxy).registerForeignToken(abi.encode(params)); + Gateway202502(GatewayProxy).registerForeignToken(abi.encode(params)); assert(IGateway(GatewayProxy).isTokenRegistered(0x70D9d338A6b17957B16836a90192BD8CDAe0b53d)); assertEq(IGateway(GatewayProxy).queryForeignTokenID(0x70D9d338A6b17957B16836a90192BD8CDAe0b53d), dotId); } @@ -100,25 +107,4 @@ contract ForkUpgradeTest is Test { assertEq(user.balance, 0); } - - function testSanityCheck() public { - // Check that the version is correctly set. - assertEq(IGateway(GatewayProxy).version(), 1); - // Check migration of ETH from Asset Hub agent - assertEq(IGateway(GatewayProxy).agentOf(AssetHubAgent).balance, 0); - // Check migration of ETH to Gateway - assertGt(address(GatewayProxy).balance, 0); - // Check AH channel nonces as expected - (uint64 inbound, uint64 outbound) = IGateway(GatewayProxy).channelNoncesOf( - ChannelID.wrap(0xc173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539) - ); - assertEq(inbound, 13); - assertEq(outbound, 172); - // Register PNA - registerForeignToken(); - // Check legacy ethereum token not affected - checkLegacyToken(); - // Check sending of ether works - checkSendingEthWithAmountAndFeeSucceeds(); - } } From eed9eb183a4c9ab9ccc2e60244199304cc489e4e Mon Sep 17 00:00:00 2001 From: Vincent Geddes <117534+vgeddes@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:21:12 +0200 Subject: [PATCH 35/35] Improve validation of passed ether --- contracts/src/Assets.sol | 98 +++------------------------ contracts/src/Gateway.sol | 15 ++-- contracts/src/Types.sol | 10 +++ contracts/src/interfaces/IGateway.sol | 4 +- contracts/test/Gateway.t.sol | 21 ++++-- 5 files changed, 48 insertions(+), 100 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index ba1358ed8a..2f48d07887 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -110,13 +110,17 @@ library Assets { ) external returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if (amount == 0) { + revert InvalidAmount(); + } + TokenInfo storage info = $.tokenRegistry[token]; if (!info.isRegistered) { revert TokenNotRegistered(); } - if (info.foreignID == bytes32(0)) { + if (info.isNativeToken()) { return _sendNativeTokenOrEther( token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount ); @@ -134,89 +138,6 @@ library Assets { } } - function _emitSendNativeTokenTicket( - address token, - address sender, - ParaID assetHubParaID, - ParaID destinationChain, - MultiAddress calldata destinationAddress, - uint128 assetHubReserveTransferFee, - uint128 destinationChainFee, - uint128 maxDestinationChainFee, - uint128 amount - ) internal returns (Ticket memory ticket) { - ticket.dest = assetHubParaID; - ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee); - - // Construct a message payload - if (destinationChain == assetHubParaID) { - // The funds will be minted into the receiver's account on AssetHub - if (destinationAddress.isAddress32()) { - // The receiver has a 32-byte account ID - ticket.payload = SubstrateTypes.SendTokenToAssetHubAddress32( - token, destinationAddress.asAddress32(), assetHubReserveTransferFee, amount - ); - } else { - // AssetHub does not support 20-byte account IDs - revert Unsupported(); - } - } else { - if (destinationChainFee == 0) { - revert InvalidDestinationFee(); - } - // The funds will be minted into sovereign account of the destination parachain on AssetHub, - // and then reserve-transferred to the receiver's account on the destination parachain. - if (destinationAddress.isAddress32()) { - // The receiver has a 32-byte account ID - ticket.payload = SubstrateTypes.SendTokenToAddress32( - token, - destinationChain, - destinationAddress.asAddress32(), - assetHubReserveTransferFee, - destinationChainFee, - amount - ); - } else if (destinationAddress.isAddress20()) { - // The receiver has a 20-byte account ID - ticket.payload = SubstrateTypes.SendTokenToAddress20( - token, - destinationChain, - destinationAddress.asAddress20(), - assetHubReserveTransferFee, - destinationChainFee, - amount - ); - } else { - revert Unsupported(); - } - } - emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); - } - - // @dev Transfer Ether to Polkadot - function _sendEther( - address sender, - ParaID destinationChain, - MultiAddress calldata destinationAddress, - uint128 destinationChainFee, - uint128 maxDestinationChainFee, - uint128 amount - ) internal returns (Ticket memory ticket) { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - - ticket = _emitSendNativeTokenTicket( - address(0), - sender, - $.assetHubParaID, - destinationChain, - destinationAddress, - $.assetHubReserveTransferFee, - destinationChainFee, - maxDestinationChainFee, - amount - ); - } - // @dev Transfer ERC20(Ethereum-native) tokens to Polkadot function _sendNativeTokenOrEther( address token, @@ -232,13 +153,10 @@ library Assets { if (token != address(0)) { // Lock ERC20 _transferToAgent($.assetHubAgent, token, sender, amount); + ticket.value = 0; } else { - // Lock Ether - if (msg.value < amount) { - revert InvalidAmount(); - } - // Lock Ether - payable($.assetHubAgent).safeNativeTransfer(amount); + // Track the ether to bridge to Polkadot. This will be handled + // in `Gateway._submitOutbound`. ticket.value = amount; } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 014e36b1ad..301ce254db 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -88,7 +88,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { error InvalidProof(); error InvalidNonce(); error NotEnoughGas(); - error FeePaymentTooLow(); + error InsufficientEther(); error Unauthorized(); error Disabled(); error AgentAlreadyCreated(); @@ -215,7 +215,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { success = false; } } else if (message.command == Command.TransferNativeFromAgent) { - // Disable this bridge command because now native Ether can be locked in an agent. + // DISABLED success = true; } else if (message.command == Command.Upgrade) { try Gateway(this).upgrade{gas: maxDispatchGas}(message.params) {} @@ -542,10 +542,17 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // Destination fee always in DOT uint256 fee = _calculateFee(ticket.costs); - // Ensure the user has enough funds for this message to be accepted + // Ensure the user has provided enough ether for this message to be accepted. + // This includes: + // 1. The bridging fee, which is collected in this gateway contract + // 2. The ether value being bridged over to Polkadot, which is locked into the AssetHub + // agent contract. uint256 totalEther = fee + ticket.value; if (msg.value < totalEther) { - revert FeePaymentTooLow(); + revert InsufficientEther(); + } + if (ticket.value > 0) { + payable(AssetsStorage.layout().assetHubAgent).safeNativeTransfer(ticket.value); } channel.outboundNonce = channel.outboundNonce + 1; diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index f57add21f7..a4a478d0c8 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -115,3 +115,13 @@ struct TokenInfo { bool isRegistered; bytes32 foreignID; } + +using {isNativeToken} for TokenInfo global; + +function isNativeToken(TokenInfo storage info) view returns (bool) { + return info.foreignID == bytes32(0); +} + +function isForeignToken(TokenInfo storage info) view returns (bool) { + return !info.isNativeToken(); +} diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 20f2ffef4a..1a578103fc 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -110,7 +110,9 @@ interface IGateway { view returns (uint256); - /// @dev Send ERC20 tokens to parachain `destinationChain` and deposit into account `destinationAddress` + /// @dev Send a token to parachain `destinationChain` and deposit into account + /// `destinationAddress`. The user can send native Ether by supplying `address(0)` for + /// the `token` parameter. function sendToken( address token, ParaID destinationChain, diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 91aed20693..3a4a0f5e25 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -483,6 +483,8 @@ contract GatewayTest is Test { fee = IGateway(address(gateway)).quoteSendTokenFee(address(token), ParaID.wrap(0), 1); + uint256 gatewayBeforeBalance = address(gateway).balance; + // Let gateway lock up to 1 tokens hoax(user); token.approve(address(gateway), 1); @@ -490,6 +492,7 @@ contract GatewayTest is Test { hoax(user, fee); IGateway(address(gateway)).sendToken{value: fee}(address(token), ParaID.wrap(0), recipientAddress32, 1, 1); + assertEq(address(gateway).balance - gatewayBeforeBalance, fee); assertEq(user.balance, 0); } @@ -500,6 +503,8 @@ contract GatewayTest is Test { ParaID paraID = ParaID.wrap(1000); uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, 1)); + uint256 gatewayBeforeBalance = address(gateway).balance; + uint256 assetHubBeforeBalance = address(assetHubAgent).balance; vm.expectEmit(); emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); @@ -508,6 +513,8 @@ contract GatewayTest is Test { hoax(user, amount + fee); IGateway(address(gateway)).sendToken{value: amount + fee}(address(0), paraID, recipientAddress32, 1, amount); + assertEq(address(gateway).balance - gatewayBeforeBalance, fee); + assertEq(address(assetHubAgent).balance - assetHubBeforeBalance, amount); assertEq(user.balance, 0); } @@ -519,6 +526,8 @@ contract GatewayTest is Test { ParaID paraID = ParaID.wrap(1000); uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, 1)); + uint256 gatewayBeforeBalance = address(gateway).balance; + uint256 assetHubBeforeBalance = address(assetHubAgent).balance; vm.expectEmit(); emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); @@ -529,10 +538,12 @@ contract GatewayTest is Test { address(0), paraID, recipientAddress32, 1, amount ); + assertEq(address(gateway).balance - gatewayBeforeBalance, fee); + assertEq(address(assetHubAgent).balance - assetHubBeforeBalance, amount); assertEq(user.balance, extra); } - function testSendingEthWithoutFeeFails() public { + function testSendingEtherWithInsufficientEther1() public { // Create a mock user address user = makeAddr("user"); uint128 amount = 1; @@ -542,12 +553,12 @@ contract GatewayTest is Test { vm.expectEmit(); emit IGateway.TokenSent(address(0), user, paraID, recipientAddress32, amount); - vm.expectRevert(Gateway.FeePaymentTooLow.selector); + vm.expectRevert(Gateway.InsufficientEther.selector); hoax(user, amount + fee); IGateway(address(gateway)).sendToken{value: amount}(address(0), paraID, recipientAddress32, 1, amount); } - function testSendingEthWithoutAmountFails() public { + function testSendingEtherWithInsufficientEther2() public { // Create a mock user address user = makeAddr("user"); uint128 amount = 1 ether; @@ -555,7 +566,7 @@ contract GatewayTest is Test { uint128 fee = uint128(IGateway(address(gateway)).quoteSendTokenFee(address(0), paraID, amount)); - vm.expectRevert(Assets.InvalidAmount.selector); + vm.expectRevert(Gateway.InsufficientEther.selector); hoax(user, amount + fee); IGateway(address(gateway)).sendToken{value: amount - 1}(address(0), paraID, recipientAddress32, 1, amount); } @@ -574,7 +585,7 @@ contract GatewayTest is Test { hoax(user); token.approve(address(gateway), 1); - vm.expectRevert(Gateway.FeePaymentTooLow.selector); + vm.expectRevert(Gateway.InsufficientEther.selector); hoax(user, 2 ether); IGateway(address(gateway)).sendToken{value: 0.002 ether}( address(token), ParaID.wrap(0), recipientAddress32, 1, 1