From 7501630c9addd8ea7b4ea87c72c00831ff8b374c Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 13 Mar 2024 18:23:08 +0800 Subject: [PATCH 01/55] Asset from polkadot --- contracts/src/AgentExecutor.sol | 34 ++- contracts/src/ERC20.sol | 295 ++++++++++++++++++++++ contracts/src/Gateway.sol | 47 +++- contracts/src/Types.sol | 8 +- contracts/src/interfaces/IERC20.sol | 1 + contracts/src/interfaces/IERC20Permit.sol | 14 + contracts/src/interfaces/IGateway.sol | 3 + contracts/src/storage/AssetsStorage.sol | 3 + contracts/src/storage/CoreStorage.sol | 2 + contracts/test/Gateway.t.sol | 29 +++ 10 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 contracts/src/ERC20.sol create mode 100644 contracts/src/interfaces/IERC20Permit.sol diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index c9cdaa885e..69bca927a7 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -7,6 +7,8 @@ import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; +import {ERC20} from "./ERC20.sol"; +import {Gateway} from "./Gateway.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. /// @dev This is a singleton contract, meaning that all agents will execute the same code. @@ -14,14 +16,24 @@ contract AgentExecutor { using SafeTokenTransfer for IERC20; using SafeNativeTransfer for address payable; + // Emitted when token minted + event TokenMinted(bytes32 indexed tokenID, address token, address recipient, uint256 amount); + /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(bytes memory data) external { + function execute(address gateway, bytes32 agentID, bytes memory data) external { (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); _transferToken(token, recipient, amount); + } else if (command == AgentExecuteCommand.RegisterToken) { + (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = + abi.decode(params, (bytes32, string, string, uint8)); + _registerToken(gateway, agentID, tokenID, name, symbol, decimals); + } else if (command == AgentExecuteCommand.MintToken) { + (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); + _mintToken(gateway, tokenID, recipient, amount); } } @@ -36,4 +48,24 @@ contract AgentExecutor { function _transferToken(address token, address recipient, uint128 amount) internal { IERC20(token).safeTransfer(recipient, amount); } + + /// @dev Register native asset from polkadto as ERC20 `token`. + function _registerToken( + address gateway, + bytes32 agentID, + bytes32 tokenID, + string memory name, + string memory symbol, + uint8 decimals + ) internal { + IERC20 token = new ERC20(name, symbol, decimals); + Gateway(gateway).registerTokenByID(tokenID, address(token), agentID); + } + + /// @dev Mint ERC20 token to `recipient`. + function _mintToken(address gateway, bytes32 tokenID, address recipient, uint256 amount) internal { + address token = Gateway(gateway).getTokenAddress(tokenID); + ERC20(token).mint(recipient, amount); + emit TokenMinted(tokenID, token, recipient, amount); + } } diff --git a/contracts/src/ERC20.sol b/contracts/src/ERC20.sol new file mode 100644 index 0000000000..b9191227a1 --- /dev/null +++ b/contracts/src/ERC20.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Axelar Network +// SPDX-FileCopyrightText: 2023 Snowfork + +pragma solidity 0.8.23; + +import {IERC20} from "./interfaces/IERC20.sol"; +import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * This supply mechanism has been added in {ERC20Permit-mint}. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is conventional and does + * not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to these events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is IERC20, IERC20Permit { + error PermitExpired(); + error InvalidS(); + error InvalidV(); + error InvalidSignature(); + error Unauthorized(); + + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + mapping(address => uint256) public nonces; + + bytes32 public immutable DOMAIN_SEPARATOR; + uint256 public override totalSupply; + + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') + bytes32 private constant DOMAIN_TYPE_SIGNATURE_HASH = + bytes32(0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f); + + // keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') + bytes32 private constant PERMIT_SIGNATURE_HASH = + bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); + + address public immutable OWNER; + string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; + uint8 public immutable decimals; + + string public name; + string public symbol; + + /** + * @dev Sets the values for {name}, {symbol}, and {decimals}. + */ + constructor(string memory name_, string memory symbol_, uint8 decimals_) { + OWNER = msg.sender; + DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name_)), keccak256(bytes("1")), block.chainid, address(this) + ) + ); + + name = name_; + symbol = symbol_; + decimals = decimals_; + } + + modifier onlyOwner() { + if (msg.sender != OWNER) { + revert Unauthorized(); + } + _; + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. Can only be called by the owner. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `to` cannot be the zero address. + */ + function mint(address account, uint256 amount) external virtual onlyOwner { + _mint(account, amount); + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) external virtual override returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: Prefer the {increaseAllowance} and {decreaseAllowance} methods, as + * they aren't vulnerable to the frontrunning attack described here: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) external virtual override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) { + uint256 _allowance = allowance[sender][msg.sender]; + + if (_allowance != type(uint256).max) { + _approve(sender, msg.sender, _allowance - amount); + } + + _transfer(sender, recipient, amount); + + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) { + _approve(msg.sender, spender, allowance[msg.sender][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) { + _approve(msg.sender, spender, allowance[msg.sender][spender] - subtractedValue); + return true; + } + + function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + { + if (block.timestamp > deadline) revert PermitExpired(); + + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); + + if (v != 27 && v != 28) revert InvalidV(); + + bytes32 digest = keccak256( + abi.encodePacked( + EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_SIGNATURE_HASH, issuer, spender, value, nonces[issuer]++, deadline)) + ) + ); + + address recoveredAddress = ecrecover(digest, v, r, s); + + if (recoveredAddress != issuer) revert InvalidSignature(); + + // _approve will revert if issuer is address(0x0) + _approve(issuer, spender, value); + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + if (sender == address(0) || recipient == address(0)) revert InvalidAccount(); + + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + if (account == address(0)) revert InvalidAccount(); + + totalSupply += amount; + balanceOf[account] += amount; + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + if (account == address(0)) revert InvalidAccount(); + + balanceOf[account] -= amount; + totalSupply -= amount; + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + if (owner == address(0) || spender == address(0)) revert InvalidAccount(); + + allowance[owner][spender] = amount; + emit Approval(owner, spender, amount); + } +} diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 60f5d8c32b..2d55e9d7ff 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -17,7 +17,8 @@ import { Command, MultiAddress, Ticket, - Costs + Costs, + TokenInfo } from "./Types.sol"; import {IGateway} from "./interfaces/IGateway.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; @@ -87,6 +88,8 @@ contract Gateway is IGateway, IInitializable { error InvalidCodeHash(); error InvalidConstructorParams(); error AlreadyInitialized(); + error TokenAlreadyRegistered(); + error TokenNotRegistered(); // handler functions are privileged modifier onlySelf() { @@ -96,6 +99,15 @@ contract Gateway is IGateway, IInitializable { _; } + // handler functions are privileged from agent only + modifier onlyAgent(bytes32 agentID) { + bytes32 _agentID = _ensureAgentAddress(msg.sender); + if (_agentID != agentID) { + revert Unauthorized(); + } + _; + } + constructor( address beefyClient, address agentExecutor, @@ -265,7 +277,7 @@ contract Gateway is IGateway, IInitializable { revert InvalidAgentExecutionPayload(); } - bytes memory call = abi.encodeCall(AgentExecutor.execute, params.payload); + bytes memory call = abi.encodeCall(AgentExecutor.execute, (address(this), params.agentID, params.payload)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); if (!success) { @@ -432,6 +444,27 @@ contract Gateway is IGateway, IInitializable { ); } + // @dev Register a new fungible Polkadot token for an agent + function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) external onlyAgent(agentID) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == true) { + revert TokenAlreadyRegistered(); + } + TokenInfo memory info = TokenInfo({isRegistered: true, tokenID: tokenID, agentID: agentID, token: token}); + $.tokenRegistry[token] = info; + $.tokenRegistryByID[tokenID] = info; + emit TokenRegistered(tokenID, agentID, token); + } + + // @dev Get token address by tokenID + function getTokenAddress(bytes32 tokenID) external view returns (address) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == false) { + revert TokenNotRegistered(); + } + return $.tokenRegistryByID[tokenID].token; + } + /** * Internal functions */ @@ -533,6 +566,14 @@ contract Gateway is IGateway, IInitializable { } } + /// @dev Ensure that the specified address is an valid agent + function _ensureAgentAddress(address agent) internal view returns (bytes32 agentID) { + agentID = CoreStorage.layout().agentAddresses[agent]; + if (agentID == bytes32(0)) { + revert AgentDoesNotExist(); + } + } + /// @dev Invoke some code within an agent function _invokeOnAgent(address agent, bytes memory data) internal returns (bytes memory) { (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(AGENT_EXECUTOR, data)); @@ -592,6 +633,7 @@ contract Gateway is IGateway, IInitializable { // Initialize agent for BridgeHub address bridgeHubAgent = address(new Agent(BRIDGE_HUB_AGENT_ID)); core.agents[BRIDGE_HUB_AGENT_ID] = bridgeHubAgent; + core.agentAddresses[bridgeHubAgent] = BRIDGE_HUB_AGENT_ID; // Initialize channel for primary governance track core.channels[PRIMARY_GOVERNANCE_CHANNEL_ID] = @@ -604,6 +646,7 @@ contract Gateway is IGateway, IInitializable { // Initialize agent for for AssetHub address assetHubAgent = address(new Agent(config.assetHubAgentID)); core.agents[config.assetHubAgentID] = assetHubAgent; + core.agentAddresses[assetHubAgent] = config.assetHubAgentID; // Initialize channel for AssetHub core.channels[config.assetHubParaID.into()] = diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 93d41bc0f5..fe78e9b55c 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -88,7 +88,9 @@ enum Command { } enum AgentExecuteCommand { - TransferToken + TransferToken, + RegisterToken, + MintToken } /// @dev Application-level costs for a message @@ -107,5 +109,7 @@ struct Ticket { struct TokenInfo { bool isRegistered; - bytes31 __padding; + bytes32 tokenID; + bytes32 agentID; + address token; } diff --git a/contracts/src/interfaces/IERC20.sol b/contracts/src/interfaces/IERC20.sol index fcf6204d79..71ef5679d3 100644 --- a/contracts/src/interfaces/IERC20.sol +++ b/contracts/src/interfaces/IERC20.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2023 Axelar Network +// SPDX-FileCopyrightText: 2023 Snowfork pragma solidity 0.8.23; diff --git a/contracts/src/interfaces/IERC20Permit.sol b/contracts/src/interfaces/IERC20Permit.sol new file mode 100644 index 0000000000..1118e88045 --- /dev/null +++ b/contracts/src/interfaces/IERC20Permit.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Axelar Network +// SPDX-FileCopyrightText: 2023 Snowfork + +pragma solidity 0.8.23; + +interface IERC20Permit { + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function nonces(address account) external view returns (uint256); + + function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; +} diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index e0d4d81da2..70a6cd2de9 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -38,6 +38,9 @@ interface IGateway { // Emitted when funds are withdrawn from an agent event AgentFundsWithdrawn(bytes32 indexed agentID, address indexed recipient, uint256 amount); + // Emitted when token registed + event TokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token); + /** * Getters */ diff --git a/contracts/src/storage/AssetsStorage.sol b/contracts/src/storage/AssetsStorage.sol index b4a34ed4dc..1777672e53 100644 --- a/contracts/src/storage/AssetsStorage.sol +++ b/contracts/src/storage/AssetsStorage.sol @@ -6,6 +6,7 @@ import {TokenInfo, ParaID} from "../Types.sol"; library AssetsStorage { struct Layout { + // Token registry by token address mapping(address token => TokenInfo) tokenRegistry; address assetHubAgent; ParaID assetHubParaID; @@ -15,6 +16,8 @@ library AssetsStorage { uint128 assetHubReserveTransferFee; // Extra fee for registering a token, to discourage spamming (Ether) uint256 registerTokenFee; + // Token registry by tokenID + mapping(bytes32 tokenID => TokenInfo) tokenRegistryByID; } bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets"); diff --git a/contracts/src/storage/CoreStorage.sol b/contracts/src/storage/CoreStorage.sol index e66c9cfaab..a9e5b959d4 100644 --- a/contracts/src/storage/CoreStorage.sol +++ b/contracts/src/storage/CoreStorage.sol @@ -12,6 +12,8 @@ library CoreStorage { mapping(ChannelID channelID => Channel) channels; // Agents mapping(bytes32 agentID => address) agents; + // Agent addresses + mapping(address agent => bytes32 agentID) agentAddresses; } bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.core"); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 604e1a9d3f..723e0cacb0 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -50,6 +50,9 @@ import "./mocks/GatewayUpgradeMock.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; contract GatewayTest is Test { + // Emitted when token minted + event TokenMinted(bytes32 indexed tokenID, address token, address recipient, uint256 amount); + ParaID public bridgeHubParaID = ParaID.wrap(1001); bytes32 public bridgeHubAgentID = keccak256("1001"); address public bridgeHubAgent; @@ -901,4 +904,30 @@ contract GatewayTest is Test { vm.expectRevert(Assets.InvalidDestinationFee.selector); IGateway(address(gateway)).sendToken{value: fee}(address(token), destPara, recipientAddress32, 0, 1); } + + function testAgentRegisterToken() public { + AgentExecuteParams memory params = AgentExecuteParams({ + agentID: assetHubAgentID, + payload: abi.encode(AgentExecuteCommand.RegisterToken, abi.encode(bytes32(uint256(1)), "DOT", "DOT", 10)) + }); + + vm.expectEmit(true, true, false, false); + emit IGateway.TokenRegistered(bytes32(uint256(1)), assetHubAgentID, address(0)); + + GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + } + + function testAgentMintToken() public { + testAgentRegisterToken(); + + AgentExecuteParams memory params = AgentExecuteParams({ + agentID: assetHubAgentID, + payload: abi.encode(AgentExecuteCommand.MintToken, abi.encode(bytes32(uint256(1)), account1, 1000)) + }); + + vm.expectEmit(true, true, false, false); + emit TokenMinted(bytes32(uint256(1)), address(0), account1, 1000); + + GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + } } From e0c358188fff6cada2ad0fcafbfef52d12d01507 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 19 Mar 2024 16:20:30 +0800 Subject: [PATCH 02/55] Remove params unnecessary --- contracts/src/AgentExecutor.sol | 23 +++++++++-------------- contracts/src/Gateway.sol | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 69bca927a7..8dc3334e84 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -22,7 +22,7 @@ contract AgentExecutor { /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(address gateway, bytes32 agentID, bytes memory data) external { + function execute(bytes32 agentID, bytes memory data) external { (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); @@ -30,10 +30,10 @@ contract AgentExecutor { } else if (command == AgentExecuteCommand.RegisterToken) { (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = abi.decode(params, (bytes32, string, string, uint8)); - _registerToken(gateway, agentID, tokenID, name, symbol, decimals); + _registerToken(agentID, tokenID, name, symbol, decimals); } else if (command == AgentExecuteCommand.MintToken) { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); - _mintToken(gateway, tokenID, recipient, amount); + _mintToken(tokenID, recipient, amount); } } @@ -50,21 +50,16 @@ contract AgentExecutor { } /// @dev Register native asset from polkadto as ERC20 `token`. - function _registerToken( - address gateway, - bytes32 agentID, - bytes32 tokenID, - string memory name, - string memory symbol, - uint8 decimals - ) internal { + function _registerToken(bytes32 agentID, bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) + internal + { IERC20 token = new ERC20(name, symbol, decimals); - Gateway(gateway).registerTokenByID(tokenID, address(token), agentID); + Gateway(msg.sender).registerTokenByID(tokenID, address(token), agentID); } /// @dev Mint ERC20 token to `recipient`. - function _mintToken(address gateway, bytes32 tokenID, address recipient, uint256 amount) internal { - address token = Gateway(gateway).getTokenAddress(tokenID); + function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { + address token = Gateway(msg.sender).getTokenAddress(tokenID); ERC20(token).mint(recipient, amount); emit TokenMinted(tokenID, token, recipient, amount); } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 2d55e9d7ff..9211d7eb05 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -277,7 +277,7 @@ contract Gateway is IGateway, IInitializable { revert InvalidAgentExecutionPayload(); } - bytes memory call = abi.encodeCall(AgentExecutor.execute, (address(this), params.agentID, params.payload)); + bytes memory call = abi.encodeCall(AgentExecutor.execute, (params.agentID, params.payload)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); if (!success) { From 780c7bd920796d22169e34406ff48ea8b4b041cf Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 20 Mar 2024 21:36:08 +0800 Subject: [PATCH 03/55] Experiment: return calldata from Agent instead of call Gateway directly --- contracts/src/AgentExecutor.sol | 11 ++++++----- contracts/src/Gateway.sol | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 8dc3334e84..e30429535a 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -22,19 +22,19 @@ contract AgentExecutor { /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(bytes32 agentID, bytes memory data) external { - (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); + function execute(AgentExecuteCommand command, bytes memory params) external returns (bytes memory) { if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); _transferToken(token, recipient, amount); } else if (command == AgentExecuteCommand.RegisterToken) { (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = abi.decode(params, (bytes32, string, string, uint8)); - _registerToken(agentID, tokenID, name, symbol, decimals); + return _registerToken(tokenID, name, symbol, decimals); } else if (command == AgentExecuteCommand.MintToken) { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); _mintToken(tokenID, recipient, amount); } + return bytes(""); } /// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`, @@ -50,11 +50,12 @@ contract AgentExecutor { } /// @dev Register native asset from polkadto as ERC20 `token`. - function _registerToken(bytes32 agentID, bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) + function _registerToken(bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) internal + returns (bytes memory) { IERC20 token = new ERC20(name, symbol, decimals); - Gateway(msg.sender).registerTokenByID(tokenID, address(token), agentID); + return abi.encode(tokenID, address(token)); } /// @dev Mint ERC20 token to `recipient`. diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 9211d7eb05..69c4f59d6d 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -18,7 +18,8 @@ import { MultiAddress, Ticket, Costs, - TokenInfo + TokenInfo, + AgentExecuteCommand } from "./Types.sol"; import {IGateway} from "./interfaces/IGateway.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; @@ -28,6 +29,8 @@ import {SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {Call} from "./utils/Call.sol"; import {Math} from "./utils/Math.sol"; import {ScaleCodec} from "./utils/ScaleCodec.sol"; +import {IERC20} from "./interfaces/IERC20.sol"; +import {ERC20} from "./ERC20.sol"; import { UpgradeParams, @@ -277,12 +280,20 @@ contract Gateway is IGateway, IInitializable { revert InvalidAgentExecutionPayload(); } - bytes memory call = abi.encodeCall(AgentExecutor.execute, (params.agentID, params.payload)); + (AgentExecuteCommand command, bytes memory payload) = abi.decode(params.payload, (AgentExecuteCommand, bytes)); + + bytes memory call = abi.encodeCall(AgentExecutor.execute, (command, payload)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); if (!success) { revert AgentExecutionFailed(returndata); } + + if (command == AgentExecuteCommand.RegisterToken) { + (bytes memory result) = abi.decode(returndata, (bytes)); + (bytes32 tokenID, address token) = abi.decode(result, (bytes32, address)); + return registerTokenByID(tokenID, token, params.agentID); + } } /// @dev Create an agent for a consensus system on Polkadot @@ -445,7 +456,7 @@ contract Gateway is IGateway, IInitializable { } // @dev Register a new fungible Polkadot token for an agent - function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) external onlyAgent(agentID) { + function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) internal { AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == true) { revert TokenAlreadyRegistered(); From 27353f0e9802d0cd04b423d1b7b13a060b80e400 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 20 Mar 2024 22:11:53 +0800 Subject: [PATCH 04/55] To store registries also on agent --- contracts/src/AgentExecutor.sol | 13 +++++++++---- contracts/src/Assets.sol | 22 ++++++++++++++++++++++ contracts/src/Gateway.sol | 29 ++--------------------------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index e30429535a..be5f9fc5fb 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -9,6 +9,7 @@ import {IERC20} from "./interfaces/IERC20.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {ERC20} from "./ERC20.sol"; import {Gateway} from "./Gateway.sol"; +import {Assets} from "./Assets.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. /// @dev This is a singleton contract, meaning that all agents will execute the same code. @@ -22,14 +23,17 @@ contract AgentExecutor { /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(AgentExecuteCommand command, bytes memory params) external returns (bytes memory) { + function execute(bytes32 agentID, AgentExecuteCommand command, bytes memory params) + external + returns (bytes memory) + { if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); _transferToken(token, recipient, amount); } else if (command == AgentExecuteCommand.RegisterToken) { (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = abi.decode(params, (bytes32, string, string, uint8)); - return _registerToken(tokenID, name, symbol, decimals); + return _registerToken(agentID, tokenID, name, symbol, decimals); } else if (command == AgentExecuteCommand.MintToken) { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); _mintToken(tokenID, recipient, amount); @@ -50,17 +54,18 @@ contract AgentExecutor { } /// @dev Register native asset from polkadto as ERC20 `token`. - function _registerToken(bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) + function _registerToken(bytes32 agentID, bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) internal returns (bytes memory) { IERC20 token = new ERC20(name, symbol, decimals); + Assets.registerTokenByID(tokenID, address(token), agentID); return abi.encode(tokenID, address(token)); } /// @dev Mint ERC20 token to `recipient`. function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { - address token = Gateway(msg.sender).getTokenAddress(tokenID); + address token = Assets.getTokenAddress(tokenID); ERC20(token).mint(recipient, amount); emit TokenMinted(tokenID, token, recipient, amount); } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index a1ccac2ff5..c7affb6ba5 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -24,6 +24,7 @@ library Assets { error TokenNotRegistered(); error Unsupported(); error InvalidDestinationFee(); + error TokenAlreadyRegistered(); function isTokenRegistered(address token) external view returns (bool) { return AssetsStorage.layout().tokenRegistry[token].isRegistered; @@ -174,4 +175,25 @@ library Assets { emit IGateway.TokenRegistrationSent(token); } + + // @dev Register a new fungible Polkadot token for an agent + function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) internal { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == true) { + revert TokenAlreadyRegistered(); + } + TokenInfo memory info = TokenInfo({isRegistered: true, tokenID: tokenID, agentID: agentID, token: token}); + $.tokenRegistry[token] = info; + $.tokenRegistryByID[tokenID] = info; + emit IGateway.TokenRegistered(tokenID, agentID, token); + } + + // @dev Get token address by tokenID + function getTokenAddress(bytes32 tokenID) internal view returns (address) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == false) { + revert TokenNotRegistered(); + } + return $.tokenRegistryByID[tokenID].token; + } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 69c4f59d6d..75bede8c74 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -29,8 +29,6 @@ import {SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {Call} from "./utils/Call.sol"; import {Math} from "./utils/Math.sol"; import {ScaleCodec} from "./utils/ScaleCodec.sol"; -import {IERC20} from "./interfaces/IERC20.sol"; -import {ERC20} from "./ERC20.sol"; import { UpgradeParams, @@ -91,8 +89,6 @@ contract Gateway is IGateway, IInitializable { error InvalidCodeHash(); error InvalidConstructorParams(); error AlreadyInitialized(); - error TokenAlreadyRegistered(); - error TokenNotRegistered(); // handler functions are privileged modifier onlySelf() { @@ -282,7 +278,7 @@ contract Gateway is IGateway, IInitializable { (AgentExecuteCommand command, bytes memory payload) = abi.decode(params.payload, (AgentExecuteCommand, bytes)); - bytes memory call = abi.encodeCall(AgentExecutor.execute, (command, payload)); + bytes memory call = abi.encodeCall(AgentExecutor.execute, (params.agentID, command, payload)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); if (!success) { @@ -292,7 +288,7 @@ contract Gateway is IGateway, IInitializable { if (command == AgentExecuteCommand.RegisterToken) { (bytes memory result) = abi.decode(returndata, (bytes)); (bytes32 tokenID, address token) = abi.decode(result, (bytes32, address)); - return registerTokenByID(tokenID, token, params.agentID); + Assets.registerTokenByID(tokenID, token, params.agentID); } } @@ -455,27 +451,6 @@ contract Gateway is IGateway, IInitializable { ); } - // @dev Register a new fungible Polkadot token for an agent - function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) internal { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == true) { - revert TokenAlreadyRegistered(); - } - TokenInfo memory info = TokenInfo({isRegistered: true, tokenID: tokenID, agentID: agentID, token: token}); - $.tokenRegistry[token] = info; - $.tokenRegistryByID[tokenID] = info; - emit TokenRegistered(tokenID, agentID, token); - } - - // @dev Get token address by tokenID - function getTokenAddress(bytes32 tokenID) external view returns (address) { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == false) { - revert TokenNotRegistered(); - } - return $.tokenRegistryByID[tokenID].token; - } - /** * Internal functions */ From 6212597e103bc3c1e216f159936cc529bf2062c3 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 22 Mar 2024 15:09:42 +0800 Subject: [PATCH 05/55] Send polkadot tokens back --- contracts/src/AgentExecutor.sol | 13 +++- contracts/src/Assets.sol | 81 ++++++++++++++++++++- contracts/src/ERC20.sol | 9 ++- contracts/src/Gateway.sol | 19 +++++ contracts/src/SubstrateTypes.sol | 38 ++++++++++ contracts/src/Types.sol | 1 + contracts/src/interfaces/IGateway.sol | 22 ++++++ contracts/test/Gateway.t.sol | 36 ++++++++- contracts/test/mocks/GatewayUpgradeMock.sol | 15 ++++ 9 files changed, 225 insertions(+), 9 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index be5f9fc5fb..3c285fd660 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -10,6 +10,7 @@ import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {ERC20} from "./ERC20.sol"; import {Gateway} from "./Gateway.sol"; import {Assets} from "./Assets.sol"; +import {TokenInfo} from "./storage/AssetsStorage.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. /// @dev This is a singleton contract, meaning that all agents will execute the same code. @@ -19,6 +20,8 @@ contract AgentExecutor { // Emitted when token minted event TokenMinted(bytes32 indexed tokenID, address token, address recipient, uint256 amount); + // Emitted when token burnt + event TokenBurnt(bytes32 indexed tokenID, address token, address sender, uint256 amount); /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. @@ -65,8 +68,16 @@ contract AgentExecutor { /// @dev Mint ERC20 token to `recipient`. function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { - address token = Assets.getTokenAddress(tokenID); + TokenInfo memory info = Assets.getTokenInfo(tokenID); + address token = info.token; ERC20(token).mint(recipient, amount); emit TokenMinted(tokenID, token, recipient, amount); } + + function burnToken(bytes32 tokenID, address sender, uint256 amount) external { + TokenInfo memory info = Assets.getTokenInfo(tokenID); + address token = info.token; + ERC20(token).burn(sender, amount); + emit TokenBurnt(tokenID, token, sender, amount); + } } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index c7affb6ba5..0011ec9b64 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -8,9 +8,14 @@ import {IGateway} from "./interfaces/IGateway.sol"; import {SafeTokenTransferFrom} from "./utils/SafeTransfer.sol"; import {AssetsStorage, TokenInfo} from "./storage/AssetsStorage.sol"; +import {CoreStorage} from "./storage/CoreStorage.sol"; + import {SubstrateTypes} from "./SubstrateTypes.sol"; import {ParaID, MultiAddress, Ticket, Costs} from "./Types.sol"; import {Address} from "./utils/Address.sol"; +import {AgentExecutor} from "./AgentExecutor.sol"; +import {Agent} from "./Agent.sol"; +import {Call} from "./utils/Call.sol"; /// @title Library for implementing Ethereum->Polkadot ERC20 transfers. library Assets { @@ -25,6 +30,7 @@ library Assets { error Unsupported(); error InvalidDestinationFee(); error TokenAlreadyRegistered(); + error AgentDoesNotExist(); function isTokenRegistered(address token) external view returns (bool) { return AssetsStorage.layout().tokenRegistry[token].isRegistered; @@ -88,6 +94,9 @@ library Assets { if (!info.isRegistered) { revert TokenNotRegistered(); } + if (info.isForeign) { + revert InvalidToken(); + } // Lock the funds into AssetHub's agent contract _transferToAgent($.assetHubAgent, token, sender, amount); @@ -182,18 +191,84 @@ library Assets { if ($.tokenRegistryByID[tokenID].isRegistered == true) { revert TokenAlreadyRegistered(); } - TokenInfo memory info = TokenInfo({isRegistered: true, tokenID: tokenID, agentID: agentID, token: token}); + TokenInfo memory info = + TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); $.tokenRegistry[token] = info; $.tokenRegistryByID[tokenID] = info; emit IGateway.TokenRegistered(tokenID, agentID, token); } // @dev Get token address by tokenID - function getTokenAddress(bytes32 tokenID) internal view returns (address) { + function getTokenInfo(bytes32 tokenID) internal view returns (TokenInfo memory) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == false) { revert TokenNotRegistered(); } - return $.tokenRegistryByID[tokenID].token; + return $.tokenRegistryByID[tokenID]; + } + + // @dev Transfer polkadot native tokens back + function transferToken( + address executor, + address token, + address sender, + ParaID destinationChain, + MultiAddress calldata destinationAddress, + uint128 destinationChainFee, + uint128 amount + ) internal returns (Ticket memory ticket) { + AssetsStorage.Layout storage $asset = AssetsStorage.layout(); + + TokenInfo storage info = $asset.tokenRegistry[token]; + if (!info.isRegistered) { + revert TokenNotRegistered(); + } + if (!info.isForeign) { + revert InvalidToken(); + } + + CoreStorage.Layout storage $core = CoreStorage.layout(); + + address agent = $core.agents[info.agentID]; + if (agent == address(0)) { + revert AgentDoesNotExist(); + } + + // Polkadot-native token: burn wrapped token + _burn(executor, agent, info.tokenID, sender, amount); + + if (destinationChainFee == 0) { + revert InvalidDestinationFee(); + } + + ticket.dest = destinationChain; + ticket.costs = _transferTokenCosts(destinationChainFee); + + if (destinationAddress.isAddress32()) { + // The receiver has a 32-byte account ID + ticket.payload = SubstrateTypes.TransferTokenToAddress32( + token, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount + ); + } else if (destinationAddress.isAddress20()) { + // The receiver has a 20-byte account ID + ticket.payload = SubstrateTypes.TransferTokenToAddress20( + token, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount + ); + } else { + revert Unsupported(); + } + + emit IGateway.TokenTransfered(token, sender, destinationChain, destinationAddress, amount); + } + + function _burn(address agentExecutor, address agent, bytes32 tokenID, address sender, uint256 amount) internal { + bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (tokenID, sender, amount)); + (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(agentExecutor, call)); + Call.verifyResult(success, returndata); + } + + function _transferTokenCosts(uint128 destinationChainFee) internal pure returns (Costs memory costs) { + costs.foreign = destinationChainFee; + costs.native = 0; } } diff --git a/contracts/src/ERC20.sol b/contracts/src/ERC20.sol index b9191227a1..3552c10e73 100644 --- a/contracts/src/ERC20.sol +++ b/contracts/src/ERC20.sol @@ -89,12 +89,19 @@ contract ERC20 is IERC20, IERC20Permit { * * Requirements: * - * - `to` cannot be the zero address. + * - `account` cannot be the zero address. */ function mint(address account, uint256 amount) external virtual onlyOwner { _mint(account, amount); } + /** + * @dev Destroys `amount` tokens from the account. + */ + function burn(address account, uint256 amount) external virtual onlyOwner { + _burn(account, amount); + } + /** * @dev See {IERC20-transfer}. * diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 4ef140a6b0..cfa35552be 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -452,6 +452,25 @@ contract Gateway is IGateway, IInitializable { ); } + // Transfer polkadot native tokens back + function transferToken( + address token, + ParaID destinationChain, + MultiAddress calldata destinationAddress, + uint128 destinationFee, + uint128 amount + ) external payable { + _submitOutbound( + Assets.transferToken( + AGENT_EXECUTOR, token, msg.sender, destinationChain, destinationAddress, destinationFee, amount + ) + ); + } + + function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory) { + return Assets.getTokenInfo(tokenID); + } + /** * Internal functions */ diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index af817ac1a1..1a4b8105e5 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -133,4 +133,42 @@ library SubstrateTypes { ScaleCodec.encodeU128(xcmFee) ); } + + // destination is AccountID32 address + function TransferTokenToAddress32(address token, ParaID paraID, bytes32 recipient, uint128 xcmFee, uint128 amount) + internal + view + returns (bytes memory) + { + return bytes.concat( + bytes1(0x00), + ScaleCodec.encodeU64(uint64(block.chainid)), + bytes1(0x02), + SubstrateTypes.H160(token), + bytes1(0x01), + ScaleCodec.encodeU32(uint32(ParaID.unwrap(paraID))), + recipient, + ScaleCodec.encodeU128(xcmFee), + ScaleCodec.encodeU128(amount) + ); + } + + // destination is AccountID20 address + function TransferTokenToAddress20(address token, ParaID paraID, bytes20 recipient, uint128 xcmFee, uint128 amount) + internal + view + returns (bytes memory) + { + return bytes.concat( + bytes1(0x00), + ScaleCodec.encodeU64(uint64(block.chainid)), + bytes1(0x02), + SubstrateTypes.H160(token), + bytes1(0x02), + ScaleCodec.encodeU32(uint32(ParaID.unwrap(paraID))), + recipient, + ScaleCodec.encodeU128(xcmFee), + ScaleCodec.encodeU128(amount) + ); + } } diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index fe78e9b55c..66ac7b5b9a 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -109,6 +109,7 @@ struct Ticket { struct TokenInfo { bool isRegistered; + bool isForeign; bytes32 tokenID; bytes32 agentID; address token; diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 70a6cd2de9..0b3f63470c 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.23; import {OperatingMode, InboundMessage, ParaID, ChannelID, MultiAddress} from "../Types.sol"; import {Verification} from "../Verification.sol"; import {UD60x18} from "prb/math/src/UD60x18.sol"; +import {TokenInfo} from "../storage/AssetsStorage.sol"; interface IGateway { /** @@ -108,4 +109,25 @@ interface IGateway { uint128 destinationFee, uint128 amount ) external payable; + + /// @dev Transfer polkadot native tokens back + function transferToken( + address token, + ParaID destinationChain, + MultiAddress calldata destinationAddress, + uint128 destinationFee, + uint128 amount + ) external payable; + + /// @dev Get tokenInfo by tokenID + function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory); + + /// @dev Emitted once the polkadot native tokens are burnt and an outbound message is successfully queued. + event TokenTransfered( + address indexed token, + address indexed sender, + ParaID indexed destinationChain, + MultiAddress destinationAddress, + uint128 amount + ); } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 7c820e3c85..9b7dea90ff 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -22,6 +22,7 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; +import {TokenInfo} from "../src/storage/AssetsStorage.sol"; import { UpgradeParams, @@ -95,6 +96,9 @@ contract GatewayTest is Test { UD60x18 public exchangeRate = ud60x18(0.0025e18); UD60x18 public multiplier = ud60x18(1e18); + // tokenID for DOT + bytes32 public dotTokenID; + function setUp() public { AgentExecutor executor = new AgentExecutor(); gatewayLogic = @@ -138,6 +142,8 @@ contract GatewayTest is Test { recipientAddress32 = multiAddressFromBytes32(keccak256("recipient")); recipientAddress20 = multiAddressFromBytes20(bytes20(keccak256("recipient"))); + + dotTokenID = bytes32(uint256(1)); } function makeCreateAgentCommand() public pure returns (Command, bytes memory) { @@ -913,10 +919,10 @@ contract GatewayTest is Test { IGateway(address(gateway)).sendToken{value: fee}(address(token), destPara, recipientAddress32, 0, 1); } - function testAgentRegisterToken() public { + function testAgentRegisterDot() public { AgentExecuteParams memory params = AgentExecuteParams({ agentID: assetHubAgentID, - payload: abi.encode(AgentExecuteCommand.RegisterToken, abi.encode(bytes32(uint256(1)), "DOT", "DOT", 10)) + payload: abi.encode(AgentExecuteCommand.RegisterToken, abi.encode(dotTokenID, "DOT", "DOT", 10)) }); vm.expectEmit(true, true, false, false); @@ -925,8 +931,8 @@ contract GatewayTest is Test { GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); } - function testAgentMintToken() public { - testAgentRegisterToken(); + function testAgentMintDot() public { + testAgentRegisterDot(); AgentExecuteParams memory params = AgentExecuteParams({ agentID: assetHubAgentID, @@ -938,4 +944,26 @@ contract GatewayTest is Test { GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); } + + function testTransferDotToAssetHub() public { + // Register and then mint some DOT to account1 + testAgentMintDot(); + + TokenInfo memory info = IGateway(address(gateway)).getTokenInfo(dotTokenID); + + ParaID destPara = assetHubParaID; + + vm.prank(account1); + + vm.expectEmit(true, true, false, true); + emit IGateway.TokenTransfered(address(info.token), account1, destPara, recipientAddress32, 1); + + // Expect the gateway to emit `OutboundMessageAccepted` + vm.expectEmit(true, false, false, false); + emit IGateway.OutboundMessageAccepted(assetHubParaID.into(), 1, messageID, bytes("")); + + IGateway(address(gateway)).transferToken{value: 0.1 ether}( + address(info.token), destPara, recipientAddress32, 1, 1 + ); + } } diff --git a/contracts/test/mocks/GatewayUpgradeMock.sol b/contracts/test/mocks/GatewayUpgradeMock.sol index 130ba07841..a34d87b05e 100644 --- a/contracts/test/mocks/GatewayUpgradeMock.sol +++ b/contracts/test/mocks/GatewayUpgradeMock.sol @@ -7,6 +7,7 @@ import {IGateway} from "../../src/interfaces/IGateway.sol"; import {IInitializable} from "../../src/interfaces/IInitializable.sol"; import {Verification} from "../../src/Verification.sol"; import {UD60x18, convert} from "prb/math/src/UD60x18.sol"; +import {TokenInfo} from "../../src/storage/AssetsStorage.sol"; contract GatewayUpgradeMock is IGateway, IInitializable { /** @@ -61,4 +62,18 @@ contract GatewayUpgradeMock is IGateway, IInitializable { function pricingParameters() external pure returns (UD60x18, uint128) { return (convert(0), uint128(0)); } + + function getTokenInfo(bytes32) external pure returns (TokenInfo memory) { + TokenInfo memory info = + TokenInfo({isRegistered: true, isForeign: true, tokenID: 0x0, agentID: 0x0, token: address(0x0)}); + return info; + } + + function transferToken( + address token, + ParaID destinationChain, + MultiAddress calldata destinationAddress, + uint128 destinationFee, + uint128 amount + ) external payable {} } From d28d9df99c8a0261f5db1861628c167cf443ddd8 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 25 Mar 2024 18:46:18 +0800 Subject: [PATCH 06/55] Cleanup for not exceed the size limit --- contracts/src/Assets.sol | 7 +++---- contracts/src/Gateway.sol | 18 +----------------- contracts/src/interfaces/IGateway.sol | 4 ++-- contracts/test/Gateway.t.sol | 2 +- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 0011ec9b64..701adb5f23 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -186,7 +186,7 @@ library Assets { } // @dev Register a new fungible Polkadot token for an agent - function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) internal { + function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) external { AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == true) { revert TokenAlreadyRegistered(); @@ -195,11 +195,10 @@ library Assets { TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); $.tokenRegistry[token] = info; $.tokenRegistryByID[tokenID] = info; - emit IGateway.TokenRegistered(tokenID, agentID, token); } // @dev Get token address by tokenID - function getTokenInfo(bytes32 tokenID) internal view returns (TokenInfo memory) { + function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == false) { revert TokenNotRegistered(); @@ -216,7 +215,7 @@ library Assets { MultiAddress calldata destinationAddress, uint128 destinationChainFee, uint128 amount - ) internal returns (Ticket memory ticket) { + ) external returns (Ticket memory ticket) { AssetsStorage.Layout storage $asset = AssetsStorage.layout(); TokenInfo storage info = $asset.tokenRegistry[token]; diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index cfa35552be..4d75dbbf4d 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -98,15 +98,6 @@ contract Gateway is IGateway, IInitializable { _; } - // handler functions are privileged from agent only - modifier onlyAgent(bytes32 agentID) { - bytes32 _agentID = _ensureAgentAddress(msg.sender); - if (_agentID != agentID) { - revert Unauthorized(); - } - _; - } - constructor( address beefyClient, address agentExecutor, @@ -289,6 +280,7 @@ contract Gateway is IGateway, IInitializable { (bytes memory result) = abi.decode(returndata, (bytes)); (bytes32 tokenID, address token) = abi.decode(result, (bytes32, address)); Assets.registerTokenByID(tokenID, token, params.agentID); + emit IGateway.ForeignTokenRegistered(tokenID, params.agentID, token); } } @@ -575,14 +567,6 @@ contract Gateway is IGateway, IInitializable { } } - /// @dev Ensure that the specified address is an valid agent - function _ensureAgentAddress(address agent) internal view returns (bytes32 agentID) { - agentID = CoreStorage.layout().agentAddresses[agent]; - if (agentID == bytes32(0)) { - revert AgentDoesNotExist(); - } - } - /// @dev Invoke some code within an agent function _invokeOnAgent(address agent, bytes memory data) internal returns (bytes memory) { (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(AGENT_EXECUTOR, data)); diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 0b3f63470c..8446d1df7e 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -39,8 +39,8 @@ interface IGateway { // Emitted when funds are withdrawn from an agent event AgentFundsWithdrawn(bytes32 indexed agentID, address indexed recipient, uint256 amount); - // Emitted when token registed - event TokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token); + // Emitted when foreign token from polkadot registed + event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token); /** * Getters diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 9b7dea90ff..e66b1d3d16 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -926,7 +926,7 @@ contract GatewayTest is Test { }); vm.expectEmit(true, true, false, false); - emit IGateway.TokenRegistered(bytes32(uint256(1)), assetHubAgentID, address(0)); + emit IGateway.ForeignTokenRegistered(bytes32(uint256(1)), assetHubAgentID, address(0)); GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); } From c23f8bb4d3d094b9cde384e924f5555f1a5c8cbb Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 26 Mar 2024 09:47:56 +0800 Subject: [PATCH 07/55] Add smoke test for register polkadot token --- contracts/test/Gateway.t.sol | 33 +- relayer/contracts/gateway.go | 373 ++++++++++++++++++++- smoketest/src/helper.rs | 42 ++- smoketest/tests/register_polkadot_token.rs | 22 ++ 4 files changed, 462 insertions(+), 8 deletions(-) create mode 100644 smoketest/tests/register_polkadot_token.rs diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index e66b1d3d16..d52d9b574f 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -54,12 +54,12 @@ contract GatewayTest is Test { // Emitted when token minted event TokenMinted(bytes32 indexed tokenID, address token, address recipient, uint256 amount); - ParaID public bridgeHubParaID = ParaID.wrap(1001); - bytes32 public bridgeHubAgentID = keccak256("1001"); + ParaID public bridgeHubParaID = ParaID.wrap(1013); + bytes32 public bridgeHubAgentID = 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314; address public bridgeHubAgent; - ParaID public assetHubParaID = ParaID.wrap(1002); - bytes32 public assetHubAgentID = keccak256("1002"); + ParaID public assetHubParaID = ParaID.wrap(1000); + bytes32 public assetHubAgentID = 0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79; address public assetHubAgent; address public relayer; @@ -966,4 +966,29 @@ contract GatewayTest is Test { address(info.token), destPara, recipientAddress32, 1, 1 ); } + + function testParseAgentExecuteCall() public { + bytes memory data = + hex"000000000000000000000000000000000000000000000000000000000000002081c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001008080778c30c20fa2ebc0ed18d2cbca1f30b027625c7d9d97f5d589721c91aeb6000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000003646f7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003646f740000000000000000000000000000000000000000000000000000000000"; + + AgentExecuteParams memory params = abi.decode(data, (AgentExecuteParams)); + + (AgentExecuteCommand command, bytes memory payload) = abi.decode(params.payload, (AgentExecuteCommand, bytes)); + + //Register foreign token + assertEq(uint256(command), uint256(1)); + + (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = + abi.decode(payload, (bytes32, string, string, uint8)); + assertEq(tokenID, 0x8080778c30c20fa2ebc0ed18d2cbca1f30b027625c7d9d97f5d589721c91aeb6); + + console.log("name:%s", name); + console.log("symbol:%s", symbol); + console.log("decimals:%s", decimals); + + vm.expectEmit(true, true, false, false); + emit IGateway.ForeignTokenRegistered(tokenID, assetHubAgentID, address(0)); + + GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + } } diff --git a/relayer/contracts/gateway.go b/relayer/contracts/gateway.go index 814a6a5017..59acd84dcc 100644 --- a/relayer/contracts/gateway.go +++ b/relayer/contracts/gateway.go @@ -47,6 +47,15 @@ type MultiAddress struct { Data []byte } +// TokenInfo is an auto generated low-level Go binding around an user-defined struct. +type TokenInfo struct { + IsRegistered bool + IsForeign bool + TokenID [32]byte + AgentID [32]byte + Token common.Address +} + // VerificationDigestItem is an auto generated low-level Go binding around an user-defined struct. type VerificationDigestItem struct { Kind *big.Int @@ -91,7 +100,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\":\"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\":\"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},{\"type\":\"event\",\"name\":\"Upgraded\",\"inputs\":[{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"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\":\"getTokenInfo\",\"inputs\":[{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"internalType\":\"structTokenInfo\",\"components\":[{\"name\":\"isRegistered\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"isForeign\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"agentID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"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\":\"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\":\"transferToken\",\"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\":\"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\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"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},{\"type\":\"event\",\"name\":\"TokenTransfered\",\"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\":\"Upgraded\",\"inputs\":[{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false}]", } // GatewayABI is the input ABI used to generate the binding from. @@ -334,6 +343,37 @@ func (_Gateway *GatewayCallerSession) ChannelOperatingModeOf(channelID [32]byte) return _Gateway.Contract.ChannelOperatingModeOf(&_Gateway.CallOpts, channelID) } +// GetTokenInfo is a free data retrieval call binding the contract method 0x2d8b70a1. +// +// Solidity: function getTokenInfo(bytes32 tokenID) view returns((bool,bool,bytes32,bytes32,address)) +func (_Gateway *GatewayCaller) GetTokenInfo(opts *bind.CallOpts, tokenID [32]byte) (TokenInfo, error) { + var out []interface{} + err := _Gateway.contract.Call(opts, &out, "getTokenInfo", tokenID) + + if err != nil { + return *new(TokenInfo), err + } + + out0 := *abi.ConvertType(out[0], new(TokenInfo)).(*TokenInfo) + + return out0, err + +} + +// GetTokenInfo is a free data retrieval call binding the contract method 0x2d8b70a1. +// +// Solidity: function getTokenInfo(bytes32 tokenID) view returns((bool,bool,bytes32,bytes32,address)) +func (_Gateway *GatewaySession) GetTokenInfo(tokenID [32]byte) (TokenInfo, error) { + return _Gateway.Contract.GetTokenInfo(&_Gateway.CallOpts, tokenID) +} + +// GetTokenInfo is a free data retrieval call binding the contract method 0x2d8b70a1. +// +// Solidity: function getTokenInfo(bytes32 tokenID) view returns((bool,bool,bytes32,bytes32,address)) +func (_Gateway *GatewayCallerSession) GetTokenInfo(tokenID [32]byte) (TokenInfo, error) { + return _Gateway.Contract.GetTokenInfo(&_Gateway.CallOpts, tokenID) +} + // Implementation is a free data retrieval call binding the contract method 0x5c60da1b. // // Solidity: function implementation() view returns(address) @@ -584,6 +624,27 @@ func (_Gateway *GatewayTransactorSession) SubmitV1(message InboundMessage, leafP return _Gateway.Contract.SubmitV1(&_Gateway.TransactOpts, message, leafProof, headerProof) } +// TransferToken is a paid mutator transaction binding the contract method 0x1382f5eb. +// +// Solidity: function transferToken(address token, uint32 destinationChain, (uint8,bytes) destinationAddress, uint128 destinationFee, uint128 amount) payable returns() +func (_Gateway *GatewayTransactor) TransferToken(opts *bind.TransactOpts, token common.Address, destinationChain uint32, destinationAddress MultiAddress, destinationFee *big.Int, amount *big.Int) (*types.Transaction, error) { + return _Gateway.contract.Transact(opts, "transferToken", token, destinationChain, destinationAddress, destinationFee, amount) +} + +// TransferToken is a paid mutator transaction binding the contract method 0x1382f5eb. +// +// Solidity: function transferToken(address token, uint32 destinationChain, (uint8,bytes) destinationAddress, uint128 destinationFee, uint128 amount) payable returns() +func (_Gateway *GatewaySession) TransferToken(token common.Address, destinationChain uint32, destinationAddress MultiAddress, destinationFee *big.Int, amount *big.Int) (*types.Transaction, error) { + return _Gateway.Contract.TransferToken(&_Gateway.TransactOpts, token, destinationChain, destinationAddress, destinationFee, amount) +} + +// TransferToken is a paid mutator transaction binding the contract method 0x1382f5eb. +// +// Solidity: function transferToken(address token, uint32 destinationChain, (uint8,bytes) destinationAddress, uint128 destinationFee, uint128 amount) payable returns() +func (_Gateway *GatewayTransactorSession) TransferToken(token common.Address, destinationChain uint32, destinationAddress MultiAddress, destinationFee *big.Int, amount *big.Int) (*types.Transaction, error) { + return _Gateway.Contract.TransferToken(&_Gateway.TransactOpts, token, destinationChain, destinationAddress, destinationFee, amount) +} + // GatewayAgentCreatedIterator is returned from FilterAgentCreated and is used to iterate over the raw logs and unpacked data for AgentCreated events raised by the Gateway contract. type GatewayAgentCreatedIterator struct { Event *GatewayAgentCreated // Event containing the contract specifics and raw log @@ -1161,6 +1222,152 @@ func (_Gateway *GatewayFilterer) ParseChannelUpdated(log types.Log) (*GatewayCha 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 + + 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 *GatewayForeignTokenRegisteredIterator) 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(GatewayForeignTokenRegistered) + 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(GatewayForeignTokenRegistered) + 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 *GatewayForeignTokenRegisteredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *GatewayForeignTokenRegisteredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// GatewayForeignTokenRegistered represents a ForeignTokenRegistered event raised by the Gateway contract. +type GatewayForeignTokenRegistered struct { + TokenID [32]byte + AgentID [32]byte + Token common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterForeignTokenRegistered is a free log retrieval operation binding the contract event 0xc4a9ffa44bcfe101c1450dc2e08360693767d951c4a5c726e36cde0371dc4ddb. +// +// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token) +func (_Gateway *GatewayFilterer) FilterForeignTokenRegistered(opts *bind.FilterOpts, tokenID [][32]byte) (*GatewayForeignTokenRegisteredIterator, error) { + + var tokenIDRule []interface{} + for _, tokenIDItem := range tokenID { + tokenIDRule = append(tokenIDRule, tokenIDItem) + } + + logs, sub, err := _Gateway.contract.FilterLogs(opts, "ForeignTokenRegistered", tokenIDRule) + if err != nil { + return nil, err + } + return &GatewayForeignTokenRegisteredIterator{contract: _Gateway.contract, event: "ForeignTokenRegistered", logs: logs, sub: sub}, nil +} + +// WatchForeignTokenRegistered is a free log subscription operation binding the contract event 0xc4a9ffa44bcfe101c1450dc2e08360693767d951c4a5c726e36cde0371dc4ddb. +// +// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token) +func (_Gateway *GatewayFilterer) WatchForeignTokenRegistered(opts *bind.WatchOpts, sink chan<- *GatewayForeignTokenRegistered, tokenID [][32]byte) (event.Subscription, error) { + + var tokenIDRule []interface{} + for _, tokenIDItem := range tokenID { + tokenIDRule = append(tokenIDRule, tokenIDItem) + } + + logs, sub, err := _Gateway.contract.WatchLogs(opts, "ForeignTokenRegistered", tokenIDRule) + 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(GatewayForeignTokenRegistered) + if err := _Gateway.contract.UnpackLog(event, "ForeignTokenRegistered", 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 +} + +// ParseForeignTokenRegistered is a log parse operation binding the contract event 0xc4a9ffa44bcfe101c1450dc2e08360693767d951c4a5c726e36cde0371dc4ddb. +// +// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token) +func (_Gateway *GatewayFilterer) ParseForeignTokenRegistered(log types.Log) (*GatewayForeignTokenRegistered, error) { + event := new(GatewayForeignTokenRegistered) + if err := _Gateway.contract.UnpackLog(event, "ForeignTokenRegistered", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // GatewayInboundMessageDispatchedIterator is returned from FilterInboundMessageDispatched and is used to iterate over the raw logs and unpacked data for InboundMessageDispatched events raised by the Gateway contract. type GatewayInboundMessageDispatchedIterator struct { Event *GatewayInboundMessageDispatched // Event containing the contract specifics and raw log @@ -2173,6 +2380,170 @@ func (_Gateway *GatewayFilterer) ParseTokenTransferFeesChanged(log types.Log) (* return event, nil } +// GatewayTokenTransferedIterator is returned from FilterTokenTransfered and is used to iterate over the raw logs and unpacked data for TokenTransfered events raised by the Gateway contract. +type GatewayTokenTransferedIterator struct { + Event *GatewayTokenTransfered // 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 *GatewayTokenTransferedIterator) 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(GatewayTokenTransfered) + 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(GatewayTokenTransfered) + 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 *GatewayTokenTransferedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *GatewayTokenTransferedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// GatewayTokenTransfered represents a TokenTransfered event raised by the Gateway contract. +type GatewayTokenTransfered struct { + Token common.Address + Sender common.Address + DestinationChain uint32 + DestinationAddress MultiAddress + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterTokenTransfered is a free log retrieval operation binding the contract event 0x83c992bcd5b2b18b0cb4ceb131a37c34e5b795f5e00e7058b99a33e9bcf1ee47. +// +// Solidity: event TokenTransfered(address indexed token, address indexed sender, uint32 indexed destinationChain, (uint8,bytes) destinationAddress, uint128 amount) +func (_Gateway *GatewayFilterer) FilterTokenTransfered(opts *bind.FilterOpts, token []common.Address, sender []common.Address, destinationChain []uint32) (*GatewayTokenTransferedIterator, error) { + + var tokenRule []interface{} + for _, tokenItem := range token { + tokenRule = append(tokenRule, tokenItem) + } + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + var destinationChainRule []interface{} + for _, destinationChainItem := range destinationChain { + destinationChainRule = append(destinationChainRule, destinationChainItem) + } + + logs, sub, err := _Gateway.contract.FilterLogs(opts, "TokenTransfered", tokenRule, senderRule, destinationChainRule) + if err != nil { + return nil, err + } + return &GatewayTokenTransferedIterator{contract: _Gateway.contract, event: "TokenTransfered", logs: logs, sub: sub}, nil +} + +// WatchTokenTransfered is a free log subscription operation binding the contract event 0x83c992bcd5b2b18b0cb4ceb131a37c34e5b795f5e00e7058b99a33e9bcf1ee47. +// +// Solidity: event TokenTransfered(address indexed token, address indexed sender, uint32 indexed destinationChain, (uint8,bytes) destinationAddress, uint128 amount) +func (_Gateway *GatewayFilterer) WatchTokenTransfered(opts *bind.WatchOpts, sink chan<- *GatewayTokenTransfered, token []common.Address, sender []common.Address, destinationChain []uint32) (event.Subscription, error) { + + var tokenRule []interface{} + for _, tokenItem := range token { + tokenRule = append(tokenRule, tokenItem) + } + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + var destinationChainRule []interface{} + for _, destinationChainItem := range destinationChain { + destinationChainRule = append(destinationChainRule, destinationChainItem) + } + + logs, sub, err := _Gateway.contract.WatchLogs(opts, "TokenTransfered", tokenRule, senderRule, destinationChainRule) + 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(GatewayTokenTransfered) + if err := _Gateway.contract.UnpackLog(event, "TokenTransfered", 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 +} + +// ParseTokenTransfered is a log parse operation binding the contract event 0x83c992bcd5b2b18b0cb4ceb131a37c34e5b795f5e00e7058b99a33e9bcf1ee47. +// +// Solidity: event TokenTransfered(address indexed token, address indexed sender, uint32 indexed destinationChain, (uint8,bytes) destinationAddress, uint128 amount) +func (_Gateway *GatewayFilterer) ParseTokenTransfered(log types.Log) (*GatewayTokenTransfered, error) { + event := new(GatewayTokenTransfered) + if err := _Gateway.contract.UnpackLog(event, "TokenTransfered", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // GatewayUpgradedIterator is returned from FilterUpgraded and is used to iterate over the raw logs and unpacked data for Upgraded events raised by the Gateway contract. type GatewayUpgradedIterator struct { Event *GatewayUpgraded // Event containing the contract specifics and raw log diff --git a/smoketest/src/helper.rs b/smoketest/src/helper.rs index 26c0b54f13..e470245f45 100644 --- a/smoketest/src/helper.rs +++ b/smoketest/src/helper.rs @@ -2,9 +2,20 @@ use crate::{ constants::*, contracts::i_gateway, parachains::{ - bridgehub::{self, api::runtime_types::snowbridge_core::outbound::v1::OperatingMode}, + bridgehub::{ + self, + api::{ + runtime_types, + runtime_types::{ + snowbridge_core::outbound::v1::OperatingMode, xcm::VersionedLocation, + }, + }, + }, penpal::{ - api::{runtime_types as penpalTypes, runtime_types::xcm::VersionedLocation}, + api::{ + runtime_types as penpalTypes, + runtime_types::xcm::VersionedLocation as penpalVersionLocation, + }, {self}, }, relaychain, @@ -185,7 +196,7 @@ pub async fn send_sudo_xcm_transact( penpal_client: &Box>, message: Box, ) -> Result, Box> { - let dest = Box::new(VersionedLocation::V3(MultiLocation { + let dest = Box::new(penpalVersionLocation::V3(MultiLocation { parents: 1, interior: Junctions::X1(Junction::Parachain(BRIDGE_HUB_PARA_ID)), })); @@ -353,3 +364,28 @@ pub fn print_event_log_for_unit_tests(log: &Log) { println!("}}") } + +pub async fn construct_register_relay_token_call( + bridge_hub_client: &Box>, +) -> Result, Box> { + type Junctions = runtime_types::staging_xcm::v4::junctions::Junctions; + type Junction = runtime_types::staging_xcm::v4::junction::Junction; + let location = VersionedLocation::V4(runtime_types::staging_xcm::v4::location::Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(ASSET_HUB_PARA_ID)]), + }); + let asset = VersionedLocation::V4(runtime_types::staging_xcm::v4::location::Location { + parents: 1, + interior: Junctions::Here, + }); + let metadata = runtime_types::snowbridge_core::AssetRegistrarMetadata { + name: "dot".as_bytes().to_vec(), + symbol: "dot".as_bytes().to_vec(), + decimals: 10, + }; + let call = bridgehub::api::ethereum_system::calls::TransactionApi + .force_register_token(location, asset, metadata) + .encode_call_data(&bridge_hub_client.metadata())?; + + Ok(call) +} diff --git a/smoketest/tests/register_polkadot_token.rs b/smoketest/tests/register_polkadot_token.rs new file mode 100644 index 0000000000..6ba466c672 --- /dev/null +++ b/smoketest/tests/register_polkadot_token.rs @@ -0,0 +1,22 @@ +use snowbridge_smoketest::{ + contracts::i_gateway::ForeignTokenRegisteredFilter, helper::*, + parachains::bridgehub::api::ethereum_system::events::RegisterToken, +}; + +#[tokio::test] +async fn register_polkadot_token() { + let test_clients = initial_clients().await.expect("initialize clients"); + + let encoded_call = construct_register_relay_token_call(&test_clients.bridge_hub_client) + .await + .expect("construct inner call."); + + governance_bridgehub_call_from_relay_chain(encoded_call) + .await + .expect("set token fees"); + + wait_for_bridgehub_event::(&test_clients.bridge_hub_client).await; + + wait_for_ethereum_event::(&test_clients.ethereum_client).await; + +} From 8c8afac9dd8ba3622cf7bea2d0c5777ffc86691a Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 26 Mar 2024 11:16:02 +0800 Subject: [PATCH 08/55] Transfer relay token to Ethereum --- smoketest/make-bindings.sh | 2 +- smoketest/src/constants.rs | 1 + smoketest/src/helper.rs | 13 +-- smoketest/tests/transfer_polkadot_token.rs | 109 +++++++++++++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 smoketest/tests/transfer_polkadot_token.rs diff --git a/smoketest/make-bindings.sh b/smoketest/make-bindings.sh index b6fb1cf0d4..9f19656d40 100755 --- a/smoketest/make-bindings.sh +++ b/smoketest/make-bindings.sh @@ -6,7 +6,7 @@ mkdir -p src/contracts # Generate Rust bindings for contracts forge bind --module --overwrite \ - --select 'IGateway|WETH9|GatewayUpgradeMock' \ + --select 'IGateway|WETH9|GatewayUpgradeMock|AgentExecutor' \ --bindings-path src/contracts \ --root ../contracts diff --git a/smoketest/src/constants.rs b/smoketest/src/constants.rs index f72404a595..5bd625b37d 100644 --- a/smoketest/src/constants.rs +++ b/smoketest/src/constants.rs @@ -25,6 +25,7 @@ pub const ETHEREUM_ADDRESS: [u8; 20] = hex!("90A987B944Cb1dCcE5564e5FDeCD7a54D3d // the order in contracts are deployed in DeployScript.sol. pub const GATEWAY_PROXY_CONTRACT: [u8; 20] = hex!("EDa338E4dC46038493b885327842fD3E301CaB39"); pub const WETH_CONTRACT: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); +pub const AGENT_EXECUTOR_CONTRACT: [u8; 20] = hex!("2ffA5ecdBe006d30397c7636d3e015EEE251369F"); // Agent for bridge hub parachain 1013 pub const BRIDGE_HUB_AGENT_ID: [u8; 32] = diff --git a/smoketest/src/helper.rs b/smoketest/src/helper.rs index e470245f45..f67a04e66a 100644 --- a/smoketest/src/helper.rs +++ b/smoketest/src/helper.rs @@ -7,7 +7,8 @@ use crate::{ api::{ runtime_types, runtime_types::{ - snowbridge_core::outbound::v1::OperatingMode, xcm::VersionedLocation, + snowbridge_core::outbound::v1::OperatingMode, + staging_xcm::v4::junction::NetworkId, xcm::VersionedLocation, }, }, }, @@ -172,7 +173,7 @@ pub async fn wait_for_ethereum_event(ethereum_client: &Box::connect(ETHEREUM_API) + .await + .unwrap() + .interval(Duration::from_millis(10u64)); + + let ethereum_client = Arc::new(ethereum_provider); + + let gateway = IGateway::new(GATEWAY_PROXY_CONTRACT, ethereum_client.clone()); + let _agent_src = + gateway.agent_of(ASSET_HUB_AGENT_ID).await.expect("could not get agent address"); + + let assethub: OnlineClient = + OnlineClient::from_url(ASSET_HUB_WS_URL).await.unwrap(); + + let amount: u128 = 1_000_000_000; + let assets = VersionedAssets::V3(MultiAssets(vec![MultiAsset { + id: AssetId::Concrete(MultiLocation { parents: 1, interior: Junctions::Here }), + 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: DESTINATION_ADDRESS.into(), + }), + }); + + let signer = dev::bob(); + + let token_transfer_call = + TransactionApi.reserve_transfer_assets(destination, beneficiary, assets, 0); + + let _ = assethub + .tx() + .sign_and_submit_then_watch_default(&token_transfer_call, &signer) + .await + .expect("call success"); + + let agent_executor_addr: Address = AGENT_EXECUTOR_CONTRACT.into(); + let agent_executor = + agent_executor::AgentExecutor::new(agent_executor_addr, ethereum_client.clone()); + + 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) = agent_executor + .event::() + .at_block_hash(block.hash.unwrap()) + .query() + .await + { + for transfer in transfers { + println!("Transfer event found at ethereum block {:?}", block.number.unwrap()); + println!("token id {:?}", transfer.token_id); + transfer_event_found = true; + } + } + if transfer_event_found { + break + } + } + assert!(transfer_event_found); +} From 0b3344cb9e35e6df5ad9c745fd24ef1559635aab Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 26 Mar 2024 17:50:12 +0800 Subject: [PATCH 09/55] Reuse IGateway.sendToken for polkadot native asset --- contracts/src/Assets.sol | 26 +++------------- contracts/src/Gateway.sol | 34 ++++++++++----------- contracts/src/interfaces/IGateway.sol | 14 +++------ contracts/test/Gateway.t.sol | 9 +++--- contracts/test/mocks/GatewayUpgradeMock.sol | 8 ----- 5 files changed, 30 insertions(+), 61 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 701adb5f23..1e375c98f1 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -90,14 +90,6 @@ library Assets { ) external returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - TokenInfo storage info = $.tokenRegistry[token]; - if (!info.isRegistered) { - revert TokenNotRegistered(); - } - if (info.isForeign) { - revert InvalidToken(); - } - // Lock the funds into AssetHub's agent contract _transferToAgent($.assetHubAgent, token, sender, amount); @@ -209,23 +201,13 @@ library Assets { // @dev Transfer polkadot native tokens back function transferToken( address executor, - address token, + TokenInfo memory info, address sender, ParaID destinationChain, MultiAddress calldata destinationAddress, uint128 destinationChainFee, uint128 amount ) external returns (Ticket memory ticket) { - AssetsStorage.Layout storage $asset = AssetsStorage.layout(); - - TokenInfo storage info = $asset.tokenRegistry[token]; - if (!info.isRegistered) { - revert TokenNotRegistered(); - } - if (!info.isForeign) { - revert InvalidToken(); - } - CoreStorage.Layout storage $core = CoreStorage.layout(); address agent = $core.agents[info.agentID]; @@ -246,18 +228,18 @@ library Assets { if (destinationAddress.isAddress32()) { // The receiver has a 32-byte account ID ticket.payload = SubstrateTypes.TransferTokenToAddress32( - token, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount + info.token, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount ); } else if (destinationAddress.isAddress20()) { // The receiver has a 20-byte account ID ticket.payload = SubstrateTypes.TransferTokenToAddress20( - token, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount + info.token, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount ); } else { revert Unsupported(); } - emit IGateway.TokenTransfered(token, sender, destinationChain, destinationAddress, amount); + emit IGateway.TokenSent(info.token, sender, destinationChain, destinationAddress, amount); } function _burn(address agentExecutor, address agent, bytes32 tokenID, address sender, uint256 amount) internal { diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 4d75dbbf4d..7b749947d3 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -89,6 +89,7 @@ contract Gateway is IGateway, IInitializable { error InvalidCodeHash(); error InvalidConstructorParams(); error AlreadyInitialized(); + error TokenNotRegistered(); // handler functions are privileged modifier onlySelf() { @@ -439,24 +440,23 @@ contract Gateway is IGateway, IInitializable { uint128 destinationFee, uint128 amount ) external payable { - _submitOutbound( - Assets.sendToken(token, msg.sender, destinationChain, destinationAddress, destinationFee, amount) - ); - } + AssetsStorage.Layout storage $ = AssetsStorage.layout(); - // Transfer polkadot native tokens back - function transferToken( - address token, - ParaID destinationChain, - MultiAddress calldata destinationAddress, - uint128 destinationFee, - uint128 amount - ) external payable { - _submitOutbound( - Assets.transferToken( - AGENT_EXECUTOR, token, msg.sender, destinationChain, destinationAddress, destinationFee, amount - ) - ); + TokenInfo storage info = $.tokenRegistry[token]; + if (!info.isRegistered) { + revert TokenNotRegistered(); + } + if (info.isForeign) { + _submitOutbound( + Assets.transferToken( + AGENT_EXECUTOR, info, msg.sender, destinationChain, destinationAddress, destinationFee, amount + ) + ); + } else { + _submitOutbound( + Assets.sendToken(token, msg.sender, destinationChain, destinationAddress, destinationFee, amount) + ); + } } function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory) { diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 8446d1df7e..e45c331d8a 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -46,10 +46,15 @@ interface IGateway { * Getters */ function operatingMode() external view returns (OperatingMode); + function channelOperatingModeOf(ChannelID channelID) external view returns (OperatingMode); + function channelNoncesOf(ChannelID channelID) external view returns (uint64, uint64); + function agentOf(bytes32 agentID) external view returns (address); + function pricingParameters() external view returns (UD60x18, uint128); + function implementation() external view returns (address); /** @@ -110,15 +115,6 @@ interface IGateway { uint128 amount ) external payable; - /// @dev Transfer polkadot native tokens back - function transferToken( - address token, - ParaID destinationChain, - MultiAddress calldata destinationAddress, - uint128 destinationFee, - uint128 amount - ) external payable; - /// @dev Get tokenInfo by tokenID function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index d52d9b574f..4fc29e161e 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -174,6 +174,7 @@ contract GatewayTest is Test { } fallback() external payable {} + receive() external payable {} /** @@ -945,7 +946,7 @@ contract GatewayTest is Test { GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); } - function testTransferDotToAssetHub() public { + function testSendRelayTokenToAssetHub() public { // Register and then mint some DOT to account1 testAgentMintDot(); @@ -956,15 +957,13 @@ contract GatewayTest is Test { vm.prank(account1); vm.expectEmit(true, true, false, true); - emit IGateway.TokenTransfered(address(info.token), account1, destPara, recipientAddress32, 1); + emit IGateway.TokenSent(address(info.token), account1, destPara, recipientAddress32, 1); // Expect the gateway to emit `OutboundMessageAccepted` vm.expectEmit(true, false, false, false); emit IGateway.OutboundMessageAccepted(assetHubParaID.into(), 1, messageID, bytes("")); - IGateway(address(gateway)).transferToken{value: 0.1 ether}( - address(info.token), destPara, recipientAddress32, 1, 1 - ); + IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(info.token), destPara, recipientAddress32, 1, 1); } function testParseAgentExecuteCall() public { diff --git a/contracts/test/mocks/GatewayUpgradeMock.sol b/contracts/test/mocks/GatewayUpgradeMock.sol index a34d87b05e..de0b64acf6 100644 --- a/contracts/test/mocks/GatewayUpgradeMock.sol +++ b/contracts/test/mocks/GatewayUpgradeMock.sol @@ -68,12 +68,4 @@ contract GatewayUpgradeMock is IGateway, IInitializable { TokenInfo({isRegistered: true, isForeign: true, tokenID: 0x0, agentID: 0x0, token: address(0x0)}); return info; } - - function transferToken( - address token, - ParaID destinationChain, - MultiAddress calldata destinationAddress, - uint128 destinationFee, - uint128 amount - ) external payable {} } From 2d17fca5f9d5f1a617ae816bfcd78d99ae31330b Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 26 Mar 2024 18:00:39 +0800 Subject: [PATCH 10/55] More cleanup --- contracts/src/interfaces/IGateway.sol | 15 +-- relayer/contracts/gateway.go | 187 +------------------------- 2 files changed, 4 insertions(+), 198 deletions(-) diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index e45c331d8a..5d4f2d8b2c 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -102,9 +102,9 @@ interface IGateway { /// 1. Delivery costs to BridgeHub /// 2. XCM execution costs on destinationChain function quoteSendTokenFee(address token, ParaID destinationChain, uint128 destinationFee) - external - view - returns (uint256); + external + view + returns (uint256); /// @dev Send ERC20 tokens to parachain `destinationChain` and deposit into account `destinationAddress` function sendToken( @@ -117,13 +117,4 @@ interface IGateway { /// @dev Get tokenInfo by tokenID function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory); - - /// @dev Emitted once the polkadot native tokens are burnt and an outbound message is successfully queued. - event TokenTransfered( - address indexed token, - address indexed sender, - ParaID indexed destinationChain, - MultiAddress destinationAddress, - uint128 amount - ); } diff --git a/relayer/contracts/gateway.go b/relayer/contracts/gateway.go index 59acd84dcc..e475172bbe 100644 --- a/relayer/contracts/gateway.go +++ b/relayer/contracts/gateway.go @@ -100,7 +100,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\":\"getTokenInfo\",\"inputs\":[{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"internalType\":\"structTokenInfo\",\"components\":[{\"name\":\"isRegistered\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"isForeign\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"agentID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"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\":\"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\":\"transferToken\",\"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\":\"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\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"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},{\"type\":\"event\",\"name\":\"TokenTransfered\",\"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\":\"Upgraded\",\"inputs\":[{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"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\":\"getTokenInfo\",\"inputs\":[{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"internalType\":\"structTokenInfo\",\"components\":[{\"name\":\"isRegistered\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"isForeign\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"agentID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"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\":\"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\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"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},{\"type\":\"event\",\"name\":\"Upgraded\",\"inputs\":[{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false}]", } // GatewayABI is the input ABI used to generate the binding from. @@ -624,27 +624,6 @@ func (_Gateway *GatewayTransactorSession) SubmitV1(message InboundMessage, leafP return _Gateway.Contract.SubmitV1(&_Gateway.TransactOpts, message, leafProof, headerProof) } -// TransferToken is a paid mutator transaction binding the contract method 0x1382f5eb. -// -// Solidity: function transferToken(address token, uint32 destinationChain, (uint8,bytes) destinationAddress, uint128 destinationFee, uint128 amount) payable returns() -func (_Gateway *GatewayTransactor) TransferToken(opts *bind.TransactOpts, token common.Address, destinationChain uint32, destinationAddress MultiAddress, destinationFee *big.Int, amount *big.Int) (*types.Transaction, error) { - return _Gateway.contract.Transact(opts, "transferToken", token, destinationChain, destinationAddress, destinationFee, amount) -} - -// TransferToken is a paid mutator transaction binding the contract method 0x1382f5eb. -// -// Solidity: function transferToken(address token, uint32 destinationChain, (uint8,bytes) destinationAddress, uint128 destinationFee, uint128 amount) payable returns() -func (_Gateway *GatewaySession) TransferToken(token common.Address, destinationChain uint32, destinationAddress MultiAddress, destinationFee *big.Int, amount *big.Int) (*types.Transaction, error) { - return _Gateway.Contract.TransferToken(&_Gateway.TransactOpts, token, destinationChain, destinationAddress, destinationFee, amount) -} - -// TransferToken is a paid mutator transaction binding the contract method 0x1382f5eb. -// -// Solidity: function transferToken(address token, uint32 destinationChain, (uint8,bytes) destinationAddress, uint128 destinationFee, uint128 amount) payable returns() -func (_Gateway *GatewayTransactorSession) TransferToken(token common.Address, destinationChain uint32, destinationAddress MultiAddress, destinationFee *big.Int, amount *big.Int) (*types.Transaction, error) { - return _Gateway.Contract.TransferToken(&_Gateway.TransactOpts, token, destinationChain, destinationAddress, destinationFee, amount) -} - // GatewayAgentCreatedIterator is returned from FilterAgentCreated and is used to iterate over the raw logs and unpacked data for AgentCreated events raised by the Gateway contract. type GatewayAgentCreatedIterator struct { Event *GatewayAgentCreated // Event containing the contract specifics and raw log @@ -2380,170 +2359,6 @@ func (_Gateway *GatewayFilterer) ParseTokenTransferFeesChanged(log types.Log) (* return event, nil } -// GatewayTokenTransferedIterator is returned from FilterTokenTransfered and is used to iterate over the raw logs and unpacked data for TokenTransfered events raised by the Gateway contract. -type GatewayTokenTransferedIterator struct { - Event *GatewayTokenTransfered // 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 *GatewayTokenTransferedIterator) 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(GatewayTokenTransfered) - 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(GatewayTokenTransfered) - 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 *GatewayTokenTransferedIterator) Error() error { - return it.fail -} - -// Close terminates the iteration process, releasing any pending underlying -// resources. -func (it *GatewayTokenTransferedIterator) Close() error { - it.sub.Unsubscribe() - return nil -} - -// GatewayTokenTransfered represents a TokenTransfered event raised by the Gateway contract. -type GatewayTokenTransfered struct { - Token common.Address - Sender common.Address - DestinationChain uint32 - DestinationAddress MultiAddress - Amount *big.Int - Raw types.Log // Blockchain specific contextual infos -} - -// FilterTokenTransfered is a free log retrieval operation binding the contract event 0x83c992bcd5b2b18b0cb4ceb131a37c34e5b795f5e00e7058b99a33e9bcf1ee47. -// -// Solidity: event TokenTransfered(address indexed token, address indexed sender, uint32 indexed destinationChain, (uint8,bytes) destinationAddress, uint128 amount) -func (_Gateway *GatewayFilterer) FilterTokenTransfered(opts *bind.FilterOpts, token []common.Address, sender []common.Address, destinationChain []uint32) (*GatewayTokenTransferedIterator, error) { - - var tokenRule []interface{} - for _, tokenItem := range token { - tokenRule = append(tokenRule, tokenItem) - } - var senderRule []interface{} - for _, senderItem := range sender { - senderRule = append(senderRule, senderItem) - } - var destinationChainRule []interface{} - for _, destinationChainItem := range destinationChain { - destinationChainRule = append(destinationChainRule, destinationChainItem) - } - - logs, sub, err := _Gateway.contract.FilterLogs(opts, "TokenTransfered", tokenRule, senderRule, destinationChainRule) - if err != nil { - return nil, err - } - return &GatewayTokenTransferedIterator{contract: _Gateway.contract, event: "TokenTransfered", logs: logs, sub: sub}, nil -} - -// WatchTokenTransfered is a free log subscription operation binding the contract event 0x83c992bcd5b2b18b0cb4ceb131a37c34e5b795f5e00e7058b99a33e9bcf1ee47. -// -// Solidity: event TokenTransfered(address indexed token, address indexed sender, uint32 indexed destinationChain, (uint8,bytes) destinationAddress, uint128 amount) -func (_Gateway *GatewayFilterer) WatchTokenTransfered(opts *bind.WatchOpts, sink chan<- *GatewayTokenTransfered, token []common.Address, sender []common.Address, destinationChain []uint32) (event.Subscription, error) { - - var tokenRule []interface{} - for _, tokenItem := range token { - tokenRule = append(tokenRule, tokenItem) - } - var senderRule []interface{} - for _, senderItem := range sender { - senderRule = append(senderRule, senderItem) - } - var destinationChainRule []interface{} - for _, destinationChainItem := range destinationChain { - destinationChainRule = append(destinationChainRule, destinationChainItem) - } - - logs, sub, err := _Gateway.contract.WatchLogs(opts, "TokenTransfered", tokenRule, senderRule, destinationChainRule) - 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(GatewayTokenTransfered) - if err := _Gateway.contract.UnpackLog(event, "TokenTransfered", 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 -} - -// ParseTokenTransfered is a log parse operation binding the contract event 0x83c992bcd5b2b18b0cb4ceb131a37c34e5b795f5e00e7058b99a33e9bcf1ee47. -// -// Solidity: event TokenTransfered(address indexed token, address indexed sender, uint32 indexed destinationChain, (uint8,bytes) destinationAddress, uint128 amount) -func (_Gateway *GatewayFilterer) ParseTokenTransfered(log types.Log) (*GatewayTokenTransfered, error) { - event := new(GatewayTokenTransfered) - if err := _Gateway.contract.UnpackLog(event, "TokenTransfered", log); err != nil { - return nil, err - } - event.Raw = log - return event, nil -} - // GatewayUpgradedIterator is returned from FilterUpgraded and is used to iterate over the raw logs and unpacked data for Upgraded events raised by the Gateway contract. type GatewayUpgradedIterator struct { Event *GatewayUpgraded // Event containing the contract specifics and raw log From 047becf63c9028a48ebbd010b2efce45420506dd Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 27 Mar 2024 00:46:27 +0800 Subject: [PATCH 11/55] Fix encode substrate types --- contracts/src/Assets.sol | 4 ++-- contracts/src/SubstrateTypes.sol | 8 ++++---- contracts/src/interfaces/IGateway.sol | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 1e375c98f1..59ab42972e 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -228,12 +228,12 @@ library Assets { if (destinationAddress.isAddress32()) { // The receiver has a 32-byte account ID ticket.payload = SubstrateTypes.TransferTokenToAddress32( - info.token, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount + info.tokenID, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount ); } else if (destinationAddress.isAddress20()) { // The receiver has a 20-byte account ID ticket.payload = SubstrateTypes.TransferTokenToAddress20( - info.token, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount + info.tokenID, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount ); } else { revert Unsupported(); diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index 1a4b8105e5..4c9b7149b8 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -135,7 +135,7 @@ library SubstrateTypes { } // destination is AccountID32 address - function TransferTokenToAddress32(address token, ParaID paraID, bytes32 recipient, uint128 xcmFee, uint128 amount) + function TransferTokenToAddress32(bytes32 tokenID, ParaID paraID, bytes32 recipient, uint128 xcmFee, uint128 amount) internal view returns (bytes memory) @@ -144,7 +144,7 @@ library SubstrateTypes { bytes1(0x00), ScaleCodec.encodeU64(uint64(block.chainid)), bytes1(0x02), - SubstrateTypes.H160(token), + tokenID, bytes1(0x01), ScaleCodec.encodeU32(uint32(ParaID.unwrap(paraID))), recipient, @@ -154,7 +154,7 @@ library SubstrateTypes { } // destination is AccountID20 address - function TransferTokenToAddress20(address token, ParaID paraID, bytes20 recipient, uint128 xcmFee, uint128 amount) + function TransferTokenToAddress20(bytes32 tokenID, ParaID paraID, bytes20 recipient, uint128 xcmFee, uint128 amount) internal view returns (bytes memory) @@ -163,7 +163,7 @@ library SubstrateTypes { bytes1(0x00), ScaleCodec.encodeU64(uint64(block.chainid)), bytes1(0x02), - SubstrateTypes.H160(token), + tokenID, bytes1(0x02), ScaleCodec.encodeU32(uint32(ParaID.unwrap(paraID))), recipient, diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 5d4f2d8b2c..bfb33375a2 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -102,9 +102,9 @@ interface IGateway { /// 1. Delivery costs to BridgeHub /// 2. XCM execution costs on destinationChain function quoteSendTokenFee(address token, ParaID destinationChain, uint128 destinationFee) - external - view - returns (uint256); + external + view + returns (uint256); /// @dev Send ERC20 tokens to parachain `destinationChain` and deposit into account `destinationAddress` function sendToken( From 3fc6cabddc67be5cac07a42a264ea28e15bed431 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 27 Mar 2024 00:47:41 +0800 Subject: [PATCH 12/55] Add smoke test send relay token back --- smoketest/src/constants.rs | 3 + smoketest/tests/register_polkadot_token.rs | 1 - smoketest/tests/send_polkadot_token.rs | 83 ++++++++++++++++++++++ smoketest/tests/transfer_polkadot_token.rs | 5 +- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 smoketest/tests/send_polkadot_token.rs diff --git a/smoketest/src/constants.rs b/smoketest/src/constants.rs index 5bd625b37d..2c27df3d1b 100644 --- a/smoketest/src/constants.rs +++ b/smoketest/src/constants.rs @@ -27,6 +27,9 @@ pub const GATEWAY_PROXY_CONTRACT: [u8; 20] = hex!("EDa338E4dC46038493b885327842f pub const WETH_CONTRACT: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); pub const AGENT_EXECUTOR_CONTRACT: [u8; 20] = hex!("2ffA5ecdBe006d30397c7636d3e015EEE251369F"); +pub const XCDOT_TOKEN_ID: [u8; 32] = + hex!("83ca7bdddc0205caa1d663fdba16db38a4c702f4dcc07b8589d5ed912610a19d"); + // Agent for bridge hub parachain 1013 pub const BRIDGE_HUB_AGENT_ID: [u8; 32] = hex!("03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314"); diff --git a/smoketest/tests/register_polkadot_token.rs b/smoketest/tests/register_polkadot_token.rs index 6ba466c672..dd10fb98a7 100644 --- a/smoketest/tests/register_polkadot_token.rs +++ b/smoketest/tests/register_polkadot_token.rs @@ -18,5 +18,4 @@ async fn register_polkadot_token() { wait_for_bridgehub_event::(&test_clients.bridge_hub_client).await; wait_for_ethereum_event::(&test_clients.ethereum_client).await; - } diff --git a/smoketest/tests/send_polkadot_token.rs b/smoketest/tests/send_polkadot_token.rs new file mode 100644 index 0000000000..e85b0d3a9b --- /dev/null +++ b/smoketest/tests/send_polkadot_token.rs @@ -0,0 +1,83 @@ +use ethers::{core::types::Address, prelude::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::balances::events::Minted, +}; +use subxt::utils::AccountId32; + +#[tokio::test] +async fn send_polkadot_token() { + 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 info = gateway.get_token_info(XCDOT_TOKEN_ID).call().await.unwrap(); + + let destination_fee = 400_000_000; + let fee: U256 = parse_units("0.01", "ether").unwrap().into(); + + let amount = 500_000_000; + + let receipt = gateway + .send_token( + info.token, + ASSET_HUB_PARA_ID, + i_gateway::MultiAddress { kind: 1, data: (*BOB_PUBLIC).into() }, + destination_fee, + amount, + ) + .value(fee) + .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 = 500; + let mut blocks = assethub + .blocks() + .subscribe_finalized() + .await + .expect("block subscription") + .take(wait_for_blocks); + + let expected_owner: AccountId32 = (*BOB_PUBLIC).into(); + + let mut event_found = false; + while let Some(Ok(block)) = blocks.next().await { + println!("Polling assethub block {} for mint event.", block.number()); + + let events = block.events().await.unwrap(); + for event_wrapped in events.find::() { + println!("event found in block {}.", block.number()); + let event = event_wrapped.unwrap(); + assert_eq!(event.who, expected_owner); + event_found = true; + } + if event_found { + break + } + } + assert!(event_found) +} diff --git a/smoketest/tests/transfer_polkadot_token.rs b/smoketest/tests/transfer_polkadot_token.rs index 1ba0d67a66..7168ca8b6d 100644 --- a/smoketest/tests/transfer_polkadot_token.rs +++ b/smoketest/tests/transfer_polkadot_token.rs @@ -5,7 +5,6 @@ use ethers::{ providers::{Provider, Ws}, }; use futures::StreamExt; -use hex_literal::hex; use snowbridge_smoketest::{ constants::*, contracts::{agent_executor, agent_executor::TokenMintedFilter, i_gateway::IGateway}, @@ -29,8 +28,6 @@ use std::{sync::Arc, time::Duration}; use subxt::OnlineClient; use subxt_signer::sr25519::dev; -const DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); - #[tokio::test] async fn transfer_polkadot_token() { let ethereum_provider = Provider::::connect(ETHEREUM_API) @@ -64,7 +61,7 @@ async fn transfer_polkadot_token() { parents: 0, interior: Junctions::X1(Junction::AccountKey20 { network: None, - key: DESTINATION_ADDRESS.into(), + key: ETHEREUM_ADDRESS.into(), }), }); From e53afd081b74a1ea820f695d5aac6acdf2376f8e Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 27 Mar 2024 09:38:41 +0800 Subject: [PATCH 13/55] Rename to sendForeignToken --- contracts/src/Assets.sol | 6 +++--- contracts/src/Gateway.sol | 2 +- contracts/src/SubstrateTypes.sol | 24 ++++++++++++++---------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 59ab42972e..c45a4bea14 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -199,7 +199,7 @@ library Assets { } // @dev Transfer polkadot native tokens back - function transferToken( + function sendForeignToken( address executor, TokenInfo memory info, address sender, @@ -227,12 +227,12 @@ library Assets { if (destinationAddress.isAddress32()) { // The receiver has a 32-byte account ID - ticket.payload = SubstrateTypes.TransferTokenToAddress32( + ticket.payload = SubstrateTypes.SendForeignTokenToAddress32( info.tokenID, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount ); } else if (destinationAddress.isAddress20()) { // The receiver has a 20-byte account ID - ticket.payload = SubstrateTypes.TransferTokenToAddress20( + ticket.payload = SubstrateTypes.SendForeignTokenToAddress20( info.tokenID, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount ); } else { diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 7b749947d3..efaebb8e16 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -448,7 +448,7 @@ contract Gateway is IGateway, IInitializable { } if (info.isForeign) { _submitOutbound( - Assets.transferToken( + Assets.sendForeignToken( AGENT_EXECUTOR, info, msg.sender, destinationChain, destinationAddress, destinationFee, amount ) ); diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index 4c9b7149b8..5d1f9228d1 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -135,11 +135,13 @@ library SubstrateTypes { } // destination is AccountID32 address - function TransferTokenToAddress32(bytes32 tokenID, ParaID paraID, bytes32 recipient, uint128 xcmFee, uint128 amount) - internal - view - returns (bytes memory) - { + function SendForeignTokenToAddress32( + bytes32 tokenID, + ParaID paraID, + bytes32 recipient, + uint128 xcmFee, + uint128 amount + ) internal view returns (bytes memory) { return bytes.concat( bytes1(0x00), ScaleCodec.encodeU64(uint64(block.chainid)), @@ -154,11 +156,13 @@ library SubstrateTypes { } // destination is AccountID20 address - function TransferTokenToAddress20(bytes32 tokenID, ParaID paraID, bytes20 recipient, uint128 xcmFee, uint128 amount) - internal - view - returns (bytes memory) - { + function SendForeignTokenToAddress20( + bytes32 tokenID, + ParaID paraID, + bytes20 recipient, + uint128 xcmFee, + uint128 amount + ) internal view returns (bytes memory) { return bytes.concat( bytes1(0x00), ScaleCodec.encodeU64(uint64(block.chainid)), From 5e9820be8f1c222dc314358be41637aa08ddbc22 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 27 Mar 2024 14:26:43 +0800 Subject: [PATCH 14/55] Improve ERC20.sol --- contracts/src/ERC20.sol | 73 ++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/contracts/src/ERC20.sol b/contracts/src/ERC20.sol index 3552c10e73..7b840b7f94 100644 --- a/contracts/src/ERC20.sol +++ b/contracts/src/ERC20.sol @@ -33,6 +33,8 @@ contract ERC20 is IERC20, IERC20Permit { error InvalidV(); error InvalidSignature(); error Unauthorized(); + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); mapping(address => uint256) public override balanceOf; @@ -152,7 +154,12 @@ contract ERC20 is IERC20, IERC20Permit { uint256 _allowance = allowance[sender][msg.sender]; if (_allowance != type(uint256).max) { - _approve(sender, msg.sender, _allowance - amount); + if (_allowance < amount) { + revert ERC20InsufficientAllowance(msg.sender, _allowance, amount); + } + unchecked { + _approve(sender, msg.sender, _allowance - amount); + } } _transfer(sender, recipient, amount); @@ -173,7 +180,10 @@ contract ERC20 is IERC20, IERC20Permit { * - `spender` cannot be the zero address. */ function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) { - _approve(msg.sender, spender, allowance[msg.sender][spender] + addedValue); + uint256 _allowance = allowance[msg.sender][spender]; + if (_allowance != type(uint256).max) { + _approve(msg.sender, spender, _allowance + addedValue); + } return true; } @@ -192,7 +202,15 @@ contract ERC20 is IERC20, IERC20Permit { * `subtractedValue`. */ function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) { - _approve(msg.sender, spender, allowance[msg.sender][spender] - subtractedValue); + uint256 _allowance = allowance[msg.sender][spender]; + if (_allowance != type(uint256).max) { + if (_allowance < subtractedValue) { + revert ERC20InsufficientAllowance(msg.sender, _allowance, subtractedValue); + } + unchecked { + _approve(msg.sender, spender, _allowance - subtractedValue); + } + } return true; } @@ -238,9 +256,7 @@ contract ERC20 is IERC20, IERC20Permit { function _transfer(address sender, address recipient, uint256 amount) internal virtual { if (sender == address(0) || recipient == address(0)) revert InvalidAccount(); - balanceOf[sender] -= amount; - balanceOf[recipient] += amount; - emit Transfer(sender, recipient, amount); + _update(sender, recipient, amount); } /** @@ -256,9 +272,7 @@ contract ERC20 is IERC20, IERC20Permit { function _mint(address account, uint256 amount) internal virtual { if (account == address(0)) revert InvalidAccount(); - totalSupply += amount; - balanceOf[account] += amount; - emit Transfer(address(0), account, amount); + _update(address(0), account, amount); } /** @@ -275,9 +289,7 @@ contract ERC20 is IERC20, IERC20Permit { function _burn(address account, uint256 amount) internal virtual { if (account == address(0)) revert InvalidAccount(); - balanceOf[account] -= amount; - totalSupply -= amount; - emit Transfer(account, address(0), amount); + _update(account, address(0), amount); } /** @@ -299,4 +311,41 @@ contract ERC20 is IERC20, IERC20Permit { allowance[owner][spender] = amount; emit Approval(owner, spender, amount); } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + totalSupply += value; + } else { + uint256 fromBalance = balanceOf[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + balanceOf[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + balanceOf[to] += value; + } + } + + emit Transfer(from, to, value); + } } From fddb9cf3b38ee6c25e4f23fc26daa70e0b9ea73e Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 00:12:56 +0800 Subject: [PATCH 15/55] Revert the change double register the token --- contracts/src/AgentExecutor.sol | 18 +++------ contracts/src/Assets.sol | 22 ----------- contracts/src/Gateway.sol | 55 ++++++++++++++++++++------- contracts/src/interfaces/IGateway.sol | 4 -- contracts/test/Gateway.t.sol | 7 ++-- 5 files changed, 50 insertions(+), 56 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 3c285fd660..d9b28d9a6b 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -9,8 +9,6 @@ import {IERC20} from "./interfaces/IERC20.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {ERC20} from "./ERC20.sol"; import {Gateway} from "./Gateway.sol"; -import {Assets} from "./Assets.sol"; -import {TokenInfo} from "./storage/AssetsStorage.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. /// @dev This is a singleton contract, meaning that all agents will execute the same code. @@ -26,17 +24,15 @@ contract AgentExecutor { /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(bytes32 agentID, AgentExecuteCommand command, bytes memory params) - external - returns (bytes memory) - { + function execute(bytes32 agentID, bytes memory data) external returns (bytes memory) { + (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); _transferToken(token, recipient, amount); } else if (command == AgentExecuteCommand.RegisterToken) { (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = abi.decode(params, (bytes32, string, string, uint8)); - return _registerToken(agentID, tokenID, name, symbol, decimals); + _registerToken(agentID, tokenID, name, symbol, decimals); } else if (command == AgentExecuteCommand.MintToken) { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); _mintToken(tokenID, recipient, amount); @@ -62,21 +58,19 @@ contract AgentExecutor { returns (bytes memory) { IERC20 token = new ERC20(name, symbol, decimals); - Assets.registerTokenByID(tokenID, address(token), agentID); + Gateway(msg.sender).registerForeignToken(tokenID, address(token), agentID); return abi.encode(tokenID, address(token)); } /// @dev Mint ERC20 token to `recipient`. function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { - TokenInfo memory info = Assets.getTokenInfo(tokenID); - address token = info.token; + address token = Gateway(msg.sender).getTokenAddress(tokenID); ERC20(token).mint(recipient, amount); emit TokenMinted(tokenID, token, recipient, amount); } function burnToken(bytes32 tokenID, address sender, uint256 amount) external { - TokenInfo memory info = Assets.getTokenInfo(tokenID); - address token = info.token; + address token = Gateway(msg.sender).getTokenAddress(tokenID); ERC20(token).burn(sender, amount); emit TokenBurnt(tokenID, token, sender, amount); } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index c45a4bea14..d98540f624 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -29,7 +29,6 @@ library Assets { error TokenNotRegistered(); error Unsupported(); error InvalidDestinationFee(); - error TokenAlreadyRegistered(); error AgentDoesNotExist(); function isTokenRegistered(address token) external view returns (bool) { @@ -177,27 +176,6 @@ library Assets { emit IGateway.TokenRegistrationSent(token); } - // @dev Register a new fungible Polkadot token for an agent - function registerTokenByID(bytes32 tokenID, address token, bytes32 agentID) external { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == true) { - revert TokenAlreadyRegistered(); - } - TokenInfo memory info = - TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); - $.tokenRegistry[token] = info; - $.tokenRegistryByID[tokenID] = info; - } - - // @dev Get token address by tokenID - function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory) { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == false) { - revert TokenNotRegistered(); - } - return $.tokenRegistryByID[tokenID]; - } - // @dev Transfer polkadot native tokens back function sendForeignToken( address executor, diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index efaebb8e16..6683e8e3c7 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -90,6 +90,7 @@ contract Gateway is IGateway, IInitializable { error InvalidConstructorParams(); error AlreadyInitialized(); error TokenNotRegistered(); + error TokenAlreadyRegistered(); // handler functions are privileged modifier onlySelf() { @@ -99,6 +100,15 @@ contract Gateway is IGateway, IInitializable { _; } + // handler functions are privileged from agent only + modifier onlyAgent(bytes32 agentID) { + bytes32 _agentID = _ensureAgentAddress(msg.sender); + if (_agentID != agentID) { + revert Unauthorized(); + } + _; + } + constructor( address beefyClient, address agentExecutor, @@ -268,21 +278,12 @@ contract Gateway is IGateway, IInitializable { revert InvalidAgentExecutionPayload(); } - (AgentExecuteCommand command, bytes memory payload) = abi.decode(params.payload, (AgentExecuteCommand, bytes)); - - bytes memory call = abi.encodeCall(AgentExecutor.execute, (params.agentID, command, payload)); + bytes memory call = abi.encodeCall(AgentExecutor.execute, (params.agentID, params.payload)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); if (!success) { revert AgentExecutionFailed(returndata); } - - if (command == AgentExecuteCommand.RegisterToken) { - (bytes memory result) = abi.decode(returndata, (bytes)); - (bytes32 tokenID, address token) = abi.decode(result, (bytes32, address)); - Assets.registerTokenByID(tokenID, token, params.agentID); - emit IGateway.ForeignTokenRegistered(tokenID, params.agentID, token); - } } /// @dev Create an agent for a consensus system on Polkadot @@ -406,6 +407,28 @@ contract Gateway is IGateway, IInitializable { emit PricingParametersChanged(); } + // @dev Register a new fungible Polkadot token for an agent + function registerForeignToken(bytes32 tokenID, address token, bytes32 agentID) external onlyAgent(agentID) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == true) { + revert TokenAlreadyRegistered(); + } + TokenInfo memory info = + TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); + $.tokenRegistry[token] = info; + $.tokenRegistryByID[tokenID] = info; + emit ForeignTokenRegistered(tokenID, agentID, token); + } + + // @dev Get token address by tokenID + function getTokenAddress(bytes32 tokenID) external view returns (address) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == false) { + revert TokenNotRegistered(); + } + return $.tokenRegistryByID[tokenID].token; + } + /** * Assets */ @@ -459,10 +482,6 @@ contract Gateway is IGateway, IInitializable { } } - function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory) { - return Assets.getTokenInfo(tokenID); - } - /** * Internal functions */ @@ -567,6 +586,14 @@ contract Gateway is IGateway, IInitializable { } } + /// @dev Ensure that the specified address is an valid agent + function _ensureAgentAddress(address agent) internal view returns (bytes32 agentID) { + agentID = CoreStorage.layout().agentAddresses[agent]; + if (agentID == bytes32(0)) { + revert AgentDoesNotExist(); + } + } + /// @dev Invoke some code within an agent function _invokeOnAgent(address agent, bytes memory data) internal returns (bytes memory) { (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(AGENT_EXECUTOR, data)); diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index bfb33375a2..423c32303d 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.23; import {OperatingMode, InboundMessage, ParaID, ChannelID, MultiAddress} from "../Types.sol"; import {Verification} from "../Verification.sol"; import {UD60x18} from "prb/math/src/UD60x18.sol"; -import {TokenInfo} from "../storage/AssetsStorage.sol"; interface IGateway { /** @@ -114,7 +113,4 @@ interface IGateway { uint128 destinationFee, uint128 amount ) external payable; - - /// @dev Get tokenInfo by tokenID - function getTokenInfo(bytes32 tokenID) external view returns (TokenInfo memory); } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 4fc29e161e..4c83f66938 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -22,7 +22,6 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; -import {TokenInfo} from "../src/storage/AssetsStorage.sol"; import { UpgradeParams, @@ -950,20 +949,20 @@ contract GatewayTest is Test { // Register and then mint some DOT to account1 testAgentMintDot(); - TokenInfo memory info = IGateway(address(gateway)).getTokenInfo(dotTokenID); + address dotToken = GatewayMock(address(gateway)).getTokenAddress(dotTokenID); ParaID destPara = assetHubParaID; vm.prank(account1); vm.expectEmit(true, true, false, true); - emit IGateway.TokenSent(address(info.token), account1, destPara, recipientAddress32, 1); + emit IGateway.TokenSent(address(dotToken), account1, destPara, recipientAddress32, 1); // Expect the gateway to emit `OutboundMessageAccepted` vm.expectEmit(true, false, false, false); emit IGateway.OutboundMessageAccepted(assetHubParaID.into(), 1, messageID, bytes("")); - IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(info.token), destPara, recipientAddress32, 1, 1); + IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); } function testParseAgentExecuteCall() public { From e5d9dd847dad0977e6778af5ed9da7a1edef3eb8 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 09:12:21 +0800 Subject: [PATCH 16/55] Migration for AssetsStorage --- contracts/src/storage/AssetsStorage.sol | 6 +-- contracts/src/storage/LegacyAssetsStorage.sol | 34 ++++++++++++++++ .../rococo/GatewayWithAssetStorageV2.sol | 39 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 contracts/src/storage/LegacyAssetsStorage.sol create mode 100644 contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol diff --git a/contracts/src/storage/AssetsStorage.sol b/contracts/src/storage/AssetsStorage.sol index 1777672e53..81b51a4699 100644 --- a/contracts/src/storage/AssetsStorage.sol +++ b/contracts/src/storage/AssetsStorage.sol @@ -6,8 +6,6 @@ import {TokenInfo, ParaID} from "../Types.sol"; library AssetsStorage { struct Layout { - // Token registry by token address - mapping(address token => TokenInfo) tokenRegistry; address assetHubAgent; ParaID assetHubParaID; // XCM fee charged by AssetHub for registering a token (DOT) @@ -16,11 +14,13 @@ library AssetsStorage { uint128 assetHubReserveTransferFee; // Extra fee for registering a token, to discourage spamming (Ether) uint256 registerTokenFee; + // Token registry by token address + mapping(address token => TokenInfo) tokenRegistry; // Token registry by tokenID mapping(bytes32 tokenID => TokenInfo) tokenRegistryByID; } - bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets"); + bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets.v2"); function layout() internal pure returns (Layout storage $) { bytes32 slot = SLOT; diff --git a/contracts/src/storage/LegacyAssetsStorage.sol b/contracts/src/storage/LegacyAssetsStorage.sol new file mode 100644 index 0000000000..568ac9a229 --- /dev/null +++ b/contracts/src/storage/LegacyAssetsStorage.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +import {ParaID} from "../Types.sol"; + +library LegacyAssetsStorage { + struct TokenInfoLegacy { + bool isRegistered; + bytes31 __padding; + } + + struct Layout { + // Legacy token registry by token address + mapping(address token => TokenInfoLegacy) tokenRegistry; + address assetHubAgent; + ParaID assetHubParaID; + // XCM fee charged by AssetHub for registering a token (DOT) + uint128 assetHubCreateAssetFee; + // XCM fee charged by AssetHub for receiving a token from the Gateway (DOT) + uint128 assetHubReserveTransferFee; + // Extra fee for registering a token, to discourage spamming (Ether) + uint256 registerTokenFee; + } + + bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets"); + + function layout() internal pure returns (Layout storage $) { + bytes32 slot = SLOT; + assembly { + $.slot := slot + } + } +} diff --git a/contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol b/contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol new file mode 100644 index 0000000000..a7f08a77d5 --- /dev/null +++ b/contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +import "../../Gateway.sol"; + +import {AssetsStorage} from "../../storage/AssetsStorage.sol"; +import {LegacyAssetsStorage} from "../../storage/LegacyAssetsStorage.sol"; + +contract GatewayWithAssetStorageV2 is Gateway { + constructor( + address beefyClient, + address agentExecutor, + ParaID bridgeHubParaID, + bytes32 bridgeHubAgentID, + uint8 foreignTokenDecimals + ) Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals) {} + + function initialize(bytes memory data) external override { + // Prevent initialization of storage in implementation contract + if (ERC1967.load() == address(0)) { + revert Unauthorized(); + } + + address[] memory tokens = abi.decode(data, (address[])); + + LegacyAssetsStorage.Layout storage $ = LegacyAssetsStorage.layout(); + AssetsStorage.Layout storage $v2 = AssetsStorage.layout(); + + $v2.assetHubAgent = $.assetHubAgent; + $v2.assetHubParaID = $.assetHubParaID; + $v2.assetHubCreateAssetFee = $.assetHubCreateAssetFee; + $v2.assetHubReserveTransferFee = $.assetHubReserveTransferFee; + $v2.registerTokenFee = $.registerTokenFee; + for (uint256 i = 0; i < tokens.length; i++) { + $v2.tokenRegistry[tokens[i]].isRegistered = $.tokenRegistry[tokens[i]].isRegistered; + } + } +} From 9f7e99b807bb0f0144110d52917ab375a400893d Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 09:23:33 +0800 Subject: [PATCH 17/55] Rename to _sendForeignTokenCosts --- 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 d98540f624..fa779caae2 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -201,7 +201,7 @@ library Assets { } ticket.dest = destinationChain; - ticket.costs = _transferTokenCosts(destinationChainFee); + ticket.costs = _sendForeignTokenCosts(destinationChainFee); if (destinationAddress.isAddress32()) { // The receiver has a 32-byte account ID @@ -226,7 +226,7 @@ library Assets { Call.verifyResult(success, returndata); } - function _transferTokenCosts(uint128 destinationChainFee) internal pure returns (Costs memory costs) { + function _sendForeignTokenCosts(uint128 destinationChainFee) internal pure returns (Costs memory costs) { costs.foreign = destinationChainFee; costs.native = 0; } From 724c649fd63d0f75678aa80ddeb8ad32af84f8e2 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 09:25:51 +0800 Subject: [PATCH 18/55] Use storage for less gas --- contracts/src/Assets.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index fa779caae2..ec83ab2149 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -179,7 +179,7 @@ library Assets { // @dev Transfer polkadot native tokens back function sendForeignToken( address executor, - TokenInfo memory info, + TokenInfo storage info, address sender, ParaID destinationChain, MultiAddress calldata destinationAddress, From 6eb8344598de0b90e2b37bd2ce32f90f0deb221c Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 09:32:03 +0800 Subject: [PATCH 19/55] Rename to tokenAddressOf --- contracts/src/AgentExecutor.sol | 4 ++-- contracts/src/Gateway.sol | 2 +- contracts/test/Gateway.t.sol | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index d9b28d9a6b..58b8b9e2c0 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -64,13 +64,13 @@ contract AgentExecutor { /// @dev Mint ERC20 token to `recipient`. function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { - address token = Gateway(msg.sender).getTokenAddress(tokenID); + address token = Gateway(msg.sender).tokenAddressOf(tokenID); ERC20(token).mint(recipient, amount); emit TokenMinted(tokenID, token, recipient, amount); } function burnToken(bytes32 tokenID, address sender, uint256 amount) external { - address token = Gateway(msg.sender).getTokenAddress(tokenID); + address token = Gateway(msg.sender).tokenAddressOf(tokenID); ERC20(token).burn(sender, amount); emit TokenBurnt(tokenID, token, sender, amount); } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 6683e8e3c7..d5a11fbe1a 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -421,7 +421,7 @@ contract Gateway is IGateway, IInitializable { } // @dev Get token address by tokenID - function getTokenAddress(bytes32 tokenID) external view returns (address) { + function tokenAddressOf(bytes32 tokenID) external view returns (address) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == false) { revert TokenNotRegistered(); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 4c83f66938..3dc4ed46ff 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -949,7 +949,7 @@ contract GatewayTest is Test { // Register and then mint some DOT to account1 testAgentMintDot(); - address dotToken = GatewayMock(address(gateway)).getTokenAddress(dotTokenID); + address dotToken = GatewayMock(address(gateway)).tokenAddressOf(dotTokenID); ParaID destPara = assetHubParaID; From 7c29684614addf8f404a914696c9a14b7fe64df5 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 09:34:53 +0800 Subject: [PATCH 20/55] Rename as _burnToken --- contracts/src/Assets.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index ec83ab2149..5cf3663deb 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -194,7 +194,7 @@ library Assets { } // Polkadot-native token: burn wrapped token - _burn(executor, agent, info.tokenID, sender, amount); + _burnToken(executor, agent, info.tokenID, sender, amount); if (destinationChainFee == 0) { revert InvalidDestinationFee(); @@ -220,7 +220,9 @@ library Assets { emit IGateway.TokenSent(info.token, sender, destinationChain, destinationAddress, amount); } - function _burn(address agentExecutor, address agent, bytes32 tokenID, address sender, uint256 amount) internal { + function _burnToken(address agentExecutor, address agent, bytes32 tokenID, address sender, uint256 amount) + internal + { bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (tokenID, sender, amount)); (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(agentExecutor, call)); Call.verifyResult(success, returndata); From edccccdf014c5d4a8934591bef649a870e430992 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 09:44:09 +0800 Subject: [PATCH 21/55] _ensureAgent in Gateway --- contracts/src/Assets.sol | 8 +------- contracts/src/Gateway.sol | 10 +++++++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 5cf3663deb..ade73b3ad4 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -178,6 +178,7 @@ library Assets { // @dev Transfer polkadot native tokens back function sendForeignToken( + address agent, address executor, TokenInfo storage info, address sender, @@ -186,13 +187,6 @@ library Assets { uint128 destinationChainFee, uint128 amount ) external returns (Ticket memory ticket) { - CoreStorage.Layout storage $core = CoreStorage.layout(); - - address agent = $core.agents[info.agentID]; - if (agent == address(0)) { - revert AgentDoesNotExist(); - } - // Polkadot-native token: burn wrapped token _burnToken(executor, agent, info.tokenID, sender, amount); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index d5a11fbe1a..7cd80d1329 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -470,9 +470,17 @@ contract Gateway is IGateway, IInitializable { revert TokenNotRegistered(); } if (info.isForeign) { + address agent = _ensureAgent(info.agentID); _submitOutbound( Assets.sendForeignToken( - AGENT_EXECUTOR, info, msg.sender, destinationChain, destinationAddress, destinationFee, amount + agent, + AGENT_EXECUTOR, + info, + msg.sender, + destinationChain, + destinationAddress, + destinationFee, + amount ) ); } else { From c170eeafec47e85fe62e2a2545d1aac6afb9627a Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 10:10:01 +0800 Subject: [PATCH 22/55] Remove unused --- contracts/src/AgentExecutor.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 58b8b9e2c0..9ace22f3ea 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -24,7 +24,7 @@ contract AgentExecutor { /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(bytes32 agentID, bytes memory data) external returns (bytes memory) { + function execute(bytes32 agentID, bytes memory data) external { (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); @@ -37,7 +37,6 @@ contract AgentExecutor { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); _mintToken(tokenID, recipient, amount); } - return bytes(""); } /// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`, @@ -55,11 +54,9 @@ contract AgentExecutor { /// @dev Register native asset from polkadto as ERC20 `token`. function _registerToken(bytes32 agentID, bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) internal - returns (bytes memory) { IERC20 token = new ERC20(name, symbol, decimals); Gateway(msg.sender).registerForeignToken(tokenID, address(token), agentID); - return abi.encode(tokenID, address(token)); } /// @dev Mint ERC20 token to `recipient`. From 953245b64f0115bc28639059464be0a29aed2505 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 29 Mar 2024 14:26:28 +0800 Subject: [PATCH 23/55] Clean up smoke test --- relayer/contracts/gateway.go | 42 +------------- smoketest/Cargo.lock | 64 +++++++++++----------- smoketest/src/constants.rs | 3 +- smoketest/tests/send_polkadot_token.rs | 4 +- smoketest/tests/transfer_polkadot_token.rs | 6 +- 5 files changed, 38 insertions(+), 81 deletions(-) diff --git a/relayer/contracts/gateway.go b/relayer/contracts/gateway.go index e475172bbe..9114d2e312 100644 --- a/relayer/contracts/gateway.go +++ b/relayer/contracts/gateway.go @@ -47,15 +47,6 @@ type MultiAddress struct { Data []byte } -// TokenInfo is an auto generated low-level Go binding around an user-defined struct. -type TokenInfo struct { - IsRegistered bool - IsForeign bool - TokenID [32]byte - AgentID [32]byte - Token common.Address -} - // VerificationDigestItem is an auto generated low-level Go binding around an user-defined struct. type VerificationDigestItem struct { Kind *big.Int @@ -100,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\":\"getTokenInfo\",\"inputs\":[{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"internalType\":\"structTokenInfo\",\"components\":[{\"name\":\"isRegistered\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"isForeign\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"tokenID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"agentID\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"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\":\"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\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"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},{\"type\":\"event\",\"name\":\"Upgraded\",\"inputs\":[{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"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\":\"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\":\"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\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"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},{\"type\":\"event\",\"name\":\"Upgraded\",\"inputs\":[{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false}]", } // GatewayABI is the input ABI used to generate the binding from. @@ -343,37 +334,6 @@ func (_Gateway *GatewayCallerSession) ChannelOperatingModeOf(channelID [32]byte) return _Gateway.Contract.ChannelOperatingModeOf(&_Gateway.CallOpts, channelID) } -// GetTokenInfo is a free data retrieval call binding the contract method 0x2d8b70a1. -// -// Solidity: function getTokenInfo(bytes32 tokenID) view returns((bool,bool,bytes32,bytes32,address)) -func (_Gateway *GatewayCaller) GetTokenInfo(opts *bind.CallOpts, tokenID [32]byte) (TokenInfo, error) { - var out []interface{} - err := _Gateway.contract.Call(opts, &out, "getTokenInfo", tokenID) - - if err != nil { - return *new(TokenInfo), err - } - - out0 := *abi.ConvertType(out[0], new(TokenInfo)).(*TokenInfo) - - return out0, err - -} - -// GetTokenInfo is a free data retrieval call binding the contract method 0x2d8b70a1. -// -// Solidity: function getTokenInfo(bytes32 tokenID) view returns((bool,bool,bytes32,bytes32,address)) -func (_Gateway *GatewaySession) GetTokenInfo(tokenID [32]byte) (TokenInfo, error) { - return _Gateway.Contract.GetTokenInfo(&_Gateway.CallOpts, tokenID) -} - -// GetTokenInfo is a free data retrieval call binding the contract method 0x2d8b70a1. -// -// Solidity: function getTokenInfo(bytes32 tokenID) view returns((bool,bool,bytes32,bytes32,address)) -func (_Gateway *GatewayCallerSession) GetTokenInfo(tokenID [32]byte) (TokenInfo, error) { - return _Gateway.Contract.GetTokenInfo(&_Gateway.CallOpts, tokenID) -} - // Implementation is a free data retrieval call binding the contract method 0x5c60da1b. // // Solidity: function implementation() view returns(address) diff --git a/smoketest/Cargo.lock b/smoketest/Cargo.lock index b8d924c190..5586f962de 100644 --- a/smoketest/Cargo.lock +++ b/smoketest/Cargo.lock @@ -457,7 +457,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -491,7 +491,7 @@ checksum = "823b8bb275161044e2ac7a25879cb3e2480cb403e3943022c7c769c599b756aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1124,7 +1124,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1172,7 +1172,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1194,7 +1194,7 @@ checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" dependencies = [ "darling_core 0.20.5", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1584,7 +1584,7 @@ dependencies = [ "regex", "serde", "serde_json", - "syn 2.0.52", + "syn 2.0.55", "toml", "walkdir", ] @@ -1601,7 +1601,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1626,7 +1626,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.52", + "syn 2.0.55", "tempfile", "thiserror", "tiny-keccak", @@ -1955,7 +1955,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -2717,9 +2717,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lru" @@ -2952,7 +2952,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -3157,7 +3157,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -3259,7 +3259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -3483,7 +3483,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -4115,7 +4115,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -4556,7 +4556,7 @@ checksum = "50535e1a5708d3ba5c1195b59ebefac61cc8679c2c24716b87a86e8b7ed2e4a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -4565,7 +4565,7 @@ version = "14.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -4681,7 +4681,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -4903,7 +4903,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -4973,7 +4973,7 @@ dependencies = [ "quote", "scale-info", "subxt-metadata", - "syn 2.0.52", + "syn 2.0.55", "thiserror", "tokio", ] @@ -5003,7 +5003,7 @@ dependencies = [ "parity-scale-codec", "proc-macro-error", "subxt-codegen", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -5052,9 +5052,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -5129,7 +5129,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -5222,7 +5222,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -5387,7 +5387,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -5706,7 +5706,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", "wasm-bindgen-shared", ] @@ -5740,7 +5740,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6247,7 +6247,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -6273,7 +6273,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -6293,5 +6293,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] diff --git a/smoketest/src/constants.rs b/smoketest/src/constants.rs index 2c27df3d1b..84613afd84 100644 --- a/smoketest/src/constants.rs +++ b/smoketest/src/constants.rs @@ -27,7 +27,8 @@ pub const GATEWAY_PROXY_CONTRACT: [u8; 20] = hex!("EDa338E4dC46038493b885327842f pub const WETH_CONTRACT: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); pub const AGENT_EXECUTOR_CONTRACT: [u8; 20] = hex!("2ffA5ecdBe006d30397c7636d3e015EEE251369F"); -pub const XCDOT_TOKEN_ID: [u8; 32] = +pub const ERC20_DOT_CONTRACT: [u8; 20] = hex!("97d3fC1832792ae4f1D48fEA3E00dc73A9101bf9"); +pub const ERC20_DOT_TOKEN_ID: [u8; 32] = hex!("83ca7bdddc0205caa1d663fdba16db38a4c702f4dcc07b8589d5ed912610a19d"); // Agent for bridge hub parachain 1013 diff --git a/smoketest/tests/send_polkadot_token.rs b/smoketest/tests/send_polkadot_token.rs index e85b0d3a9b..a81e662e05 100644 --- a/smoketest/tests/send_polkadot_token.rs +++ b/smoketest/tests/send_polkadot_token.rs @@ -17,7 +17,7 @@ async fn send_polkadot_token() { let gateway_addr: Address = GATEWAY_PROXY_CONTRACT.into(); let gateway = i_gateway::IGateway::new(gateway_addr, ethereum_client.clone()); - let info = gateway.get_token_info(XCDOT_TOKEN_ID).call().await.unwrap(); + let token: Address = ERC20_DOT_CONTRACT.into(); let destination_fee = 400_000_000; let fee: U256 = parse_units("0.01", "ether").unwrap().into(); @@ -26,7 +26,7 @@ async fn send_polkadot_token() { let receipt = gateway .send_token( - info.token, + token, ASSET_HUB_PARA_ID, i_gateway::MultiAddress { kind: 1, data: (*BOB_PUBLIC).into() }, destination_fee, diff --git a/smoketest/tests/transfer_polkadot_token.rs b/smoketest/tests/transfer_polkadot_token.rs index 7168ca8b6d..cdd63793f9 100644 --- a/smoketest/tests/transfer_polkadot_token.rs +++ b/smoketest/tests/transfer_polkadot_token.rs @@ -7,7 +7,7 @@ use ethers::{ use futures::StreamExt; use snowbridge_smoketest::{ constants::*, - contracts::{agent_executor, agent_executor::TokenMintedFilter, i_gateway::IGateway}, + contracts::{agent_executor, agent_executor::TokenMintedFilter}, helper::AssetHubConfig, parachains::assethub::{ api::runtime_types::{ @@ -37,10 +37,6 @@ async fn transfer_polkadot_token() { let ethereum_client = Arc::new(ethereum_provider); - let gateway = IGateway::new(GATEWAY_PROXY_CONTRACT, ethereum_client.clone()); - let _agent_src = - gateway.agent_of(ASSET_HUB_AGENT_ID).await.expect("could not get agent address"); - let assethub: OnlineClient = OnlineClient::from_url(ASSET_HUB_WS_URL).await.unwrap(); From adc73a32e83e6ced8bffe0fcbd4cb7c6a4dcfd6c Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 10:27:18 +0800 Subject: [PATCH 24/55] Terminate register foreign token in Gateway --- contracts/src/AgentExecutor.sol | 15 +-------------- contracts/src/ERC20.sol | 14 +++++++++++++- contracts/src/Gateway.sol | 22 +++++++++++++++++----- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 9ace22f3ea..d0aef820a1 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -24,15 +24,10 @@ contract AgentExecutor { /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, /// the `data` parameter is constructed by the BridgeHub parachain. /// - function execute(bytes32 agentID, bytes memory data) external { - (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); + function execute(AgentExecuteCommand command, bytes memory params) external { if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); _transferToken(token, recipient, amount); - } else if (command == AgentExecuteCommand.RegisterToken) { - (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = - abi.decode(params, (bytes32, string, string, uint8)); - _registerToken(agentID, tokenID, name, symbol, decimals); } else if (command == AgentExecuteCommand.MintToken) { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); _mintToken(tokenID, recipient, amount); @@ -51,14 +46,6 @@ contract AgentExecutor { IERC20(token).safeTransfer(recipient, amount); } - /// @dev Register native asset from polkadto as ERC20 `token`. - function _registerToken(bytes32 agentID, bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) - internal - { - IERC20 token = new ERC20(name, symbol, decimals); - Gateway(msg.sender).registerForeignToken(tokenID, address(token), agentID); - } - /// @dev Mint ERC20 token to `recipient`. function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { address token = Gateway(msg.sender).tokenAddressOf(tokenID); diff --git a/contracts/src/ERC20.sol b/contracts/src/ERC20.sol index 7b840b7f94..4b06fddcd0 100644 --- a/contracts/src/ERC20.sol +++ b/contracts/src/ERC20.sol @@ -35,6 +35,7 @@ contract ERC20 is IERC20, IERC20Permit { error Unauthorized(); error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + error OwnableInvalidOwner(address owner); mapping(address => uint256) public override balanceOf; @@ -53,7 +54,7 @@ contract ERC20 is IERC20, IERC20Permit { bytes32 private constant PERMIT_SIGNATURE_HASH = bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); - address public immutable OWNER; + address public OWNER; string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; uint8 public immutable decimals; @@ -83,6 +84,17 @@ contract ERC20 is IERC20, IERC20Permit { _; } + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + OWNER = newOwner; + } + /** * @dev Creates `amount` tokens and assigns them to `account`, increasing * the total supply. Can only be called by the owner. diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 7cd80d1329..aab2ca32ca 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -47,6 +47,7 @@ import {PricingStorage} from "./storage/PricingStorage.sol"; import {AssetsStorage} from "./storage/AssetsStorage.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; +import {ERC20} from "./ERC20.sol"; contract Gateway is IGateway, IInitializable { using Address for address; @@ -278,11 +279,17 @@ contract Gateway is IGateway, IInitializable { revert InvalidAgentExecutionPayload(); } - bytes memory call = abi.encodeCall(AgentExecutor.execute, (params.agentID, params.payload)); + (AgentExecuteCommand command, bytes memory commandParams) = + abi.decode(params.payload, (AgentExecuteCommand, bytes)); - (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); - if (!success) { - revert AgentExecutionFailed(returndata); + if (command == AgentExecuteCommand.RegisterToken) { + _registerForeignToken(params.agentID, agent, commandParams); + } else { + bytes memory call = abi.encodeCall(AgentExecutor.execute, (command, commandParams)); + (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); + if (!success) { + revert AgentExecutionFailed(returndata); + } } } @@ -408,15 +415,20 @@ contract Gateway is IGateway, IInitializable { } // @dev Register a new fungible Polkadot token for an agent - function registerForeignToken(bytes32 tokenID, address token, bytes32 agentID) external onlyAgent(agentID) { + function _registerForeignToken(bytes32 agentID, address agent, bytes memory params) internal { + (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = + abi.decode(params, (bytes32, string, string, uint8)); AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == true) { revert TokenAlreadyRegistered(); } + ERC20 foreignToken = new ERC20(name, symbol, decimals); + address token = address(foreignToken); TokenInfo memory info = TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); $.tokenRegistry[token] = info; $.tokenRegistryByID[tokenID] = info; + foreignToken.transferOwnership(agent); emit ForeignTokenRegistered(tokenID, agentID, token); } From 28f513d669b7bf4e97ac60e719147694599504c6 Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 11:33:19 +0800 Subject: [PATCH 25/55] Mint foreign token without the callback --- contracts/src/AgentExecutor.sol | 6 +----- contracts/src/Gateway.sol | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index d0aef820a1..ab2b20d8d1 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -28,9 +28,6 @@ contract AgentExecutor { if (command == AgentExecuteCommand.TransferToken) { (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); _transferToken(token, recipient, amount); - } else if (command == AgentExecuteCommand.MintToken) { - (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); - _mintToken(tokenID, recipient, amount); } } @@ -47,8 +44,7 @@ contract AgentExecutor { } /// @dev Mint ERC20 token to `recipient`. - function _mintToken(bytes32 tokenID, address recipient, uint256 amount) internal { - address token = Gateway(msg.sender).tokenAddressOf(tokenID); + function mintToken(bytes32 tokenID, address token, address recipient, uint256 amount) external { ERC20(token).mint(recipient, amount); emit TokenMinted(tokenID, token, recipient, amount); } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index aab2ca32ca..1fa9a0712c 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -284,6 +284,8 @@ contract Gateway is IGateway, IInitializable { if (command == AgentExecuteCommand.RegisterToken) { _registerForeignToken(params.agentID, agent, commandParams); + } else if (command == AgentExecuteCommand.MintToken) { + _mintForeignToken(agent, commandParams); } else { bytes memory call = abi.encodeCall(AgentExecutor.execute, (command, commandParams)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); @@ -432,8 +434,24 @@ contract Gateway is IGateway, IInitializable { emit ForeignTokenRegistered(tokenID, agentID, token); } + // @dev Mint foreign token from Polkadot + function _mintForeignToken(address agent, bytes memory params) internal { + (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); + address token = _tokenAddressOf(tokenID); + bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (tokenID, token, recipient, amount)); + (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); + if (!success) { + revert AgentExecutionFailed(returndata); + } + } + // @dev Get token address by tokenID function tokenAddressOf(bytes32 tokenID) external view returns (address) { + return _tokenAddressOf(tokenID); + } + + // @dev Get token address by tokenID + function _tokenAddressOf(bytes32 tokenID) internal view returns (address) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); if ($.tokenRegistryByID[tokenID].isRegistered == false) { revert TokenNotRegistered(); From 9b7811d301f04d2417fd9fad3e4ba1bc1dfc59a5 Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 11:49:56 +0800 Subject: [PATCH 26/55] Burn token without the callback --- contracts/src/AgentExecutor.sol | 3 +-- contracts/src/Assets.sol | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index ab2b20d8d1..c28c0275af 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -49,8 +49,7 @@ contract AgentExecutor { emit TokenMinted(tokenID, token, recipient, amount); } - function burnToken(bytes32 tokenID, address sender, uint256 amount) external { - address token = Gateway(msg.sender).tokenAddressOf(tokenID); + function burnToken(bytes32 tokenID, address token, address sender, uint256 amount) external { ERC20(token).burn(sender, amount); emit TokenBurnt(tokenID, token, sender, amount); } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index ade73b3ad4..6f240c17e4 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -188,7 +188,7 @@ library Assets { uint128 amount ) external returns (Ticket memory ticket) { // Polkadot-native token: burn wrapped token - _burnToken(executor, agent, info.tokenID, sender, amount); + _burnToken(executor, agent, info.tokenID, info.token, sender, amount); if (destinationChainFee == 0) { revert InvalidDestinationFee(); @@ -214,10 +214,15 @@ library Assets { emit IGateway.TokenSent(info.token, sender, destinationChain, destinationAddress, amount); } - function _burnToken(address agentExecutor, address agent, bytes32 tokenID, address sender, uint256 amount) - internal - { - bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (tokenID, sender, amount)); + function _burnToken( + address agentExecutor, + address agent, + bytes32 tokenID, + address token, + address sender, + uint256 amount + ) internal { + bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (tokenID, token, sender, amount)); (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(agentExecutor, call)); Call.verifyResult(success, returndata); } From b00538f4ed35464f2bf270433d60e873fd540edd Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 22:11:17 +0800 Subject: [PATCH 27/55] constructor ERC20 with the agent as owner --- contracts/src/ERC20.sol | 17 +++-------------- contracts/src/Gateway.sol | 3 +-- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/contracts/src/ERC20.sol b/contracts/src/ERC20.sol index 4b06fddcd0..da8779cb75 100644 --- a/contracts/src/ERC20.sol +++ b/contracts/src/ERC20.sol @@ -54,7 +54,7 @@ contract ERC20 is IERC20, IERC20Permit { bytes32 private constant PERMIT_SIGNATURE_HASH = bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); - address public OWNER; + address public immutable OWNER; string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; uint8 public immutable decimals; @@ -64,8 +64,8 @@ contract ERC20 is IERC20, IERC20Permit { /** * @dev Sets the values for {name}, {symbol}, and {decimals}. */ - constructor(string memory name_, string memory symbol_, uint8 decimals_) { - OWNER = msg.sender; + constructor(address _owner, string memory name_, string memory symbol_, uint8 decimals_) { + OWNER = _owner; DOMAIN_SEPARATOR = keccak256( abi.encode( DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name_)), keccak256(bytes("1")), block.chainid, address(this) @@ -84,17 +84,6 @@ contract ERC20 is IERC20, IERC20Permit { _; } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public virtual onlyOwner { - if (newOwner == address(0)) { - revert OwnableInvalidOwner(address(0)); - } - OWNER = newOwner; - } - /** * @dev Creates `amount` tokens and assigns them to `account`, increasing * the total supply. Can only be called by the owner. diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 1fa9a0712c..624a3e7af7 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -424,13 +424,12 @@ contract Gateway is IGateway, IInitializable { if ($.tokenRegistryByID[tokenID].isRegistered == true) { revert TokenAlreadyRegistered(); } - ERC20 foreignToken = new ERC20(name, symbol, decimals); + ERC20 foreignToken = new ERC20(agent, name, symbol, decimals); address token = address(foreignToken); TokenInfo memory info = TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); $.tokenRegistry[token] = info; $.tokenRegistryByID[tokenID] = info; - foreignToken.transferOwnership(agent); emit ForeignTokenRegistered(tokenID, agentID, token); } From 3da6be486e3c02c96e77583cc3279f7dac270481 Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 22:47:36 +0800 Subject: [PATCH 28/55] Move handler to Assets.sol reduce contract size --- contracts/src/Assets.sol | 51 +++++++++++++++++++++++++++++++++++ contracts/src/Gateway.sol | 57 ++++++++------------------------------- 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 6f240c17e4..f50653dc93 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -16,6 +16,7 @@ import {Address} from "./utils/Address.sol"; import {AgentExecutor} from "./AgentExecutor.sol"; import {Agent} from "./Agent.sol"; import {Call} from "./utils/Call.sol"; +import {ERC20} from "./ERC20.sol"; /// @title Library for implementing Ethereum->Polkadot ERC20 transfers. library Assets { @@ -30,6 +31,8 @@ library Assets { error Unsupported(); error InvalidDestinationFee(); error AgentDoesNotExist(); + error TokenAlreadyRegistered(); + error TokenMintFailed(); function isTokenRegistered(address token) external view returns (bool) { return AssetsStorage.layout().tokenRegistry[token].isRegistered; @@ -231,4 +234,52 @@ library Assets { costs.foreign = destinationChainFee; costs.native = 0; } + + // @dev Register a new fungible Polkadot token for an agent + function registerForeignToken( + bytes32 agentID, + address agent, + bytes32 tokenID, + string memory name, + string memory symbol, + uint8 decimals + ) external { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == true) { + revert TokenAlreadyRegistered(); + } + ERC20 foreignToken = new ERC20(agent, name, symbol, decimals); + address token = address(foreignToken); + TokenInfo memory info = + TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); + $.tokenRegistry[token] = info; + $.tokenRegistryByID[tokenID] = info; + emit IGateway.ForeignTokenRegistered(tokenID, agentID, token); + } + + // @dev Mint foreign token from Polkadot + function mintForeignToken(address executor, address agent, bytes32 tokenID, address recipient, uint256 amount) + external + { + address token = _tokenAddressOf(tokenID); + bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (tokenID, token, recipient, amount)); + (bool success,) = Agent(payable(agent)).invoke(executor, call); + if (!success) { + revert TokenMintFailed(); + } + } + + // @dev Get token address by tokenID + function tokenAddressOf(bytes32 tokenID) external view returns (address) { + return _tokenAddressOf(tokenID); + } + + // @dev Get token address by tokenID + function _tokenAddressOf(bytes32 tokenID) internal view returns (address) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.tokenRegistryByID[tokenID].isRegistered == false) { + revert TokenNotRegistered(); + } + return $.tokenRegistryByID[tokenID].token; + } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 624a3e7af7..2f68cc7d58 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -47,7 +47,6 @@ import {PricingStorage} from "./storage/PricingStorage.sol"; import {AssetsStorage} from "./storage/AssetsStorage.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; -import {ERC20} from "./ERC20.sol"; contract Gateway is IGateway, IInitializable { using Address for address; @@ -91,7 +90,6 @@ contract Gateway is IGateway, IInitializable { error InvalidConstructorParams(); error AlreadyInitialized(); error TokenNotRegistered(); - error TokenAlreadyRegistered(); // handler functions are privileged modifier onlySelf() { @@ -283,9 +281,13 @@ contract Gateway is IGateway, IInitializable { abi.decode(params.payload, (AgentExecuteCommand, bytes)); if (command == AgentExecuteCommand.RegisterToken) { - _registerForeignToken(params.agentID, agent, commandParams); + (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = + abi.decode(commandParams, (bytes32, string, string, uint8)); + Assets.registerForeignToken(params.agentID, agent, tokenID, name, symbol, decimals); } else if (command == AgentExecuteCommand.MintToken) { - _mintForeignToken(agent, commandParams); + (bytes32 tokenID, address recipient, uint256 amount) = + abi.decode(commandParams, (bytes32, address, uint256)); + Assets.mintForeignToken(AGENT_EXECUTOR, agent, tokenID, recipient, amount); } else { bytes memory call = abi.encodeCall(AgentExecutor.execute, (command, commandParams)); (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); @@ -416,48 +418,6 @@ contract Gateway is IGateway, IInitializable { emit PricingParametersChanged(); } - // @dev Register a new fungible Polkadot token for an agent - function _registerForeignToken(bytes32 agentID, address agent, bytes memory params) internal { - (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = - abi.decode(params, (bytes32, string, string, uint8)); - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == true) { - revert TokenAlreadyRegistered(); - } - ERC20 foreignToken = new ERC20(agent, name, symbol, decimals); - address token = address(foreignToken); - TokenInfo memory info = - TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); - $.tokenRegistry[token] = info; - $.tokenRegistryByID[tokenID] = info; - emit ForeignTokenRegistered(tokenID, agentID, token); - } - - // @dev Mint foreign token from Polkadot - function _mintForeignToken(address agent, bytes memory params) internal { - (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(params, (bytes32, address, uint256)); - address token = _tokenAddressOf(tokenID); - bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (tokenID, token, recipient, amount)); - (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); - if (!success) { - revert AgentExecutionFailed(returndata); - } - } - - // @dev Get token address by tokenID - function tokenAddressOf(bytes32 tokenID) external view returns (address) { - return _tokenAddressOf(tokenID); - } - - // @dev Get token address by tokenID - function _tokenAddressOf(bytes32 tokenID) internal view returns (address) { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == false) { - revert TokenNotRegistered(); - } - return $.tokenRegistryByID[tokenID].token; - } - /** * Assets */ @@ -519,6 +479,11 @@ contract Gateway is IGateway, IInitializable { } } + // @dev Get token address by tokenID + function tokenAddressOf(bytes32 tokenID) external view returns (address) { + return Assets.tokenAddressOf(tokenID); + } + /** * Internal functions */ From e051f4c20ed99bba33da3581481e0fe57eab11be Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 23:07:16 +0800 Subject: [PATCH 29/55] Refactoring to move transferToken to Assets --- contracts/src/AgentExecutor.sol | 12 +----------- contracts/src/Assets.sol | 12 ++++++++++++ contracts/src/Gateway.sol | 9 +++------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index c28c0275af..5d4bdac3f6 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -21,16 +21,6 @@ contract AgentExecutor { // Emitted when token burnt event TokenBurnt(bytes32 indexed tokenID, address token, address sender, uint256 amount); - /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, - /// the `data` parameter is constructed by the BridgeHub parachain. - /// - function execute(AgentExecuteCommand command, bytes memory params) external { - if (command == AgentExecuteCommand.TransferToken) { - (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); - _transferToken(token, recipient, amount); - } - } - /// @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. /// @@ -39,7 +29,7 @@ contract AgentExecutor { } /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. - function _transferToken(address token, address recipient, uint128 amount) internal { + function transferToken(address token, address recipient, uint128 amount) external { IERC20(token).safeTransfer(recipient, amount); } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index f50653dc93..5f4bfeef9c 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -33,6 +33,7 @@ library Assets { error AgentDoesNotExist(); error TokenAlreadyRegistered(); error TokenMintFailed(); + error TokenTransferFailed(); function isTokenRegistered(address token) external view returns (bool) { return AssetsStorage.layout().tokenRegistry[token].isRegistered; @@ -269,6 +270,17 @@ library Assets { } } + // @dev Transfer ERC20 to `recipient` + function transferToken(address executor, address agent, address token, address recipient, uint128 amount) + external + { + bytes memory call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount)); + (bool success,) = Agent(payable(agent)).invoke(executor, call); + if (!success) { + revert TokenTransferFailed(); + } + } + // @dev Get token address by tokenID function tokenAddressOf(bytes32 tokenID) external view returns (address) { return _tokenAddressOf(tokenID); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 2f68cc7d58..ff5e95f11c 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -288,12 +288,9 @@ contract Gateway is IGateway, IInitializable { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(commandParams, (bytes32, address, uint256)); Assets.mintForeignToken(AGENT_EXECUTOR, agent, tokenID, recipient, amount); - } else { - bytes memory call = abi.encodeCall(AgentExecutor.execute, (command, commandParams)); - (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); - if (!success) { - revert AgentExecutionFailed(returndata); - } + } else if (command == AgentExecuteCommand.TransferToken) { + (address token, address recipient, uint128 amount) = abi.decode(commandParams, (address, address, uint128)); + Assets.transferToken(AGENT_EXECUTOR, agent, token, recipient, amount); } } From 79c1ba0f8848df15b15f3642c299e6b336cf2f81 Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 4 Apr 2024 23:22:01 +0800 Subject: [PATCH 30/55] Remove TokenMinted&TokenBurned event --- contracts/src/AgentExecutor.sol | 11 ++--------- contracts/src/Assets.sol | 15 ++++----------- contracts/test/Gateway.t.sol | 6 +++--- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 5d4bdac3f6..a6d47e094c 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -16,11 +16,6 @@ contract AgentExecutor { using SafeTokenTransfer for IERC20; using SafeNativeTransfer for address payable; - // Emitted when token minted - event TokenMinted(bytes32 indexed tokenID, address token, address recipient, uint256 amount); - // Emitted when token burnt - event TokenBurnt(bytes32 indexed tokenID, address token, address sender, uint256 amount); - /// @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. /// @@ -34,13 +29,11 @@ contract AgentExecutor { } /// @dev Mint ERC20 token to `recipient`. - function mintToken(bytes32 tokenID, address token, address recipient, uint256 amount) external { + function mintToken(address token, address recipient, uint256 amount) external { ERC20(token).mint(recipient, amount); - emit TokenMinted(tokenID, token, recipient, amount); } - function burnToken(bytes32 tokenID, address token, address sender, uint256 amount) external { + function burnToken(address token, address sender, uint256 amount) external { ERC20(token).burn(sender, amount); - emit TokenBurnt(tokenID, token, sender, amount); } } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 5f4bfeef9c..5fd94c2803 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -192,7 +192,7 @@ library Assets { uint128 amount ) external returns (Ticket memory ticket) { // Polkadot-native token: burn wrapped token - _burnToken(executor, agent, info.tokenID, info.token, sender, amount); + _burnToken(executor, agent, info.token, sender, amount); if (destinationChainFee == 0) { revert InvalidDestinationFee(); @@ -218,15 +218,8 @@ library Assets { emit IGateway.TokenSent(info.token, sender, destinationChain, destinationAddress, amount); } - function _burnToken( - address agentExecutor, - address agent, - bytes32 tokenID, - address token, - address sender, - uint256 amount - ) internal { - bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (tokenID, token, sender, amount)); + function _burnToken(address agentExecutor, address agent, address token, address sender, uint256 amount) internal { + bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (token, sender, amount)); (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(agentExecutor, call)); Call.verifyResult(success, returndata); } @@ -263,7 +256,7 @@ library Assets { external { address token = _tokenAddressOf(tokenID); - bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (tokenID, token, recipient, amount)); + bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (token, recipient, amount)); (bool success,) = Agent(payable(agent)).invoke(executor, call); if (!success) { revert TokenMintFailed(); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 3dc4ed46ff..5121b85adc 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -50,8 +50,8 @@ import "./mocks/GatewayUpgradeMock.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; contract GatewayTest is Test { - // Emitted when token minted - event TokenMinted(bytes32 indexed tokenID, address token, address recipient, uint256 amount); + // Emitted when token minted/burnt/transfered + event Transfer(address indexed from, address indexed to, uint256 value); ParaID public bridgeHubParaID = ParaID.wrap(1013); bytes32 public bridgeHubAgentID = 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314; @@ -940,7 +940,7 @@ contract GatewayTest is Test { }); vm.expectEmit(true, true, false, false); - emit TokenMinted(bytes32(uint256(1)), address(0), account1, 1000); + emit Transfer(address(0), account1, 1000); GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); } From fdf15aa92abe3bd9d35ce8d21f2f62e8f29cd4df Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 5 Apr 2024 00:18:58 +0800 Subject: [PATCH 31/55] Move Command.RegisterToken to top level --- contracts/src/Gateway.sol | 21 +++++++++++----- contracts/src/Params.sol | 14 +++++++++++ contracts/src/Types.sol | 4 +-- contracts/test/Gateway.t.sol | 37 ++++++---------------------- contracts/test/mocks/GatewayMock.sol | 4 +++ 5 files changed, 43 insertions(+), 37 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index ff5e95f11c..6ece098a98 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -39,7 +39,8 @@ import { SetOperatingModeParams, TransferNativeFromAgentParams, SetTokenTransferFeesParams, - SetPricingParametersParams + SetPricingParametersParams, + RegisterForeignTokenParams } from "./Params.sol"; import {CoreStorage} from "./storage/CoreStorage.sol"; @@ -214,6 +215,11 @@ contract Gateway is IGateway, IInitializable { catch { success = false; } + } else if (message.command == Command.RegisterForeignToken) { + try Gateway(this).registerForeignToken{gas: maxDispatchGas}(message.params) {} + catch { + success = false; + } } // Calculate a gas refund, capped to protect against huge spikes in `tx.gasprice` @@ -280,11 +286,7 @@ contract Gateway is IGateway, IInitializable { (AgentExecuteCommand command, bytes memory commandParams) = abi.decode(params.payload, (AgentExecuteCommand, bytes)); - if (command == AgentExecuteCommand.RegisterToken) { - (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = - abi.decode(commandParams, (bytes32, string, string, uint8)); - Assets.registerForeignToken(params.agentID, agent, tokenID, name, symbol, decimals); - } else if (command == AgentExecuteCommand.MintToken) { + if (command == AgentExecuteCommand.MintToken) { (bytes32 tokenID, address recipient, uint256 amount) = abi.decode(commandParams, (bytes32, address, uint256)); Assets.mintForeignToken(AGENT_EXECUTOR, agent, tokenID, recipient, amount); @@ -418,6 +420,13 @@ contract Gateway is IGateway, IInitializable { /** * Assets */ + // @dev Register a new fungible Polkadot token for an agent + function registerForeignToken(bytes calldata data) external onlySelf { + RegisterForeignTokenParams memory params = abi.decode(data, (RegisterForeignTokenParams)); + address agent = _ensureAgent(params.agentID); + Assets.registerForeignToken(params.agentID, agent, params.tokenID, params.name, params.symbol, params.decimals); + } + function isTokenRegistered(address token) external view returns (bool) { return Assets.isTokenRegistered(token); } diff --git a/contracts/src/Params.sol b/contracts/src/Params.sol index fc02989152..8ecee9ead4 100644 --- a/contracts/src/Params.sol +++ b/contracts/src/Params.sol @@ -82,3 +82,17 @@ struct SetPricingParametersParams { /// @dev Fee multiplier UD60x18 multiplier; } + +// Payload for RegisterForeignToken +struct RegisterForeignTokenParams { + /// @dev The agent ID of the consensus system + bytes32 agentID; + /// @dev The token ID + bytes32 tokenID; + /// @dev The name of the token + string name; + /// @dev The symbol of the token + string symbol; + /// @dev The decimal of the token + uint8 decimals; +} diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 66ac7b5b9a..360a7b451e 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -84,12 +84,12 @@ enum Command { SetOperatingMode, TransferNativeFromAgent, SetTokenTransferFees, - SetPricingParameters + SetPricingParameters, + RegisterForeignToken } enum AgentExecuteCommand { TransferToken, - RegisterToken, MintToken } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 5121b85adc..9c063e6cb1 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -32,7 +32,8 @@ import { SetOperatingModeParams, TransferNativeFromAgentParams, SetTokenTransferFeesParams, - SetPricingParametersParams + SetPricingParametersParams, + RegisterForeignTokenParams } from "../src/Params.sol"; import { @@ -920,15 +921,18 @@ contract GatewayTest is Test { } function testAgentRegisterDot() public { - AgentExecuteParams memory params = AgentExecuteParams({ + RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ agentID: assetHubAgentID, - payload: abi.encode(AgentExecuteCommand.RegisterToken, abi.encode(dotTokenID, "DOT", "DOT", 10)) + tokenID: dotTokenID, + name: "DOT", + symbol: "DOT", + decimals: 10 }); vm.expectEmit(true, true, false, false); emit IGateway.ForeignTokenRegistered(bytes32(uint256(1)), assetHubAgentID, address(0)); - GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + GatewayMock(address(gateway)).registerForeignTokenPublic(abi.encode(params)); } function testAgentMintDot() public { @@ -964,29 +968,4 @@ contract GatewayTest is Test { IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); } - - function testParseAgentExecuteCall() public { - bytes memory data = - hex"000000000000000000000000000000000000000000000000000000000000002081c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001008080778c30c20fa2ebc0ed18d2cbca1f30b027625c7d9d97f5d589721c91aeb6000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000003646f7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003646f740000000000000000000000000000000000000000000000000000000000"; - - AgentExecuteParams memory params = abi.decode(data, (AgentExecuteParams)); - - (AgentExecuteCommand command, bytes memory payload) = abi.decode(params.payload, (AgentExecuteCommand, bytes)); - - //Register foreign token - assertEq(uint256(command), uint256(1)); - - (bytes32 tokenID, string memory name, string memory symbol, uint8 decimals) = - abi.decode(payload, (bytes32, string, string, uint8)); - assertEq(tokenID, 0x8080778c30c20fa2ebc0ed18d2cbca1f30b027625c7d9d97f5d589721c91aeb6); - - console.log("name:%s", name); - console.log("symbol:%s", symbol); - console.log("decimals:%s", decimals); - - vm.expectEmit(true, true, false, false); - emit IGateway.ForeignTokenRegistered(tokenID, assetHubAgentID, address(0)); - - GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); - } } diff --git a/contracts/test/mocks/GatewayMock.sol b/contracts/test/mocks/GatewayMock.sol index b1014811be..40e58fc367 100644 --- a/contracts/test/mocks/GatewayMock.sol +++ b/contracts/test/mocks/GatewayMock.sol @@ -72,6 +72,10 @@ contract GatewayMock is Gateway { function setPricingParametersPublic(bytes calldata params) external { this.setPricingParameters(params); } + + function registerForeignTokenPublic(bytes calldata params) external { + this.registerForeignToken(params); + } } library AdditionalStorage { From 343921360234e31870f33c6c4d7b2acb556b1dc1 Mon Sep 17 00:00:00 2001 From: ron Date: Sat, 6 Apr 2024 23:15:05 +0800 Subject: [PATCH 32/55] Fix smoke tests --- smoketest/Cargo.lock | 55 +++++++++++++++++++++- smoketest/make-bindings.sh | 2 +- smoketest/src/constants.rs | 2 +- smoketest/tests/transfer_polkadot_token.rs | 15 +++--- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/smoketest/Cargo.lock b/smoketest/Cargo.lock index 5586f962de..b9dbef6854 100644 --- a/smoketest/Cargo.lock +++ b/smoketest/Cargo.lock @@ -896,6 +896,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -1233,6 +1239,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1276,6 +1293,33 @@ dependencies = [ "subtle", ] +[[package]] +name = "docify" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2f138ad521dc4a2ced1a4576148a6a610b4c5923933b062a263130a6802ce" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a081e51fb188742f5a7a1164ad752121abcb22874b21e2c3b0dd040c515fdad" +dependencies = [ + "common-path", + "derive-syn-parse", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn 2.0.55", + "termcolor", + "toml", + "walkdir", +] + [[package]] name = "downcast-rs" version = "1.2.0" @@ -4477,6 +4521,7 @@ dependencies = [ name = "sp-arithmetic" version = "23.0.0" dependencies = [ + "docify", "integer-sqrt", "num-traits", "parity-scale-codec", @@ -4809,7 +4854,6 @@ dependencies = [ "smallvec", "sp-arithmetic 23.0.0", "sp-debug-derive 14.0.0", - "sp-std 14.0.0", ] [[package]] @@ -5112,6 +5156,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.56" diff --git a/smoketest/make-bindings.sh b/smoketest/make-bindings.sh index 9f19656d40..0ce4c7c137 100755 --- a/smoketest/make-bindings.sh +++ b/smoketest/make-bindings.sh @@ -6,7 +6,7 @@ mkdir -p src/contracts # Generate Rust bindings for contracts forge bind --module --overwrite \ - --select 'IGateway|WETH9|GatewayUpgradeMock|AgentExecutor' \ + --select 'IGateway|WETH9|GatewayUpgradeMock|ERC20' \ --bindings-path src/contracts \ --root ../contracts diff --git a/smoketest/src/constants.rs b/smoketest/src/constants.rs index 84613afd84..a0ff29af36 100644 --- a/smoketest/src/constants.rs +++ b/smoketest/src/constants.rs @@ -27,7 +27,7 @@ pub const GATEWAY_PROXY_CONTRACT: [u8; 20] = hex!("EDa338E4dC46038493b885327842f pub const WETH_CONTRACT: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); pub const AGENT_EXECUTOR_CONTRACT: [u8; 20] = hex!("2ffA5ecdBe006d30397c7636d3e015EEE251369F"); -pub const ERC20_DOT_CONTRACT: [u8; 20] = hex!("97d3fC1832792ae4f1D48fEA3E00dc73A9101bf9"); +pub const ERC20_DOT_CONTRACT: [u8; 20] = hex!("B501F2C3051dC8919FAFf398326a705B1f33402A"); pub const ERC20_DOT_TOKEN_ID: [u8; 32] = hex!("83ca7bdddc0205caa1d663fdba16db38a4c702f4dcc07b8589d5ed912610a19d"); diff --git a/smoketest/tests/transfer_polkadot_token.rs b/smoketest/tests/transfer_polkadot_token.rs index cdd63793f9..f9c3b5a763 100644 --- a/smoketest/tests/transfer_polkadot_token.rs +++ b/smoketest/tests/transfer_polkadot_token.rs @@ -7,7 +7,7 @@ use ethers::{ use futures::StreamExt; use snowbridge_smoketest::{ constants::*, - contracts::{agent_executor, agent_executor::TokenMintedFilter}, + contracts::{erc20, erc20::TransferFilter}, helper::AssetHubConfig, parachains::assethub::{ api::runtime_types::{ @@ -72,9 +72,8 @@ async fn transfer_polkadot_token() { .await .expect("call success"); - let agent_executor_addr: Address = AGENT_EXECUTOR_CONTRACT.into(); - let agent_executor = - agent_executor::AgentExecutor::new(agent_executor_addr, ethereum_client.clone()); + let erc20_dot_address: Address = ERC20_DOT_CONTRACT.into(); + let erc20_dot = erc20::ERC20::new(erc20_dot_address, ethereum_client.clone()); let wait_for_blocks = 500; let mut stream = ethereum_client.subscribe_blocks().await.unwrap().take(wait_for_blocks); @@ -82,15 +81,17 @@ async fn transfer_polkadot_token() { 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) = agent_executor - .event::() + if let Ok(transfers) = erc20_dot + .event::() .at_block_hash(block.hash.unwrap()) .query() .await { for transfer in transfers { println!("Transfer event found at ethereum block {:?}", block.number.unwrap()); - println!("token id {:?}", transfer.token_id); + println!("from {:?}", transfer.from); + println!("to {:?}", transfer.to); + assert_eq!(transfer.value, amount.into()); transfer_event_found = true; } } From c2adf8f219d90407de1bafcad1ebaab5cf9617fb Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 8 Apr 2024 22:33:09 +0800 Subject: [PATCH 33/55] Move the check before interactions --- contracts/src/Assets.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 5fd94c2803..20937372b4 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -191,12 +191,11 @@ library Assets { uint128 destinationChainFee, uint128 amount ) external returns (Ticket memory ticket) { - // Polkadot-native token: burn wrapped token - _burnToken(executor, agent, info.token, sender, amount); - if (destinationChainFee == 0) { revert InvalidDestinationFee(); } + // Polkadot-native token: burn wrapped token + _burnToken(executor, agent, info.token, sender, amount); ticket.dest = destinationChain; ticket.costs = _sendForeignTokenCosts(destinationChainFee); From b0b582dd38096f9074e4f085c79cda2934fbee40 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 8 Apr 2024 22:36:10 +0800 Subject: [PATCH 34/55] Polish --- contracts/src/Gateway.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index c26fa5421c..6f8760ec29 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -460,17 +460,17 @@ contract Gateway is IGateway, IInitializable { ) external payable { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - TokenInfo storage info = $.tokenRegistry[token]; - if (!info.isRegistered) { + TokenInfo storage tokenInfo = $.tokenRegistry[token]; + if (!tokenInfo.isRegistered) { revert TokenNotRegistered(); } - if (info.isForeign) { - address agent = _ensureAgent(info.agentID); + if (tokenInfo.isForeign) { + address agent = _ensureAgent(tokenInfo.agentID); _submitOutbound( Assets.sendForeignToken( agent, AGENT_EXECUTOR, - info, + tokenInfo, msg.sender, destinationChain, destinationAddress, From 985115fe6fa72a50daf64ce90b648be7a9e42f8a Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 9 Apr 2024 11:32:15 +0800 Subject: [PATCH 35/55] Introduce ERC20Lib.sol --- contracts/src/ERC20.sol | 209 +++-------------------- contracts/src/ERC20Lib.sol | 341 +++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+), 185 deletions(-) create mode 100644 contracts/src/ERC20Lib.sol diff --git a/contracts/src/ERC20.sol b/contracts/src/ERC20.sol index da8779cb75..b4f2051ea0 100644 --- a/contracts/src/ERC20.sol +++ b/contracts/src/ERC20.sol @@ -6,6 +6,7 @@ pragma solidity 0.8.23; import {IERC20} from "./interfaces/IERC20.sol"; import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; +import {ERC20Lib} from "./ERC20Lib.sol"; /** * @dev Implementation of the {IERC20} interface. @@ -28,34 +29,14 @@ import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; * allowances. See {IERC20-approve}. */ contract ERC20 is IERC20, IERC20Permit { - error PermitExpired(); - error InvalidS(); - error InvalidV(); - error InvalidSignature(); - error Unauthorized(); - error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); - error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); - error OwnableInvalidOwner(address owner); - - mapping(address => uint256) public override balanceOf; - - mapping(address => mapping(address => uint256)) public override allowance; - - mapping(address => uint256) public nonces; + using ERC20Lib for ERC20Lib.TokenStorage; - bytes32 public immutable DOMAIN_SEPARATOR; - uint256 public override totalSupply; - - // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') - bytes32 private constant DOMAIN_TYPE_SIGNATURE_HASH = - bytes32(0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f); + error Unauthorized(); - // keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') - bytes32 private constant PERMIT_SIGNATURE_HASH = - bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); + ERC20Lib.TokenStorage token; address public immutable OWNER; - string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; + uint8 public immutable decimals; string public name; @@ -66,15 +47,10 @@ contract ERC20 is IERC20, IERC20Permit { */ constructor(address _owner, string memory name_, string memory symbol_, uint8 decimals_) { OWNER = _owner; - DOMAIN_SEPARATOR = keccak256( - abi.encode( - DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name_)), keccak256(bytes("1")), block.chainid, address(this) - ) - ); - name = name_; symbol = symbol_; decimals = decimals_; + token.init(name_); } modifier onlyOwner() { @@ -95,14 +71,14 @@ contract ERC20 is IERC20, IERC20Permit { * - `account` cannot be the zero address. */ function mint(address account, uint256 amount) external virtual onlyOwner { - _mint(account, amount); + token.mint(account, amount); } /** * @dev Destroys `amount` tokens from the account. */ function burn(address account, uint256 amount) external virtual onlyOwner { - _burn(account, amount); + token.burn(account, amount); } /** @@ -114,8 +90,7 @@ contract ERC20 is IERC20, IERC20Permit { * - the caller must have a balance of at least `amount`. */ function transfer(address recipient, uint256 amount) external virtual override returns (bool) { - _transfer(msg.sender, recipient, amount); - return true; + return token.transfer(msg.sender, recipient, amount); } /** @@ -134,8 +109,7 @@ contract ERC20 is IERC20, IERC20Permit { * - `spender` cannot be the zero address. */ function approve(address spender, uint256 amount) external virtual override returns (bool) { - _approve(msg.sender, spender, amount); - return true; + return token.approve(msg.sender, spender, amount); } /** @@ -152,20 +126,7 @@ contract ERC20 is IERC20, IERC20Permit { * `amount`. */ function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) { - uint256 _allowance = allowance[sender][msg.sender]; - - if (_allowance != type(uint256).max) { - if (_allowance < amount) { - revert ERC20InsufficientAllowance(msg.sender, _allowance, amount); - } - unchecked { - _approve(sender, msg.sender, _allowance - amount); - } - } - - _transfer(sender, recipient, amount); - - return true; + return token.transferFrom(sender, recipient, amount); } /** @@ -181,11 +142,7 @@ contract ERC20 is IERC20, IERC20Permit { * - `spender` cannot be the zero address. */ function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) { - uint256 _allowance = allowance[msg.sender][spender]; - if (_allowance != type(uint256).max) { - _approve(msg.sender, spender, _allowance + addedValue); - } - return true; + return token.increaseAllowance(spender, addedValue); } /** @@ -203,150 +160,32 @@ contract ERC20 is IERC20, IERC20Permit { * `subtractedValue`. */ function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) { - uint256 _allowance = allowance[msg.sender][spender]; - if (_allowance != type(uint256).max) { - if (_allowance < subtractedValue) { - revert ERC20InsufficientAllowance(msg.sender, _allowance, subtractedValue); - } - unchecked { - _approve(msg.sender, spender, _allowance - subtractedValue); - } - } - return true; + return token.decreaseAllowance(spender, subtractedValue); } function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { - if (block.timestamp > deadline) revert PermitExpired(); - - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); - - if (v != 27 && v != 28) revert InvalidV(); - - bytes32 digest = keccak256( - abi.encodePacked( - EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, - DOMAIN_SEPARATOR, - keccak256(abi.encode(PERMIT_SIGNATURE_HASH, issuer, spender, value, nonces[issuer]++, deadline)) - ) - ); - - address recoveredAddress = ecrecover(digest, v, r, s); - - if (recoveredAddress != issuer) revert InvalidSignature(); - - // _approve will revert if issuer is address(0x0) - _approve(issuer, spender, value); + token.permit(issuer, spender, value, deadline, v, r, s); } - /** - * @dev Moves tokens `amount` from `sender` to `recipient`. - * - * This is internal function is equivalent to {transfer}, and can be used to - * e.g. implement automatic token fees, slashing mechanisms, etc. - * - * Emits a {Transfer} event. - * - * Requirements: - * - * - `sender` cannot be the zero address. - * - `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - */ - function _transfer(address sender, address recipient, uint256 amount) internal virtual { - if (sender == address(0) || recipient == address(0)) revert InvalidAccount(); - - _update(sender, recipient, amount); + function balanceOf(address account) external view returns (uint256) { + return token.balancesOf(account); } - /** - * @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * Emits a {Transfer} event with `from` set to the zero address. - * - * Requirements: - * - * - `to` cannot be the zero address. - */ - function _mint(address account, uint256 amount) internal virtual { - if (account == address(0)) revert InvalidAccount(); - - _update(address(0), account, amount); + function nonces(address account) external view returns (uint256) { + return token.noncesOf(account); } - /** - * @dev Destroys `amount` tokens from `account`, reducing the - * total supply. - * - * Emits a {Transfer} event with `to` set to the zero address. - * - * Requirements: - * - * - `account` cannot be the zero address. - * - `account` must have at least `amount` tokens. - */ - function _burn(address account, uint256 amount) internal virtual { - if (account == address(0)) revert InvalidAccount(); - - _update(account, address(0), amount); + function totalSupply() external view returns (uint256) { + return token.totalSupplyOf(); } - /** - * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. - * - * This internal function is equivalent to `approve`, and can be used to - * e.g. set automatic allowances for certain subsystems, etc. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `owner` cannot be the zero address. - * - `spender` cannot be the zero address. - */ - function _approve(address owner, address spender, uint256 amount) internal virtual { - if (owner == address(0) || spender == address(0)) revert InvalidAccount(); - - allowance[owner][spender] = amount; - emit Approval(owner, spender, amount); + function allowance(address owner, address spender) external view returns (uint256) { + return token.allowanceOf(owner, spender); } - /** - * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` - * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding - * this function. - * - * Emits a {Transfer} event. - */ - function _update(address from, address to, uint256 value) internal virtual { - if (from == address(0)) { - // Overflow check required: The rest of the code assumes that totalSupply never overflows - totalSupply += value; - } else { - uint256 fromBalance = balanceOf[from]; - if (fromBalance < value) { - revert ERC20InsufficientBalance(from, fromBalance, value); - } - unchecked { - // Overflow not possible: value <= fromBalance <= totalSupply. - balanceOf[from] = fromBalance - value; - } - } - - if (to == address(0)) { - unchecked { - // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. - totalSupply -= value; - } - } else { - unchecked { - // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. - balanceOf[to] += value; - } - } - - emit Transfer(from, to, value); + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return token.domainSeparatorOf(); } } diff --git a/contracts/src/ERC20Lib.sol b/contracts/src/ERC20Lib.sol new file mode 100644 index 0000000000..1fa882600d --- /dev/null +++ b/contracts/src/ERC20Lib.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Axelar Network +// SPDX-FileCopyrightText: 2023 Snowfork + +pragma solidity 0.8.23; + +import {IERC20} from "./interfaces/IERC20.sol"; +import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; + +library ERC20Lib { + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') + bytes32 internal constant DOMAIN_TYPE_SIGNATURE_HASH = + bytes32(0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f); + + // keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') + bytes32 internal constant PERMIT_SIGNATURE_HASH = + bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); + + string internal constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; + + error InvalidAccount(); + error PermitExpired(); + error InvalidS(); + error InvalidV(); + error InvalidSignature(); + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + error OwnableInvalidOwner(address owner); + + struct TokenStorage { + mapping(address => uint256) balanceOf; + mapping(address => mapping(address => uint256)) allowance; + mapping(address => uint256) nonces; + uint256 totalSupply; + bytes32 domainSeparator; + } + + bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets.foreign"); + + function layout() internal pure returns (TokenStorage storage $) { + bytes32 slot = SLOT; + assembly { + $.slot := slot + } + } + + function init(TokenStorage storage self, string memory name_) internal { + self.domainSeparator = keccak256( + abi.encode( + DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name_)), keccak256(bytes("1")), block.chainid, address(this) + ) + ); + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(TokenStorage storage self, address sender, address recipient, uint256 amount) + external + returns (bool) + { + _transfer(self, sender, recipient, amount); + return true; + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `to` cannot be the zero address. + */ + function mint(TokenStorage storage self, address account, uint256 amount) external { + if (account == address(0)) revert InvalidAccount(); + + _update(self, address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function burn(TokenStorage storage self, address account, uint256 amount) external { + if (account == address(0)) revert InvalidAccount(); + + _update(self, account, address(0), amount); + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: Prefer the {increaseAllowance} and {decreaseAllowance} methods, as + * they aren't vulnerable to the frontrunning attack described here: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(TokenStorage storage self, address owner, address spender, uint256 amount) + external + returns (bool) + { + _approve(self, owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom(TokenStorage storage self, address sender, address recipient, uint256 amount) + external + returns (bool) + { + uint256 _allowance = self.allowance[sender][msg.sender]; + + if (_allowance != type(uint256).max) { + if (_allowance < amount) { + revert ERC20InsufficientAllowance(msg.sender, _allowance, amount); + } + unchecked { + _approve(self, sender, msg.sender, _allowance - amount); + } + } + + _transfer(self, sender, recipient, amount); + + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(TokenStorage storage self, address spender, uint256 addedValue) + external + returns (bool) + { + uint256 _allowance = self.allowance[msg.sender][spender]; + if (_allowance != type(uint256).max) { + _approve(self, msg.sender, spender, _allowance + addedValue); + } + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(TokenStorage storage self, address spender, uint256 subtractedValue) + external + returns (bool) + { + uint256 _allowance = self.allowance[msg.sender][spender]; + if (_allowance != type(uint256).max) { + if (_allowance < subtractedValue) { + revert ERC20InsufficientAllowance(msg.sender, _allowance, subtractedValue); + } + unchecked { + _approve(self, msg.sender, spender, _allowance - subtractedValue); + } + } + return true; + } + + function permit( + TokenStorage storage self, + address issuer, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + if (block.timestamp > deadline) revert PermitExpired(); + + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); + + if (v != 27 && v != 28) revert InvalidV(); + + bytes32 digest = keccak256( + abi.encodePacked( + EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, + self.domainSeparator, + keccak256(abi.encode(PERMIT_SIGNATURE_HASH, issuer, spender, value, self.nonces[issuer]++, deadline)) + ) + ); + + address recoveredAddress = ecrecover(digest, v, r, s); + + if (recoveredAddress != issuer) revert InvalidSignature(); + + // _approve will revert if issuer is address(0x0) + _approve(self, issuer, spender, value); + } + + function balancesOf(TokenStorage storage self, address account) internal view returns (uint256) { + return self.balanceOf[account]; + } + + function noncesOf(TokenStorage storage self, address account) external view returns (uint256) { + return self.nonces[account]; + } + + function totalSupplyOf(TokenStorage storage self) external view returns (uint256) { + return self.totalSupply; + } + + function allowanceOf(TokenStorage storage self, address owner, address spender) external view returns (uint256) { + return self.allowance[owner][spender]; + } + + function domainSeparatorOf(TokenStorage storage self) external view returns (bytes32) { + return self.domainSeparator; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(TokenStorage storage self, address sender, address recipient, uint256 amount) internal { + if (sender == address(0) || recipient == address(0)) revert InvalidAccount(); + + _update(self, sender, recipient, amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(TokenStorage storage self, address owner, address spender, uint256 amount) internal { + if (owner == address(0) || spender == address(0)) revert InvalidAccount(); + + self.allowance[owner][spender] = amount; + emit IERC20.Approval(owner, spender, amount); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(TokenStorage storage self, address from, address to, uint256 value) internal { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + self.totalSupply += value; + } else { + uint256 fromBalance = self.balanceOf[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + self.balanceOf[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + self.totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + self.balanceOf[to] += value; + } + } + + emit IERC20.Transfer(from, to, value); + } +} From ec335484a6c27a98d83d6cf1fd06555d2d35bbc4 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 9 Apr 2024 13:14:43 +0800 Subject: [PATCH 36/55] Remove unused --- contracts/src/ERC20Lib.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contracts/src/ERC20Lib.sol b/contracts/src/ERC20Lib.sol index 1fa882600d..334c018080 100644 --- a/contracts/src/ERC20Lib.sol +++ b/contracts/src/ERC20Lib.sol @@ -35,15 +35,6 @@ library ERC20Lib { bytes32 domainSeparator; } - bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets.foreign"); - - function layout() internal pure returns (TokenStorage storage $) { - bytes32 slot = SLOT; - assembly { - $.slot := slot - } - } - function init(TokenStorage storage self, string memory name_) internal { self.domainSeparator = keccak256( abi.encode( From 32f8d86de73f43ec5ec15950794236fe350a6e90 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 9 Apr 2024 13:25:10 +0800 Subject: [PATCH 37/55] More test --- contracts/test/Gateway.t.sol | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 9c063e6cb1..122de7584e 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -49,6 +49,7 @@ import { import {WETH9} from "canonical-weth/WETH9.sol"; import "./mocks/GatewayUpgradeMock.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; +import {ERC20} from "../src/ERC20.sol"; contract GatewayTest is Test { // Emitted when token minted/burnt/transfered @@ -938,15 +939,23 @@ contract GatewayTest is Test { function testAgentMintDot() public { testAgentRegisterDot(); + uint256 amount = 1000; + AgentExecuteParams memory params = AgentExecuteParams({ agentID: assetHubAgentID, - payload: abi.encode(AgentExecuteCommand.MintToken, abi.encode(bytes32(uint256(1)), account1, 1000)) + payload: abi.encode(AgentExecuteCommand.MintToken, abi.encode(bytes32(uint256(1)), account1, amount)) }); vm.expectEmit(true, true, false, false); emit Transfer(address(0), account1, 1000); GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + + address dotToken = GatewayMock(address(gateway)).tokenAddressOf(dotTokenID); + + uint256 balance = ERC20(dotToken).balanceOf(account1); + + assertEq(balance, amount); } function testSendRelayTokenToAssetHub() public { From b2a666ac836fd926e087580ace88acb98e58a0d4 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 9 Apr 2024 18:01:02 +0800 Subject: [PATCH 38/55] Remove AgentExecuteCommand --- contracts/src/AgentExecutor.sol | 2 +- contracts/src/Gateway.sol | 55 +++++++++++++--------------- contracts/src/Params.sol | 30 ++++++++++++--- contracts/src/Types.sol | 10 ++--- contracts/test/Gateway.t.sol | 40 +++++++------------- contracts/test/mocks/GatewayMock.sol | 12 ++++-- 6 files changed, 75 insertions(+), 74 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index a6d47e094c..8ce7a9bae5 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023 Snowfork pragma solidity 0.8.23; -import {AgentExecuteCommand, ParaID} from "./Types.sol"; +import {ParaID} from "./Types.sol"; import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 6f8760ec29..b27a655cb9 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -18,8 +18,7 @@ import { MultiAddress, Ticket, Costs, - TokenInfo, - AgentExecuteCommand + TokenInfo } from "./Types.sol"; import {IGateway} from "./interfaces/IGateway.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; @@ -33,14 +32,15 @@ import {ScaleCodec} from "./utils/ScaleCodec.sol"; import { UpgradeParams, CreateAgentParams, - AgentExecuteParams, CreateChannelParams, UpdateChannelParams, SetOperatingModeParams, TransferNativeFromAgentParams, SetTokenTransferFeesParams, SetPricingParametersParams, - RegisterForeignTokenParams + RegisterForeignTokenParams, + MintForeignTokenParams, + TransferTokenParams } from "./Params.sol"; import {CoreStorage} from "./storage/CoreStorage.sol"; @@ -85,7 +85,6 @@ contract Gateway is IGateway, IInitializable { error ChannelAlreadyCreated(); error ChannelDoesNotExist(); error InvalidChannelUpdate(); - error AgentExecutionFailed(bytes returndata); error InvalidAgentExecutionPayload(); error InvalidCodeHash(); error InvalidConstructorParams(); @@ -170,8 +169,8 @@ contract Gateway is IGateway, IInitializable { bool success = true; // Dispatch message to a handler - if (message.command == Command.AgentExecute) { - try Gateway(this).agentExecute{gas: maxDispatchGas}(message.params) {} + if (message.command == Command.TransferToken) { + try Gateway(this).transferToken{gas: maxDispatchGas}(message.params) {} catch { success = false; } @@ -220,6 +219,11 @@ contract Gateway is IGateway, IInitializable { catch { success = false; } + } else if (message.command == Command.MintForeignToken) { + try Gateway(this).mintForeignToken{gas: maxDispatchGas}(message.params) {} + catch { + success = false; + } } // Calculate a gas refund, capped to protect against huge spikes in `tx.gasprice` @@ -273,29 +277,6 @@ contract Gateway is IGateway, IInitializable { * Handlers */ - // Execute code within an agent - function agentExecute(bytes calldata data) external onlySelf { - AgentExecuteParams memory params = abi.decode(data, (AgentExecuteParams)); - - address agent = _ensureAgent(params.agentID); - - if (params.payload.length == 0) { - revert InvalidAgentExecutionPayload(); - } - - (AgentExecuteCommand command, bytes memory commandParams) = - abi.decode(params.payload, (AgentExecuteCommand, bytes)); - - if (command == AgentExecuteCommand.MintToken) { - (bytes32 tokenID, address recipient, uint256 amount) = - abi.decode(commandParams, (bytes32, address, uint256)); - Assets.mintForeignToken(AGENT_EXECUTOR, agent, tokenID, recipient, amount); - } else if (command == AgentExecuteCommand.TransferToken) { - (address token, address recipient, uint128 amount) = abi.decode(commandParams, (address, address, uint128)); - Assets.transferToken(AGENT_EXECUTOR, agent, token, recipient, amount); - } - } - /// @dev Create an agent for a consensus system on Polkadot function createAgent(bytes calldata data) external onlySelf { CoreStorage.Layout storage $ = CoreStorage.layout(); @@ -427,6 +408,20 @@ contract Gateway is IGateway, IInitializable { Assets.registerForeignToken(params.agentID, agent, params.tokenID, params.name, params.symbol, params.decimals); } + // @dev Mint foreign token from polkadot + function mintForeignToken(bytes calldata data) external onlySelf { + MintForeignTokenParams memory params = abi.decode(data, (MintForeignTokenParams)); + address agent = _ensureAgent(params.agentID); + Assets.mintForeignToken(AGENT_EXECUTOR, agent, params.tokenID, params.recipient, params.amount); + } + + // @dev Transfer Ethereum native token back from polkadot + function transferToken(bytes calldata data) external onlySelf { + TransferTokenParams memory params = abi.decode(data, (TransferTokenParams)); + address agent = _ensureAgent(params.agentID); + Assets.transferToken(AGENT_EXECUTOR, agent, params.token, params.recipient, params.amount); + } + function isTokenRegistered(address token) external view returns (bool) { return Assets.isTokenRegistered(token); } diff --git a/contracts/src/Params.sol b/contracts/src/Params.sol index 8ecee9ead4..99aedd19d8 100644 --- a/contracts/src/Params.sol +++ b/contracts/src/Params.sol @@ -5,12 +5,6 @@ pragma solidity 0.8.23; import {ChannelID, OperatingMode} from "./Types.sol"; import {UD60x18} from "prb/math/src/UD60x18.sol"; -// Payload for AgentExecute -struct AgentExecuteParams { - bytes32 agentID; - bytes payload; -} - // Payload for CreateAgent struct CreateAgentParams { /// @dev The agent ID of the consensus system @@ -96,3 +90,27 @@ struct RegisterForeignTokenParams { /// @dev The decimal of the token uint8 decimals; } + +// Payload for MintForeignToken +struct MintForeignTokenParams { + /// @dev The agent ID of the consensus system + bytes32 agentID; + /// @dev The token ID + bytes32 tokenID; + /// @dev The address of the recipient + address recipient; + /// @dev The amount to mint with + uint256 amount; +} + +// Payload for TransferToken +struct TransferTokenParams { + /// @dev The agent ID of the consensus system + bytes32 agentID; + /// @dev The token address + address token; + /// @dev The address of the recipient + address recipient; + /// @dev The amount to mint with + uint128 amount; +} diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 360a7b451e..afe21ab17f 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -76,7 +76,7 @@ enum OperatingMode { /// @dev Messages from Polkadot take the form of these commands. enum Command { - AgentExecute, + TransferToken, Upgrade, CreateAgent, CreateChannel, @@ -85,12 +85,8 @@ enum Command { TransferNativeFromAgent, SetTokenTransferFees, SetPricingParameters, - RegisterForeignToken -} - -enum AgentExecuteCommand { - TransferToken, - MintToken + RegisterForeignToken, + MintForeignToken } /// @dev Application-level costs for a message diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 122de7584e..fa1ae51dc2 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -26,18 +26,18 @@ import {PricingStorage} from "../src/storage/PricingStorage.sol"; import { UpgradeParams, CreateAgentParams, - AgentExecuteParams, CreateChannelParams, UpdateChannelParams, SetOperatingModeParams, TransferNativeFromAgentParams, SetTokenTransferFeesParams, SetPricingParametersParams, - RegisterForeignTokenParams + RegisterForeignTokenParams, + TransferTokenParams, + MintForeignTokenParams } from "../src/Params.sol"; import { - AgentExecuteCommand, InboundMessage, OperatingMode, ParaID, @@ -351,30 +351,19 @@ contract GatewayTest is Test { function testAgentExecution() public { token.transfer(address(assetHubAgent), 200); - AgentExecuteParams memory params = AgentExecuteParams({ - agentID: assetHubAgentID, - payload: abi.encode(AgentExecuteCommand.TransferToken, abi.encode(address(token), address(account2), 10)) - }); + TransferTokenParams memory params = + TransferTokenParams({agentID: assetHubAgentID, token: address(token), recipient: account2, amount: 10}); bytes memory encodedParams = abi.encode(params); - GatewayMock(address(gateway)).agentExecutePublic(encodedParams); + GatewayMock(address(gateway)).transferTokenPublic(encodedParams); } function testAgentExecutionBadOrigin() public { - AgentExecuteParams memory params = AgentExecuteParams({ - agentID: bytes32(0), - payload: abi.encode(keccak256("transferNativeToken"), abi.encode(address(token), address(this), 1)) - }); + TransferNativeFromAgentParams memory params = + TransferNativeFromAgentParams({agentID: bytes32(0), recipient: address(this), amount: 1}); vm.expectRevert(Gateway.AgentDoesNotExist.selector); - GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); - } - - function testAgentExecutionBadPayload() public { - AgentExecuteParams memory params = AgentExecuteParams({agentID: assetHubAgentID, payload: ""}); - - vm.expectRevert(Gateway.InvalidAgentExecutionPayload.selector); - GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + GatewayMock(address(gateway)).transferNativeFromAgentPublic(abi.encode(params)); } function testCreateAgent() public { @@ -797,9 +786,6 @@ contract GatewayTest is Test { // Handler functions should not be externally callable function testHandlersNotExternallyCallable() public { - vm.expectRevert(Gateway.Unauthorized.selector); - Gateway(address(gateway)).agentExecute(""); - vm.expectRevert(Gateway.Unauthorized.selector); Gateway(address(gateway)).createAgent(""); @@ -941,15 +927,17 @@ contract GatewayTest is Test { uint256 amount = 1000; - AgentExecuteParams memory params = AgentExecuteParams({ + MintForeignTokenParams memory params = MintForeignTokenParams({ agentID: assetHubAgentID, - payload: abi.encode(AgentExecuteCommand.MintToken, abi.encode(bytes32(uint256(1)), account1, amount)) + tokenID: bytes32(uint256(1)), + recipient: account1, + amount: amount }); vm.expectEmit(true, true, false, false); emit Transfer(address(0), account1, 1000); - GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params)); + GatewayMock(address(gateway)).mintForeignTokenPublic(abi.encode(params)); address dotToken = GatewayMock(address(gateway)).tokenAddressOf(dotTokenID); diff --git a/contracts/test/mocks/GatewayMock.sol b/contracts/test/mocks/GatewayMock.sol index 40e58fc367..3a86626464 100644 --- a/contracts/test/mocks/GatewayMock.sol +++ b/contracts/test/mocks/GatewayMock.sol @@ -19,10 +19,6 @@ contract GatewayMock is Gateway { uint8 foreignTokenDecimals ) Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubHubAgentID, foreignTokenDecimals) {} - function agentExecutePublic(bytes calldata params) external { - this.agentExecute(params); - } - function createAgentPublic(bytes calldata params) external { this.createAgent(params); } @@ -76,6 +72,14 @@ contract GatewayMock is Gateway { function registerForeignTokenPublic(bytes calldata params) external { this.registerForeignToken(params); } + + function mintForeignTokenPublic(bytes calldata params) external { + this.mintForeignToken(params); + } + + function transferTokenPublic(bytes calldata params) external { + this.transferToken(params); + } } library AdditionalStorage { From 6148c038b97c2a26982dd0825576438d73a21b44 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 9 Apr 2024 22:23:41 +0800 Subject: [PATCH 39/55] More contract tests --- contracts/test/Gateway.t.sol | 76 ++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index fa1ae51dc2..e2670a8098 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -22,6 +22,7 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; +import {ERC20Lib} from "../src/ERC20Lib.sol"; import { UpgradeParams, @@ -907,7 +908,7 @@ contract GatewayTest is Test { IGateway(address(gateway)).sendToken{value: fee}(address(token), destPara, recipientAddress32, 0, 1); } - function testAgentRegisterDot() public { + function testAgentRegisterToken() public { RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ agentID: assetHubAgentID, tokenID: dotTokenID, @@ -922,8 +923,40 @@ contract GatewayTest is Test { GatewayMock(address(gateway)).registerForeignTokenPublic(abi.encode(params)); } - function testAgentMintDot() public { - testAgentRegisterDot(); + function testAgentRegisterTokenWithAgentIDNotExistWillFail() public { + testAgentRegisterToken(); + + RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ + agentID: bytes32(0), + tokenID: dotTokenID, + name: "DOT", + symbol: "DOT", + decimals: 10 + }); + + vm.expectRevert(Gateway.AgentDoesNotExist.selector); + + GatewayMock(address(gateway)).registerForeignTokenPublic(abi.encode(params)); + } + + function testAgentRegisterSameTokenAgainWillFail() public { + testAgentRegisterToken(); + + RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ + agentID: assetHubAgentID, + tokenID: dotTokenID, + name: "DOT", + symbol: "DOT", + decimals: 10 + }); + + vm.expectRevert(Assets.TokenAlreadyRegistered.selector); + + GatewayMock(address(gateway)).registerForeignTokenPublic(abi.encode(params)); + } + + function testAgentMintToken() public { + testAgentRegisterToken(); uint256 amount = 1000; @@ -946,9 +979,22 @@ contract GatewayTest is Test { assertEq(balance, amount); } + function testAgentMintNotRegisteredTokenWillFail() public { + MintForeignTokenParams memory params = MintForeignTokenParams({ + agentID: assetHubAgentID, + tokenID: bytes32(uint256(1)), + recipient: account1, + amount: 1000 + }); + + vm.expectRevert(Assets.TokenNotRegistered.selector); + + GatewayMock(address(gateway)).mintForeignTokenPublic(abi.encode(params)); + } + function testSendRelayTokenToAssetHub() public { // Register and then mint some DOT to account1 - testAgentMintDot(); + testAgentMintToken(); address dotToken = GatewayMock(address(gateway)).tokenAddressOf(dotTokenID); @@ -965,4 +1011,26 @@ contract GatewayTest is Test { IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); } + + function testSendNotRegisteredTokenWillFail() public { + ParaID destPara = assetHubParaID; + + vm.expectRevert(Assets.TokenNotRegistered.selector); + + IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(0x0), destPara, recipientAddress32, 1, 1); + } + + function testSendTokenFromNotMintedAccountWillFail() public { + testAgentRegisterToken(); + + address dotToken = GatewayMock(address(gateway)).tokenAddressOf(dotTokenID); + + ParaID destPara = assetHubParaID; + + vm.prank(account1); + + vm.expectRevert(abi.encodeWithSelector(ERC20Lib.ERC20InsufficientBalance.selector, account1, 0, 1)); + + IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); + } } From 4075c11e2e0248110b19d566cef5a6a058e2bb37 Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 11 Apr 2024 00:18:09 +0800 Subject: [PATCH 40/55] Update contract address --- smoketest/src/constants.rs | 10 +++++----- web/packages/test/scripts/set-env.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smoketest/src/constants.rs b/smoketest/src/constants.rs index a0ff29af36..b6ac555ae1 100644 --- a/smoketest/src/constants.rs +++ b/smoketest/src/constants.rs @@ -23,13 +23,13 @@ pub const ETHEREUM_ADDRESS: [u8; 20] = hex!("90A987B944Cb1dCcE5564e5FDeCD7a54D3d // The deployment addresses of the following contracts are stable in our E2E env, unless we modify // the order in contracts are deployed in DeployScript.sol. -pub const GATEWAY_PROXY_CONTRACT: [u8; 20] = hex!("EDa338E4dC46038493b885327842fD3E301CaB39"); -pub const WETH_CONTRACT: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); -pub const AGENT_EXECUTOR_CONTRACT: [u8; 20] = hex!("2ffA5ecdBe006d30397c7636d3e015EEE251369F"); +pub const GATEWAY_PROXY_CONTRACT: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); +pub const WETH_CONTRACT: [u8; 20] = hex!("774667629726ec1FaBEbCEc0D9139bD1C8f72a23"); +pub const AGENT_EXECUTOR_CONTRACT: [u8; 20] = hex!("Fc97A6197dc90bef6bbEFD672742Ed75E9768553"); -pub const ERC20_DOT_CONTRACT: [u8; 20] = hex!("B501F2C3051dC8919FAFf398326a705B1f33402A"); +pub const ERC20_DOT_CONTRACT: [u8; 20] = hex!("B8C39CbCe8106c8415472e3AAe88Eb694Cc70B57"); pub const ERC20_DOT_TOKEN_ID: [u8; 32] = - hex!("83ca7bdddc0205caa1d663fdba16db38a4c702f4dcc07b8589d5ed912610a19d"); + hex!("fb3d635c7cb573d1b9e9bff4a64ab4f25190d29b6fd8db94c605a218a23fa9ad"); // Agent for bridge hub parachain 1013 pub const BRIDGE_HUB_AGENT_ID: [u8; 32] = diff --git a/web/packages/test/scripts/set-env.sh b/web/packages/test/scripts/set-env.sh index 85481110e2..f007ade14c 100755 --- a/web/packages/test/scripts/set-env.sh +++ b/web/packages/test/scripts/set-env.sh @@ -111,7 +111,7 @@ export REMOTE_REWARD="${REMOTE_REWARD:-1000000000000000}" export BRIDGE_HUB_INITIAL_DEPOSIT="${ETH_BRIDGE_HUB_INITIAL_DEPOSIT:-10000000000000000000}" export GATEWAY_STORAGE_KEY="${GATEWAY_STORAGE_KEY:-0xaed97c7854d601808b98ae43079dafb3}" -export GATEWAY_PROXY_CONTRACT="${GATEWAY_PROXY_CONTRACT:-0xEDa338E4dC46038493b885327842fD3E301CaB39}" +export GATEWAY_PROXY_CONTRACT="${GATEWAY_PROXY_CONTRACT:-0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d}" address_for() { jq -r ".contracts.${1}.address" "$output_dir/contracts.json" From fcd7539afd1d7d36b1a5b3d2bf53b49c54fe956a Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 11 Apr 2024 13:44:24 +0800 Subject: [PATCH 41/55] Add AgentExecuteCommand back for compatibility --- contracts/src/AgentExecutor.sol | 20 +++++++++++++++-- contracts/src/Gateway.sol | 32 +++++++++++++++++++++++++--- contracts/src/Params.sol | 6 ++++++ contracts/src/Types.sol | 8 ++++++- contracts/test/Gateway.t.sol | 14 ++++++++++++ contracts/test/mocks/GatewayMock.sol | 4 ++++ 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 8ce7a9bae5..6727ee8af0 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023 Snowfork pragma solidity 0.8.23; -import {ParaID} from "./Types.sol"; +import {AgentExecuteCommand, ParaID} from "./Types.sol"; import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; @@ -16,6 +16,17 @@ contract AgentExecutor { using SafeTokenTransfer for IERC20; using SafeNativeTransfer for address payable; + /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, + /// the `data` parameter is constructed by the BridgeHub parachain. + /// + function execute(bytes memory data) external { + (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); + if (command == AgentExecuteCommand.TransferToken) { + (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); + _transferToken(token, recipient, amount); + } + } + /// @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. /// @@ -24,10 +35,15 @@ contract AgentExecutor { } /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. - function transferToken(address token, address recipient, uint128 amount) external { + function _transferToken(address token, address recipient, uint128 amount) internal { IERC20(token).safeTransfer(recipient, amount); } + /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. + function transferToken(address token, address recipient, uint128 amount) external { + _transferToken(token, recipient, amount); + } + /// @dev Mint ERC20 token to `recipient`. function mintToken(address token, address recipient, uint256 amount) external { ERC20(token).mint(recipient, amount); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index b27a655cb9..0cbee80984 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -18,7 +18,8 @@ import { MultiAddress, Ticket, Costs, - TokenInfo + TokenInfo, + AgentExecuteCommand } from "./Types.sol"; import {IGateway} from "./interfaces/IGateway.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; @@ -30,6 +31,7 @@ import {Math} from "./utils/Math.sol"; import {ScaleCodec} from "./utils/ScaleCodec.sol"; import { + AgentExecuteParams, UpgradeParams, CreateAgentParams, CreateChannelParams, @@ -85,6 +87,7 @@ contract Gateway is IGateway, IInitializable { error ChannelAlreadyCreated(); error ChannelDoesNotExist(); error InvalidChannelUpdate(); + error AgentExecutionFailed(bytes returndata); error InvalidAgentExecutionPayload(); error InvalidCodeHash(); error InvalidConstructorParams(); @@ -169,8 +172,8 @@ contract Gateway is IGateway, IInitializable { bool success = true; // Dispatch message to a handler - if (message.command == Command.TransferToken) { - try Gateway(this).transferToken{gas: maxDispatchGas}(message.params) {} + if (message.command == Command.AgentExecute) { + try Gateway(this).agentExecute{gas: maxDispatchGas}(message.params) {} catch { success = false; } @@ -214,6 +217,11 @@ contract Gateway is IGateway, IInitializable { catch { success = false; } + } else if (message.command == Command.TransferToken) { + try Gateway(this).transferToken{gas: maxDispatchGas}(message.params) {} + catch { + success = false; + } } else if (message.command == Command.RegisterForeignToken) { try Gateway(this).registerForeignToken{gas: maxDispatchGas}(message.params) {} catch { @@ -277,6 +285,24 @@ contract Gateway is IGateway, IInitializable { * Handlers */ + // Execute code within an agent + function agentExecute(bytes calldata data) external onlySelf { + AgentExecuteParams memory params = abi.decode(data, (AgentExecuteParams)); + + address agent = _ensureAgent(params.agentID); + + if (params.payload.length == 0) { + revert InvalidAgentExecutionPayload(); + } + + bytes memory call = abi.encodeCall(AgentExecutor.execute, params.payload); + + (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); + if (!success) { + revert AgentExecutionFailed(returndata); + } + } + /// @dev Create an agent for a consensus system on Polkadot function createAgent(bytes calldata data) external onlySelf { CoreStorage.Layout storage $ = CoreStorage.layout(); diff --git a/contracts/src/Params.sol b/contracts/src/Params.sol index 99aedd19d8..8287437d4c 100644 --- a/contracts/src/Params.sol +++ b/contracts/src/Params.sol @@ -5,6 +5,12 @@ pragma solidity 0.8.23; import {ChannelID, OperatingMode} from "./Types.sol"; import {UD60x18} from "prb/math/src/UD60x18.sol"; +// Payload for AgentExecute +struct AgentExecuteParams { + bytes32 agentID; + bytes payload; +} + // Payload for CreateAgent struct CreateAgentParams { /// @dev The agent ID of the consensus system diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index afe21ab17f..ee35dd026a 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -76,7 +76,7 @@ enum OperatingMode { /// @dev Messages from Polkadot take the form of these commands. enum Command { - TransferToken, + AgentExecute, Upgrade, CreateAgent, CreateChannel, @@ -85,10 +85,16 @@ enum Command { TransferNativeFromAgent, SetTokenTransferFees, SetPricingParameters, + TransferToken, RegisterForeignToken, MintForeignToken } +enum AgentExecuteCommand { + TransferToken, + MintToken +} + /// @dev Application-level costs for a message struct Costs { /// @dev Costs in foreign currency diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index e2670a8098..17aa42563d 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -27,6 +27,7 @@ import {ERC20Lib} from "../src/ERC20Lib.sol"; import { UpgradeParams, CreateAgentParams, + AgentExecuteParams, CreateChannelParams, UpdateChannelParams, SetOperatingModeParams, @@ -39,6 +40,7 @@ import { } from "../src/Params.sol"; import { + AgentExecuteCommand, InboundMessage, OperatingMode, ParaID, @@ -1033,4 +1035,16 @@ contract GatewayTest is Test { IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); } + + function testLegacyAgentExecutionForCompatibility() public { + token.transfer(address(assetHubAgent), 200); + + AgentExecuteParams memory params = AgentExecuteParams({ + agentID: assetHubAgentID, + payload: abi.encode(AgentExecuteCommand.TransferToken, abi.encode(address(token), address(account2), 10)) + }); + + bytes memory encodedParams = abi.encode(params); + GatewayMock(address(gateway)).agentExecutePublic(encodedParams); + } } diff --git a/contracts/test/mocks/GatewayMock.sol b/contracts/test/mocks/GatewayMock.sol index 3a86626464..29e04cfb48 100644 --- a/contracts/test/mocks/GatewayMock.sol +++ b/contracts/test/mocks/GatewayMock.sol @@ -19,6 +19,10 @@ contract GatewayMock is Gateway { uint8 foreignTokenDecimals ) Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubHubAgentID, foreignTokenDecimals) {} + function agentExecutePublic(bytes calldata params) external { + this.agentExecute(params); + } + function createAgentPublic(bytes calldata params) external { this.createAgent(params); } From dc7827637eb1ac2a0f5c57d0ad19a918cd3b67ec Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 11 Apr 2024 21:08:24 +0800 Subject: [PATCH 42/55] Cleanup --- contracts/src/Types.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index ee35dd026a..8e81888cfc 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -91,8 +91,7 @@ enum Command { } enum AgentExecuteCommand { - TransferToken, - MintToken + TransferToken } /// @dev Application-level costs for a message From 03cf01cacee909b14b19dc753ecf89696c466037 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 16 Apr 2024 21:55:49 +0800 Subject: [PATCH 43/55] Add rfc --- rfc/polkadot-native-assets.md | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 rfc/polkadot-native-assets.md diff --git a/rfc/polkadot-native-assets.md b/rfc/polkadot-native-assets.md new file mode 100644 index 0000000000..1c08826a52 --- /dev/null +++ b/rfc/polkadot-native-assets.md @@ -0,0 +1,115 @@ +# RFC: Introduce Polkadot-native assets to Ethereum + + +## Summary + +This RFC proposes the feature to introduce Polkadot-native assets to Ethereum through our bridge, including two PRs separately with https://github.com/Snowfork/snowbridge/pull/1155 for solidity and https://github.com/Snowfork/polkadot-sdk/pull/128 for substrate. + +## Motivation + +Currently only native ERC20 from Ethereum can be bridged to substrate, we do want to introduce Polkadot-native assets to Ethereum. + +## Explanation + +The basic work flow includes steps as following: + +### 1. Register Polkadot-native assets as ERC20 + +By adding a [dispatchable](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/bridges/snowbridge/pallets/system/src/lib.rs#L604) to `EthereumControl` pallet to register new Polkadot-native assets called via XCM, this dispatchable will send a message over the bridge to the Agent of the Parachain. + +On Ethereum side the agent will [instantiate a new ERC20 token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L259) representing the Polkadot-native asset. + +There is a E2E test [register_relay_token](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L578) for demonstration. + +### 2. Send Polkadot-native assets via [reserve_transfer_assets](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/polkadot/xcm/pallet-xcm/src/lib.rs#L1027) + +First it requires the source parachain to extend the `XcmRouter` to route xcm with destination to Ethereum through our bridge on BH, [config](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs#L681) on AssetHub for the reference. + +Also worth to note that the [fee config in BridgeTable](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs#L886-L889) should cover the total execution cost on BridgeHub and Ethereum. + +There is a E2E test [send_relay_token_from_substrate_to_ethereum](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L645) for demonstration. + +Check the xcm executed on AssetHub from the log as following it shows that the relay token has been reserved to the sovereign account of Ethereum. + +``` +2024-04-16T12:22:11.680296Z TRACE xcm::process_instruction: === TransferAsset { assets: Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(100000000000) }]), beneficiary: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } } +2024-04-16T12:22:11.680315Z TRACE xcm::fungible_adapter: internal_transfer_asset what: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(100000000000) }, from: Location { parents: 0, interior: X1([AccountId32 { network: Some(Rococo), id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) }, to: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-16T12:22:11.680342Z TRACE xcm::location_conversion: GlobalConsensusParachainConvertsFor universal_source: X2([GlobalConsensus(Rococo), Parachain(1000)]), location: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-16T12:22:11.680549Z TRACE xcm::execute: result: Ok(()) +``` + + +The xcm forwarded to BridgeHub as following: +``` +instructions: [ + WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4200000000000) }])), + BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4200000000000) }, weight_limit: Unlimited }, + SetAppendix(Xcm([DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 1, interior: X1([Parachain(1000)]) } }])), + ExportMessage { + network: Ethereum { chain_id: 11155111 }, + destination: Here, + xcm: Xcm([ + ReserveAssetDeposited(Assets([Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }])), + ClearOrigin, + BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, weight_limit: Unlimited }, + DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 0, interior: X1([AccountKey20 { network: None, key: [68, 165, 126, 226, 242, 252, 203, 133, 253, 162, 176, 177, 142, 189, 13, 141, 35, 51, 112, 14] }]) } }, + SetTopic([156, 140, 10, 35, 194, 14, 239, 123, 235, 56, 179, 99, 185, 189, 107, 206, 228, 222, 106, 10, 227, 75, 47, 41, 171, 186, 195, 157, 172, 237, 251, 50]) + ]) + } +] +``` + +So the top-level `WithdrawAsset` will withdraw relay token from sovereign account of AssetHub as fee to pay for the execution cost on both BridgeHub and Ethereum. + +What we really care about is the internal xcm in `ExportMessage` with [the convert logic in outbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/outbound/mod.rs#L222) it will be converted into a simple `Command` which will be relayed and finally executed on Ethereum. + +On Ethereum side based on the `Command` the Agent will [mint foreign token to the recipient](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L269) to finish the whole flow. + + +### 3. Send Polkadot-native assets back from Ethereum to Substrate via [sendToken](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Gateway.sol#L463) + +So first on Ethereum the Agent will [burn foreign token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L235) and the [payload](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L220) will be relayed and finally executed on BridgeHub. + +Then on BridgeHub with [the convert logic in inbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L354) it will be converted into a [xcm](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L386-L395) which will be sent to the destination chain. + +There is a E2E test [send_relay_token_from_ethereum_to_substrate](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L725) with the xcm forwarded to AssetHub as following: + + +``` + instructions: [ + ReceiveTeleportedAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4000000000) }])), + BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4000000000) }, weight_limit: Unlimited }, + DescendOrigin(X1([PalletInstance(80)])), + UniversalOrigin(GlobalConsensus(Ethereum { chain_id: 11155111 })), + WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }])), + ClearOrigin, + DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } +] +``` + +Check the xcm executed on AssetHub and from the log as following it shows that the relay token has been withdraw from the sovereign account of Ethereum and then deposit to the beneficiary as expected. + +``` +2024-04-16T13:22:57.534652Z TRACE xcm::process_instruction: === WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }])) +2024-04-16T13:22:57.534659Z TRACE xcm::ensure_can_subsume_assets: worst_case_holding_len: 2, holding_limit: 64 +2024-04-16T13:22:57.534668Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-16T13:22:57.534675Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-16T13:22:57.534692Z TRACE xcm::location_conversion: GlobalConsensusParachainConvertsFor universal_source: X2([GlobalConsensus(Rococo), Parachain(1000)]), location: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-16T13:22:57.534823Z TRACE xcm::process_instruction: === ClearOrigin +2024-04-16T13:22:57.534831Z TRACE xcm::process_instruction: === DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } +2024-04-16T13:22:57.534858Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(3959106667) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } +2024-04-16T13:22:57.534914Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } +2024-04-16T13:22:57.534924Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } +2024-04-16T13:22:57.534985Z TRACE xcm::process_instruction: === SetTopic([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) +2024-04-16T13:22:57.534995Z TRACE xcm::execute: result: Ok(()) +``` + +## Testing, Security, and Privacy + +In https://github.com/Snowfork/snowbridge/pull/1155 we add smoke tests to to cover the 3 basic flows above. + +``` +cargo test --test register_polkadot_token // register relay token(i.e. ROC|KSM|DOT) on Ethereum +cargo test --test transfer_polkadot_token // transfer relay token from AssetHub to Ethereum +cargo test --test send_polkadot_token // send relay token from Ethereum back to AssetHub +``` From e9013cd0637051dd26afa969008a0a3f64763d62 Mon Sep 17 00:00:00 2001 From: Ron Date: Tue, 16 Apr 2024 23:32:33 +0800 Subject: [PATCH 44/55] Update polkadot-native-assets.md --- rfc/polkadot-native-assets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/polkadot-native-assets.md b/rfc/polkadot-native-assets.md index 1c08826a52..2ed411265d 100644 --- a/rfc/polkadot-native-assets.md +++ b/rfc/polkadot-native-assets.md @@ -61,7 +61,7 @@ instructions: [ So the top-level `WithdrawAsset` will withdraw relay token from sovereign account of AssetHub as fee to pay for the execution cost on both BridgeHub and Ethereum. -What we really care about is the internal xcm in `ExportMessage` with [the convert logic in outbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/outbound/mod.rs#L222) it will be converted into a simple `Command` which will be relayed and finally executed on Ethereum. +What we really care about is the internal xcm in `ExportMessage` with [the convert logic in outbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/outbound/mod.rs#L318) it will be converted into a simple `Command` which will be relayed and finally executed on Ethereum. On Ethereum side based on the `Command` the Agent will [mint foreign token to the recipient](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L269) to finish the whole flow. From 3b7e178b1b60445011188fdb598d50e9e931e3de Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 22 Apr 2024 13:52:58 +0800 Subject: [PATCH 45/55] Update rfc --- rfc/polkadot-native-assets.md | 99 +++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/rfc/polkadot-native-assets.md b/rfc/polkadot-native-assets.md index 2ed411265d..821c4b2e3a 100644 --- a/rfc/polkadot-native-assets.md +++ b/rfc/polkadot-native-assets.md @@ -5,37 +5,33 @@ This RFC proposes the feature to introduce Polkadot-native assets to Ethereum through our bridge, including two PRs separately with https://github.com/Snowfork/snowbridge/pull/1155 for solidity and https://github.com/Snowfork/polkadot-sdk/pull/128 for substrate. -## Motivation - -Currently only native ERC20 from Ethereum can be bridged to substrate, we do want to introduce Polkadot-native assets to Ethereum. ## Explanation -The basic work flow includes steps as following: +We use native token on Penpal for the integration and the basic work flow includes steps as following: ### 1. Register Polkadot-native assets as ERC20 -By adding a [dispatchable](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/bridges/snowbridge/pallets/system/src/lib.rs#L604) to `EthereumControl` pallet to register new Polkadot-native assets called via XCM, this dispatchable will send a message over the bridge to the Agent of the Parachain. +First by adding a [dispatchable](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/bridges/snowbridge/pallets/system/src/lib.rs#L604) to `EthereumControl` pallet to register new Polkadot-native assets called via XCM, this dispatchable will send a message over the bridge to the agent of the Parachain. -On Ethereum side the agent will [instantiate a new ERC20 token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L259) representing the Polkadot-native asset. +On Ethereum the agent will [instantiate a new ERC20 token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L259) representing the Polkadot-native asset. -There is a E2E test [register_relay_token](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L578) for demonstration. +There is a E2E test [register_penpal_native_token](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L571) for demonstration. ### 2. Send Polkadot-native assets via [reserve_transfer_assets](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/polkadot/xcm/pallet-xcm/src/lib.rs#L1027) -First it requires the source parachain to extend the `XcmRouter` to route xcm with destination to Ethereum through our bridge on BH, [config](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs#L681) on AssetHub for the reference. +First it requires the source parachain to extend the `XcmRouter` to route xcm with destination to Ethereum through our bridge on BH, [config](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs#L401) on Penpal for the reference. -Also worth to note that the [fee config in BridgeTable](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs#L886-L889) should cover the total execution cost on BridgeHub and Ethereum. +Worth to note that the [fee config in BridgeTable](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs#L465-L468) should cover the total execution cost on BridgeHub and Ethereum in DOT. -There is a E2E test [send_relay_token_from_substrate_to_ethereum](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L645) for demonstration. +There is a E2E test [send_penpal_native_token_to_ethereum](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L655) for demonstration. -Check the xcm executed on AssetHub from the log as following it shows that the relay token has been reserved to the sovereign account of Ethereum. +Check the xcm executed on penpal it shows that the native token has been reserved to the sovereign account of Ethereum. ``` -2024-04-16T12:22:11.680296Z TRACE xcm::process_instruction: === TransferAsset { assets: Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(100000000000) }]), beneficiary: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } } -2024-04-16T12:22:11.680315Z TRACE xcm::fungible_adapter: internal_transfer_asset what: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(100000000000) }, from: Location { parents: 0, interior: X1([AccountId32 { network: Some(Rococo), id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) }, to: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-16T12:22:11.680342Z TRACE xcm::location_conversion: GlobalConsensusParachainConvertsFor universal_source: X2([GlobalConsensus(Rococo), Parachain(1000)]), location: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-16T12:22:11.680549Z TRACE xcm::execute: result: Ok(()) +2024-04-22T05:08:45.117066Z TRACE xcm::process_instruction: === TransferAsset { assets: Assets([Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(100000000000) }]), beneficiary: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } } +2024-04-22T05:08:45.117087Z TRACE xcm::fungible_adapter: internal_transfer_asset what: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(100000000000) }, from: Location { parents: 0, interior: X1([AccountId32 { network: Some(Rococo), id: [212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125] }]) }, to: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-22T05:08:45.117476Z TRACE xcm::execute: result: Ok(()) ``` @@ -44,14 +40,14 @@ The xcm forwarded to BridgeHub as following: instructions: [ WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4200000000000) }])), BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4200000000000) }, weight_limit: Unlimited }, - SetAppendix(Xcm([DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 1, interior: X1([Parachain(1000)]) } }])), + SetAppendix(Xcm([DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 1, interior: X1([Parachain(2000)]) } }])), ExportMessage { network: Ethereum { chain_id: 11155111 }, destination: Here, xcm: Xcm([ - ReserveAssetDeposited(Assets([Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }])), + ReserveAssetDeposited(Assets([Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }])), ClearOrigin, - BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, weight_limit: Unlimited }, + BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, weight_limit: Unlimited }, DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 0, interior: X1([AccountKey20 { network: None, key: [68, 165, 126, 226, 242, 252, 203, 133, 253, 162, 176, 177, 142, 189, 13, 141, 35, 51, 112, 14] }]) } }, SetTopic([156, 140, 10, 35, 194, 14, 239, 123, 235, 56, 179, 99, 185, 189, 107, 206, 228, 222, 106, 10, 227, 75, 47, 41, 171, 186, 195, 157, 172, 237, 251, 50]) ]) @@ -59,57 +55,68 @@ instructions: [ ] ``` -So the top-level `WithdrawAsset` will withdraw relay token from sovereign account of AssetHub as fee to pay for the execution cost on both BridgeHub and Ethereum. +So the top-level `WithdrawAsset` will withdraw relay token from sovereign account of Penpal as fee to pay for the execution cost on both BridgeHub and Ethereum. What we really care about is the internal xcm in `ExportMessage` with [the convert logic in outbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/outbound/mod.rs#L318) it will be converted into a simple `Command` which will be relayed and finally executed on Ethereum. On Ethereum side based on the `Command` the Agent will [mint foreign token to the recipient](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L269) to finish the whole flow. +#### Fee flow + +| Penpal | Bridgehub | Ethereum +|----------|:-------------:|------: +|`reserve_transfer_assets` will charge `EthereumBaseFee` from end user in DOT to cover both the execution cost on BH and Ethereum. |`BuyExecution` with fee paid by the sovereign account of penpal in DOT | Agent refund the relayer in Ether + ### 3. Send Polkadot-native assets back from Ethereum to Substrate via [sendToken](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Gateway.sol#L463) So first on Ethereum the Agent will [burn foreign token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L235) and the [payload](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L220) will be relayed and finally executed on BridgeHub. -Then on BridgeHub with [the convert logic in inbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L354) it will be converted into a [xcm](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L386-L395) which will be sent to the destination chain. +Then on BridgeHub with [the convert logic in inbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L354) it will be converted into a [xcm](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L392-L399) which will be sent to the destination chain. -There is a E2E test [send_relay_token_from_ethereum_to_substrate](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L725) with the xcm forwarded to AssetHub as following: +There is a E2E test [send_penpal_native_token_from_ethereum](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L759) with the xcm forwarded to Penpal as following: ``` instructions: [ - ReceiveTeleportedAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4000000000) }])), - BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4000000000) }, weight_limit: Unlimited }, - DescendOrigin(X1([PalletInstance(80)])), - UniversalOrigin(GlobalConsensus(Ethereum { chain_id: 11155111 })), - WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }])), + DescendOrigin(X1([PalletInstance(80)])), + UniversalOrigin(GlobalConsensus(Ethereum { chain_id: 11155111 })), + WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }])), + BuyExecution { fees: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, weight_limit: Unlimited }, ClearOrigin, DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } ] ``` -Check the xcm executed on AssetHub and from the log as following it shows that the relay token has been withdraw from the sovereign account of Ethereum and then deposit to the beneficiary as expected. +Check the xcm executed on Penpal it shows that the native token has been withdraw from the sovereign account of Ethereum and then deposit to the beneficiary as expected. ``` -2024-04-16T13:22:57.534652Z TRACE xcm::process_instruction: === WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }])) -2024-04-16T13:22:57.534659Z TRACE xcm::ensure_can_subsume_assets: worst_case_holding_len: 2, holding_limit: 64 -2024-04-16T13:22:57.534668Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-16T13:22:57.534675Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-16T13:22:57.534692Z TRACE xcm::location_conversion: GlobalConsensusParachainConvertsFor universal_source: X2([GlobalConsensus(Rococo), Parachain(1000)]), location: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-16T13:22:57.534823Z TRACE xcm::process_instruction: === ClearOrigin -2024-04-16T13:22:57.534831Z TRACE xcm::process_instruction: === DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } -2024-04-16T13:22:57.534858Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(3959106667) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } -2024-04-16T13:22:57.534914Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } -2024-04-16T13:22:57.534924Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X1([GlobalConsensus(Rococo)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } -2024-04-16T13:22:57.534985Z TRACE xcm::process_instruction: === SetTopic([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) -2024-04-16T13:22:57.534995Z TRACE xcm::execute: result: Ok(()) +2024-04-22T05:23:54.708694Z TRACE xcm::process: origin: Some(Location { parents: 1, interior: X1([Parachain(1013)]) }), total_surplus/refunded: Weight { ref_time: 0, proof_size: 0 }/Weight { ref_time: 0, proof_size: 0 }, error_handler_weight: Weight { ref_time: 0, proof_size: 0 } +2024-04-22T05:23:54.708707Z TRACE xcm::process_instruction: === DescendOrigin(X1([PalletInstance(80)])) +2024-04-22T05:23:54.708718Z TRACE xcm::process_instruction: === UniversalOrigin(GlobalConsensus(Ethereum { chain_id: 11155111 })) +2024-04-22T05:23:54.708804Z TRACE xcm::process_instruction: === WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }])) +2024-04-22T05:23:54.708815Z TRACE xcm::ensure_can_subsume_assets: worst_case_holding_len: 2, holding_limit: 64 +2024-04-22T05:23:54.708845Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-22T05:23:54.708956Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-22T05:23:54.708966Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } +2024-04-22T05:23:54.709091Z TRACE xcm::process_instruction: === BuyExecution { fees: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, weight_limit: Limited(Weight { ref_time: 7000000000, proof_size: 458752 }) } +2024-04-22T05:23:54.709110Z TRACE xcm::weight: UsingComponents::buy_weight weight: Weight { ref_time: 7000000000, proof_size: 458752 }, payment: AssetsInHolding { fungible: {AssetId(Location { parents: 0, interior: Here }): 8000000000}, non_fungible: {} }, context: XcmContext { origin: Some(Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) }), message_id: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], topic: None } +2024-04-22T05:23:54.709147Z TRACE xcm::weight: UsingComponents::buy_weight weight: Weight { ref_time: 7000000000, proof_size: 458752 }, payment: AssetsInHolding { fungible: {AssetId(Location { parents: 0, interior: Here }): 8000000000}, non_fungible: {} }, context: XcmContext { origin: Some(Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) }), message_id: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], topic: None } +2024-04-22T05:23:54.709166Z TRACE xcm::process_instruction: === ClearOrigin +2024-04-22T05:23:54.709174Z TRACE xcm::process_instruction: === DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } +2024-04-22T05:23:54.709191Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(3412480000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } +2024-04-22T05:23:54.709350Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } +2024-04-22T05:23:54.709362Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } +2024-04-22T05:23:54.709459Z TRACE xcm::process_instruction: === SetTopic([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) +2024-04-22T05:23:54.709470Z TRACE xcm::execute: result: Ok(()) +2024-04-22T05:23:54.709477Z TRACE xcm::refund_surplus: total_surplus: Weight { ref_time: 0, proof_size: 0 }, total_refunded: Weight { ref_time: 0, proof_size: 0 }, current_surplus: Weight { ref_time: 0, proof_size: 0 } +2024-04-22T05:23:54.709484Z TRACE xcm::refund_surplus: total_refunded: Weight { ref_time: 0, proof_size: 0 } +2024-04-22T05:23:54.709546Z TRACE xcm::process-message: XCM message execution complete, used weight: Weight(ref_time: 7000000000, proof_size: 458752) ``` -## Testing, Security, and Privacy +#### Fee Flow -In https://github.com/Snowfork/snowbridge/pull/1155 we add smoke tests to to cover the 3 basic flows above. -``` -cargo test --test register_polkadot_token // register relay token(i.e. ROC|KSM|DOT) on Ethereum -cargo test --test transfer_polkadot_token // transfer relay token from AssetHub to Ethereum -cargo test --test send_polkadot_token // send relay token from Ethereum back to AssetHub -``` +| Ethereum | Bridgehub | Penpal +|----------|:-------------:|------: +|Charge from end user to the agent of penpal with fee in Ether | Refund the relayer from sovereign of penpal in DOT | `BuyExecution` with fee paid by a pre-funded sovereign account of Ethereum with `destination_fee` in native token From 63e8322c147e1c3e0cca4d5d2244eebcbeb5b809 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 22 Apr 2024 22:48:44 +0800 Subject: [PATCH 46/55] Update rfc with fee section --- rfc/polkadot-native-assets.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/rfc/polkadot-native-assets.md b/rfc/polkadot-native-assets.md index 821c4b2e3a..3177733bd1 100644 --- a/rfc/polkadot-native-assets.md +++ b/rfc/polkadot-native-assets.md @@ -63,9 +63,15 @@ On Ethereum side based on the `Command` the Agent will [mint foreign token to th #### Fee flow -| Penpal | Bridgehub | Ethereum -|----------|:-------------:|------: -|`reserve_transfer_assets` will charge `EthereumBaseFee` from end user in DOT to cover both the execution cost on BH and Ethereum. |`BuyExecution` with fee paid by the sovereign account of penpal in DOT | Agent refund the relayer in Ether +- User represents a user who kicks off an extrinsic on the parachain. +- Parachain represents the source parachain, its sovereign or its agent depending on context. + +Sequence|Where|Who|What +-|-|-|- +1|Penpal|User| For `reserve_transfer_assets` pays(DOT, Native) to node to execute custom extrinsic; pays (DOT) to Treasury for both delivery cost on BH and execution cost on Ethereum(i.e. `EthereumBaseFee`). +2|Bridge Hub|Parachain|Pays(DOT) to Treasury Account for delivery(local fee), pays(DOT) to Parachain sovereign for delivery(remote fee), essentially a refund. Remote fee converted to ETH here. +3|Gateway|Relayer|pays(ETH) to validate and execute message. +4|Gateway|Parachain Agent|pays(ETH) to relayer for delivery(reward+refund) and execution. ### 3. Send Polkadot-native assets back from Ethereum to Substrate via [sendToken](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Gateway.sol#L463) @@ -116,7 +122,13 @@ Check the xcm executed on Penpal it shows that the native token has been withdra #### Fee Flow +- dApp is represents `msg.sender` or its sovereign depending on context. +- Parachain represents the target parachain, its sovereign or its agent depending on context. +- Ethereum Sovereign represents `Location{parent:2,interior:[GlobalConsensus(Ethereum)]}` -| Ethereum | Bridgehub | Penpal -|----------|:-------------:|------: -|Charge from end user to the agent of penpal with fee in Ether | Refund the relayer from sovereign of penpal in DOT | `BuyExecution` with fee paid by a pre-funded sovereign account of Ethereum with `destination_fee` in native token +Sequence|Where|Who|What +-|-|-|- +1|Gateway|dApp|pays(ETH, converted to DOT here) Parachain Agent for both delivery cost on BH and execution cost on destination(DOT,Native). +2|Bridge Hub|Relayer|pays(DOT) node for execution +3|Bridge Hub|Parachain Sovereign|pays(DOT) Relayer for delivery (refund+reward) +4|Parachain|Ethereum Sovereign|pays(DOT, Native) for execution only. From c887e7e06fc8bd12dfcfa31ff441142151356b50 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 3 May 2024 22:10:57 +0800 Subject: [PATCH 47/55] Fix _calculateFee for polkadot native token --- contracts/src/Gateway.sol | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index f4b3884e44..609332f1d9 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -557,7 +557,17 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // Ensure outbound messaging is allowed _ensureOutboundMessagingEnabled(channel); - uint256 fee = _calculateFee(ticket.costs); + uint256 fee; + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if (ticket.dest == $.assetHubParaID) { + fee = _calculateFee(ticket.costs); + } else { + // The assumption here is that no matter what the fee token is, usually the xcm execution cost on substrate chain is very tiny + // as demonstrated in https://www.notion.so/snowfork/Gateway-Parameters-0cf913d089374027a86721883306ee61 + // the DELIVERY_COST on BH and TRANSFER_TOKEN_FEE are all no more than 0.1 DOT so the total cost as 0.2 DOT equals 0.00048 ETH. + // Consider a 5x buffer a hard code 0.002 ETH should be pretty enough to cover both costs + fee = 2000000000000000; + } // Ensure the user has enough funds for this message to be accepted if (msg.value < fee) { From 84bb52c54b9cfa2d05b529f856a330588cad3085 Mon Sep 17 00:00:00 2001 From: Vincent Geddes <117534+vgeddes@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:24:37 +0200 Subject: [PATCH 48/55] Improve solidity code for PNA (#1275) * Improve PNA implementation * More improvents to ERC20 token * Fix implementation of Assets._sendForeignToken * Fix build payload for foreign token --------- Co-authored-by: ron --- contracts/src/AgentExecutor.sol | 27 +-- contracts/src/Assets.sol | 232 ++++++++++++------- contracts/src/Gateway.sol | 80 ++----- contracts/src/Params.sol | 12 +- contracts/src/SubstrateTypes.sol | 29 ++- contracts/src/{ERC20.sol => Token.sol} | 69 +++--- contracts/src/{ERC20Lib.sol => TokenLib.sol} | 157 +++++-------- contracts/src/Types.sol | 8 +- contracts/src/interfaces/IERC20.sol | 2 + contracts/src/interfaces/IERC20Permit.sol | 5 + contracts/src/interfaces/IGateway.sol | 2 +- contracts/src/storage/AssetsStorage.sol | 3 +- contracts/test/Gateway.t.sol | 89 +++---- contracts/test/mocks/MockGateway.sol | 4 +- 14 files changed, 341 insertions(+), 378 deletions(-) rename contracts/src/{ERC20.sol => Token.sol} (79%) rename contracts/src/{ERC20Lib.sol => TokenLib.sol} (58%) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 8b5f56fedf..c3ed7e2f81 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -7,7 +7,6 @@ import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; -import {ERC20} from "./ERC20.sol"; import {Gateway} from "./Gateway.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. @@ -16,17 +15,6 @@ contract AgentExecutor { using SafeTokenTransfer for IERC20; using SafeNativeTransfer for address payable; - /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, - /// the `data` parameter is constructed by the BridgeHub parachain. - /// - function execute(bytes memory data) external { - (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); - if (command == AgentExecuteCommand.TransferToken) { - (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); - _transferToken(token, recipient, amount); - } - } - /// @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. /// @@ -34,22 +22,13 @@ contract AgentExecutor { recipient.safeNativeTransfer(amount); } - /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. - function _transferToken(address token, address recipient, uint128 amount) internal { - IERC20(token).safeTransfer(recipient, amount); - } - /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. function transferToken(address token, address recipient, uint128 amount) external { _transferToken(token, recipient, amount); } - /// @dev Mint ERC20 token to `recipient`. - function mintToken(address token, address recipient, uint256 amount) external { - ERC20(token).mint(recipient, amount); - } - - function burnToken(address token, address sender, uint256 amount) external { - ERC20(token).burn(sender, 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 1183f72986..c88ead8846 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -16,7 +16,7 @@ import {Address} from "./utils/Address.sol"; import {AgentExecutor} from "./AgentExecutor.sol"; import {Agent} from "./Agent.sol"; import {Call} from "./utils/Call.sol"; -import {ERC20} from "./ERC20.sol"; +import {Token} from "./Token.sol"; /// @title Library for implementing Ethereum->Polkadot ERC20 transfers. library Assets { @@ -108,6 +108,41 @@ library Assets { ) external returns (Ticket memory ticket) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); + TokenInfo storage info = $.tokenRegistry[token]; + + if (!info.isRegistered) { + revert TokenNotRegistered(); + } + + if (info.foreignID == bytes32(0)) { + return _sendNativeToken( + token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount + ); + } else { + return _sendForeignToken( + info.foreignID, + token, + sender, + destinationChain, + destinationAddress, + destinationChainFee, + maxDestinationChainFee, + amount + ); + } + } + + 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 funds into AssetHub's agent contract _transferToAgent($.assetHubAgent, token, sender, amount); @@ -159,6 +194,98 @@ library Assets { emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); } + function _sendForeignTokenCosts( + ParaID destinationChain, + uint128 destinationChainFee, + uint128 maxDestinationChainFee + ) internal view returns (Costs memory costs) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + if ($.assetHubParaID == destinationChain) { + costs.foreign = $.assetHubReserveTransferFee; + } else { + // Reduce the ability for users to perform arbitrage by exploiting a + // favourable exchange rate. For example supplying Ether + // and gaining a more valuable amount of DOT on the destination chain. + // + // Also prevents users from mistakenly sending more fees than would be required + // which has negative effects like draining AssetHub's sovereign account. + // + // For safety, `maxDestinationChainFee` should be less valuable + // than the gas cost to send tokens. + if (destinationChainFee > maxDestinationChainFee) { + revert InvalidDestinationFee(); + } + + // If the final destination chain is not AssetHub, then the fee needs to additionally + // include the cost of executing an XCM on the final destination parachain. + costs.foreign = $.assetHubReserveTransferFee + destinationChainFee; + } + // We don't charge any extra fees beyond delivery costs + costs.native = 0; + } + + // @dev Transfer Polkadot-native tokens back to Polkadot + function _sendForeignToken( + bytes32 foreignID, + 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(); + + Token(token).burn(sender, amount); + + ticket.dest = $.assetHubParaID; + ticket.costs = _sendForeignTokenCosts(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.SendForeignTokenToAssetHubAddress32( + foreignID, destinationAddress.asAddress32(), $.assetHubReserveTransferFee, amount + ); + } else { + // AssetHub does not support 20-byte account IDs + revert Unsupported(); + } + } else { + if (destinationChainFee == 0) { + revert InvalidDestinationFee(); + } + if (destinationAddress.isAddress32()) { + // The receiver has a 32-byte account ID + ticket.payload = SubstrateTypes.SendForeignTokenToAddress32( + foreignID, + destinationChain, + destinationAddress.asAddress32(), + $.assetHubReserveTransferFee, + destinationChainFee, + amount + ); + } else if (destinationAddress.isAddress20()) { + // The receiver has a 20-byte account ID + ticket.payload = SubstrateTypes.SendForeignTokenToAddress20( + foreignID, + destinationChain, + destinationAddress.asAddress20(), + $.assetHubReserveTransferFee, + destinationChainFee, + amount + ); + } else { + revert Unsupported(); + } + } + + emit IGateway.TokenSent(token, sender, destinationChain, destinationAddress, amount); + } + function registerTokenCosts() external view returns (Costs memory costs) { return _registerTokenCosts(); } @@ -195,90 +322,31 @@ library Assets { emit IGateway.TokenRegistrationSent(token); } - // @dev Transfer polkadot native tokens back - function sendForeignToken( - address agent, - address executor, - TokenInfo storage info, - address sender, - ParaID destinationChain, - MultiAddress calldata destinationAddress, - uint128 destinationChainFee, - uint128 amount - ) external returns (Ticket memory ticket) { - if (destinationChainFee == 0) { - revert InvalidDestinationFee(); - } - // Polkadot-native token: burn wrapped token - _burnToken(executor, agent, info.token, sender, amount); - - ticket.dest = destinationChain; - ticket.costs = _sendForeignTokenCosts(destinationChainFee); - - if (destinationAddress.isAddress32()) { - // The receiver has a 32-byte account ID - ticket.payload = SubstrateTypes.SendForeignTokenToAddress32( - info.tokenID, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount - ); - } else if (destinationAddress.isAddress20()) { - // The receiver has a 20-byte account ID - ticket.payload = SubstrateTypes.SendForeignTokenToAddress20( - info.tokenID, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount - ); - } else { - revert Unsupported(); - } - - emit IGateway.TokenSent(info.token, sender, destinationChain, destinationAddress, amount); - } - - function _burnToken(address agentExecutor, address agent, address token, address sender, uint256 amount) internal { - bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (token, sender, amount)); - (bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(agentExecutor, call)); - Call.verifyResult(success, returndata); - } - - function _sendForeignTokenCosts(uint128 destinationChainFee) internal pure returns (Costs memory costs) { - costs.foreign = destinationChainFee; - costs.native = 0; - } - // @dev Register a new fungible Polkadot token for an agent - function registerForeignToken( - bytes32 agentID, - address agent, - bytes32 tokenID, - string memory name, - string memory symbol, - uint8 decimals - ) external { + function registerForeignToken(bytes32 foreignTokenID, string memory name, string memory symbol, uint8 decimals) + external + { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == true) { + if ($.tokenAddressOf[foreignTokenID] != address(0)) { revert TokenAlreadyRegistered(); } - ERC20 foreignToken = new ERC20(agent, name, symbol, decimals); - address token = address(foreignToken); - TokenInfo memory info = - TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token}); - $.tokenRegistry[token] = info; - $.tokenRegistryByID[tokenID] = info; - emit IGateway.ForeignTokenRegistered(tokenID, agentID, token); + Token token = new Token(name, symbol, decimals); + TokenInfo memory info = TokenInfo({isRegistered: true, foreignID: foreignTokenID}); + + $.tokenAddressOf[foreignTokenID] = address(token); + $.tokenRegistry[address(token)] = info; + + emit IGateway.ForeignTokenRegistered(foreignTokenID, address(token)); } // @dev Mint foreign token from Polkadot - function mintForeignToken(address executor, address agent, bytes32 tokenID, address recipient, uint256 amount) - external - { - address token = _tokenAddressOf(tokenID); - bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (token, recipient, amount)); - (bool success,) = Agent(payable(agent)).invoke(executor, call); - if (!success) { - revert TokenMintFailed(); - } + function mintForeignToken(bytes32 foreignTokenID, address recipient, uint256 amount) external { + address token = _ensureTokenAddressOf(foreignTokenID); + Token(token).mint(recipient, amount); } // @dev Transfer ERC20 to `recipient` - function transferToken(address executor, address agent, address token, address recipient, uint128 amount) + function transferNativeToken(address executor, address agent, address token, address recipient, uint128 amount) external { bytes memory call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount)); @@ -290,15 +358,21 @@ library Assets { // @dev Get token address by tokenID function tokenAddressOf(bytes32 tokenID) external view returns (address) { - return _tokenAddressOf(tokenID); + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + return $.tokenAddressOf[tokenID]; } // @dev Get token address by tokenID - function _tokenAddressOf(bytes32 tokenID) internal view returns (address) { + function _ensureTokenAddressOf(bytes32 tokenID) internal view returns (address) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if ($.tokenRegistryByID[tokenID].isRegistered == false) { + if ($.tokenAddressOf[tokenID] == address(0)) { revert TokenNotRegistered(); } - return $.tokenRegistryByID[tokenID].token; + return $.tokenAddressOf[tokenID]; + } + + function _isTokenRegistered(address token) internal view returns (bool) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + return $.tokenRegistry[token].isRegistered; } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 7a6d44a9dd..d4ac703db6 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -44,7 +44,7 @@ import { SetPricingParametersParams, RegisterForeignTokenParams, MintForeignTokenParams, - TransferTokenParams + TransferNativeTokenParams } from "./Params.sol"; import {CoreStorage} from "./storage/CoreStorage.sol"; @@ -228,8 +228,8 @@ contract Gateway is IGateway, IInitializable, IUpgradable { catch { success = false; } - } else if (message.command == Command.TransferToken) { - try Gateway(this).transferToken{gas: maxDispatchGas}(message.params) {} + } else if (message.command == Command.TransferNativeToken) { + try Gateway(this).transferNativeToken{gas: maxDispatchGas}(message.params) {} catch { success = false; } @@ -306,11 +306,11 @@ contract Gateway is IGateway, IInitializable, IUpgradable { revert InvalidAgentExecutionPayload(); } - bytes memory call = abi.encodeCall(AgentExecutor.execute, params.payload); - - (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); - if (!success) { - revert AgentExecutionFailed(returndata); + (AgentExecuteCommand command, bytes memory commandParams) = + abi.decode(params.payload, (AgentExecuteCommand, bytes)); + if (command == AgentExecuteCommand.TransferToken) { + (address token, address recipient, uint128 amount) = abi.decode(commandParams, (address, address, uint128)); + Assets.transferNativeToken(AGENT_EXECUTOR, agent, token, recipient, amount); } } @@ -419,22 +419,20 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // @dev Register a new fungible Polkadot token for an agent function registerForeignToken(bytes calldata data) external onlySelf { RegisterForeignTokenParams memory params = abi.decode(data, (RegisterForeignTokenParams)); - address agent = _ensureAgent(params.agentID); - Assets.registerForeignToken(params.agentID, agent, params.tokenID, params.name, params.symbol, params.decimals); + Assets.registerForeignToken(params.foreignTokenID, params.name, params.symbol, params.decimals); } // @dev Mint foreign token from polkadot function mintForeignToken(bytes calldata data) external onlySelf { MintForeignTokenParams memory params = abi.decode(data, (MintForeignTokenParams)); - address agent = _ensureAgent(params.agentID); - Assets.mintForeignToken(AGENT_EXECUTOR, agent, params.tokenID, params.recipient, params.amount); + Assets.mintForeignToken(params.foreignTokenID, params.recipient, params.amount); } // @dev Transfer Ethereum native token back from polkadot - function transferToken(bytes calldata data) external onlySelf { - TransferTokenParams memory params = abi.decode(data, (TransferTokenParams)); + function transferNativeToken(bytes calldata data) external onlySelf { + TransferNativeTokenParams memory params = abi.decode(data, (TransferNativeTokenParams)); address agent = _ensureAgent(params.agentID); - Assets.transferToken(AGENT_EXECUTOR, agent, params.token, params.recipient, params.amount); + Assets.transferNativeToken(AGENT_EXECUTOR, agent, params.token, params.recipient, params.amount); } function isTokenRegistered(address token) external view returns (bool) { @@ -468,33 +466,11 @@ contract Gateway is IGateway, IInitializable, IUpgradable { uint128 destinationFee, uint128 amount ) external payable { - AssetsStorage.Layout storage $ = AssetsStorage.layout(); + Ticket memory ticket = Assets.sendToken( + token, msg.sender, destinationChain, destinationAddress, destinationFee, MAX_DESTINATION_FEE, amount + ); - TokenInfo storage tokenInfo = $.tokenRegistry[token]; - if (!tokenInfo.isRegistered) { - revert TokenNotRegistered(); - } - if (tokenInfo.isForeign) { - address agent = _ensureAgent(tokenInfo.agentID); - _submitOutbound( - Assets.sendForeignToken( - agent, - AGENT_EXECUTOR, - tokenInfo, - msg.sender, - destinationChain, - destinationAddress, - destinationFee, - amount - ) - ); - } else { - _submitOutbound( - Assets.sendToken( - token, msg.sender, destinationChain, destinationAddress, destinationFee, MAX_DESTINATION_FEE, amount - ) - ); - } + _submitOutbound(ticket); } // @dev Get token address by tokenID @@ -721,26 +697,4 @@ contract Gateway is IGateway, IInitializable, IUpgradable { OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); operatorStorage.operator = config.rescueOperator; } - - /// @dev Temporary rescue ability for the initial bootstrapping phase of the bridge - function rescue(address impl, bytes32 implCodeHash, bytes calldata initializerParams) external { - OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); - if (msg.sender != operatorStorage.operator) { - revert Unauthorized(); - } - Upgrade.upgrade(impl, implCodeHash, initializerParams); - } - - function dropRescueAbility() external { - OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); - if (msg.sender != operatorStorage.operator) { - revert Unauthorized(); - } - operatorStorage.operator = address(0); - } - - function rescueOperator() external view returns (address) { - OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); - return operatorStorage.operator; - } } diff --git a/contracts/src/Params.sol b/contracts/src/Params.sol index 869cc4e75c..882c2c7856 100644 --- a/contracts/src/Params.sol +++ b/contracts/src/Params.sol @@ -85,10 +85,8 @@ struct SetPricingParametersParams { // Payload for RegisterForeignToken struct RegisterForeignTokenParams { - /// @dev The agent ID of the consensus system - bytes32 agentID; - /// @dev The token ID - bytes32 tokenID; + /// @dev The token ID (hash of stable location id of token) + bytes32 foreignTokenID; /// @dev The name of the token string name; /// @dev The symbol of the token @@ -99,10 +97,8 @@ struct RegisterForeignTokenParams { // Payload for MintForeignToken struct MintForeignTokenParams { - /// @dev The agent ID of the consensus system - bytes32 agentID; /// @dev The token ID - bytes32 tokenID; + bytes32 foreignTokenID; /// @dev The address of the recipient address recipient; /// @dev The amount to mint with @@ -110,7 +106,7 @@ struct MintForeignTokenParams { } // Payload for TransferToken -struct TransferTokenParams { +struct TransferNativeTokenParams { /// @dev The agent ID of the consensus system bytes32 agentID; /// @dev The token address diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index 61bbb29933..296f32ce57 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -134,12 +134,30 @@ library SubstrateTypes { ); } + function SendForeignTokenToAssetHubAddress32(bytes32 tokenID, bytes32 recipient, uint128 xcmFee, uint128 amount) + internal + view + returns (bytes memory) + { + return bytes.concat( + bytes1(0x00), + ScaleCodec.encodeU64(uint64(block.chainid)), + bytes1(0x02), + tokenID, + bytes1(0x00), + recipient, + ScaleCodec.encodeU128(amount), + ScaleCodec.encodeU128(xcmFee) + ); + } + // destination is AccountID32 address function SendForeignTokenToAddress32( bytes32 tokenID, ParaID paraID, bytes32 recipient, uint128 xcmFee, + uint128 destinationXcmFee, uint128 amount ) internal view returns (bytes memory) { return bytes.concat( @@ -150,8 +168,9 @@ library SubstrateTypes { bytes1(0x01), ScaleCodec.encodeU32(uint32(ParaID.unwrap(paraID))), recipient, - ScaleCodec.encodeU128(xcmFee), - ScaleCodec.encodeU128(amount) + ScaleCodec.encodeU128(destinationXcmFee), + ScaleCodec.encodeU128(amount), + ScaleCodec.encodeU128(xcmFee) ); } @@ -161,6 +180,7 @@ library SubstrateTypes { ParaID paraID, bytes20 recipient, uint128 xcmFee, + uint128 destinationXcmFee, uint128 amount ) internal view returns (bytes memory) { return bytes.concat( @@ -171,8 +191,9 @@ library SubstrateTypes { bytes1(0x02), ScaleCodec.encodeU32(uint32(ParaID.unwrap(paraID))), recipient, - ScaleCodec.encodeU128(xcmFee), - ScaleCodec.encodeU128(amount) + ScaleCodec.encodeU128(destinationXcmFee), + ScaleCodec.encodeU128(amount), + ScaleCodec.encodeU128(xcmFee) ); } } diff --git a/contracts/src/ERC20.sol b/contracts/src/Token.sol similarity index 79% rename from contracts/src/ERC20.sol rename to contracts/src/Token.sol index ee9c88c42a..f66a9e34be 100644 --- a/contracts/src/ERC20.sol +++ b/contracts/src/Token.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.25; import {IERC20} from "./interfaces/IERC20.sol"; import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; -import {ERC20Lib} from "./ERC20Lib.sol"; +import {TokenLib} from "./TokenLib.sol"; /** * @dev Implementation of the {IERC20} interface. @@ -28,33 +28,40 @@ import {ERC20Lib} from "./ERC20Lib.sol"; * functions have been added to mitigate the well-known issues around setting * allowances. See {IERC20-approve}. */ -contract ERC20 is IERC20, IERC20Permit { - using ERC20Lib for ERC20Lib.TokenStorage; - - error Unauthorized(); - - ERC20Lib.TokenStorage token; - - address public immutable OWNER; +contract Token is IERC20, IERC20Permit { + using TokenLib for TokenLib.Token; + address public immutable GATEWAY; + bytes32 public immutable DOMAIN_SEPARATOR; uint8 public immutable decimals; string public name; string public symbol; + TokenLib.Token token; + + error Unauthorized(); /** * @dev Sets the values for {name}, {symbol}, and {decimals}. */ - constructor(address _owner, string memory name_, string memory symbol_, uint8 decimals_) { - OWNER = _owner; - name = name_; - symbol = symbol_; - decimals = decimals_; - token.init(name_); + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + GATEWAY = msg.sender; + DOMAIN_SEPARATOR = keccak256( + abi.encode( + TokenLib.DOMAIN_TYPE_SIGNATURE_HASH, + keccak256(bytes(_name)), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); } - modifier onlyOwner() { - if (msg.sender != OWNER) { + modifier onlyGateway() { + if (msg.sender != GATEWAY) { revert Unauthorized(); } _; @@ -70,14 +77,14 @@ contract ERC20 is IERC20, IERC20Permit { * * - `account` cannot be the zero address. */ - function mint(address account, uint256 amount) external virtual onlyOwner { + function mint(address account, uint256 amount) external onlyGateway { token.mint(account, amount); } /** * @dev Destroys `amount` tokens from the account. */ - function burn(address account, uint256 amount) external virtual onlyOwner { + function burn(address account, uint256 amount) external onlyGateway { token.burn(account, amount); } @@ -89,7 +96,7 @@ contract ERC20 is IERC20, IERC20Permit { * - `recipient` cannot be the zero address. * - the caller must have a balance of at least `amount`. */ - function transfer(address recipient, uint256 amount) external virtual override returns (bool) { + function transfer(address recipient, uint256 amount) external returns (bool) { return token.transfer(msg.sender, recipient, amount); } @@ -108,7 +115,7 @@ contract ERC20 is IERC20, IERC20Permit { * * - `spender` cannot be the zero address. */ - function approve(address spender, uint256 amount) external virtual override returns (bool) { + function approve(address spender, uint256 amount) external returns (bool) { return token.approve(msg.sender, spender, amount); } @@ -125,7 +132,7 @@ contract ERC20 is IERC20, IERC20Permit { * - the caller must have allowance for ``sender``'s tokens of at least * `amount`. */ - function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) { + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { return token.transferFrom(sender, recipient, amount); } @@ -141,7 +148,7 @@ contract ERC20 is IERC20, IERC20Permit { * * - `spender` cannot be the zero address. */ - function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) { + function increaseAllowance(address spender, uint256 addedValue) external returns (bool) { return token.increaseAllowance(spender, addedValue); } @@ -159,33 +166,29 @@ contract ERC20 is IERC20, IERC20Permit { * - `spender` must have allowance for the caller of at least * `subtractedValue`. */ - function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) { + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) { return token.decreaseAllowance(spender, subtractedValue); } function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { - token.permit(issuer, spender, value, deadline, v, r, s); + token.permit(DOMAIN_SEPARATOR, issuer, spender, value, deadline, v, r, s); } function balanceOf(address account) external view returns (uint256) { - return token.balancesOf(account); + return token.balance[account]; } function nonces(address account) external view returns (uint256) { - return token.noncesOf(account); + return token.nonces[account]; } function totalSupply() external view returns (uint256) { - return token.totalSupplyOf(); + return token.totalSupply; } function allowance(address owner, address spender) external view returns (uint256) { - return token.allowanceOf(owner, spender); - } - - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return token.domainSeparatorOf(); + return token.allowance[owner][spender]; } } diff --git a/contracts/src/ERC20Lib.sol b/contracts/src/TokenLib.sol similarity index 58% rename from contracts/src/ERC20Lib.sol rename to contracts/src/TokenLib.sol index f67863cbc0..de21ea679f 100644 --- a/contracts/src/ERC20Lib.sol +++ b/contracts/src/TokenLib.sol @@ -7,7 +7,7 @@ pragma solidity 0.8.25; import {IERC20} from "./interfaces/IERC20.sol"; import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; -library ERC20Lib { +library TokenLib { // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') bytes32 internal constant DOMAIN_TYPE_SIGNATURE_HASH = bytes32(0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f); @@ -18,29 +18,11 @@ library ERC20Lib { string internal constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; - error InvalidAccount(); - error PermitExpired(); - error InvalidS(); - error InvalidV(); - error InvalidSignature(); - error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); - error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); - error OwnableInvalidOwner(address owner); - - struct TokenStorage { - mapping(address => uint256) balanceOf; - mapping(address => mapping(address => uint256)) allowance; - mapping(address => uint256) nonces; + struct Token { + mapping(address account => uint256) balance; + mapping(address account => mapping(address spender => uint256)) allowance; + mapping(address token => uint256) nonces; uint256 totalSupply; - bytes32 domainSeparator; - } - - function init(TokenStorage storage self, string memory name_) internal { - self.domainSeparator = keccak256( - abi.encode( - DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name_)), keccak256(bytes("1")), block.chainid, address(this) - ) - ); } /** @@ -51,11 +33,8 @@ library ERC20Lib { * - `recipient` cannot be the zero address. * - the caller must have a balance of at least `amount`. */ - function transfer(TokenStorage storage self, address sender, address recipient, uint256 amount) - external - returns (bool) - { - _transfer(self, sender, recipient, amount); + function transfer(Token storage token, address sender, address recipient, uint256 amount) external returns (bool) { + _transfer(token, sender, recipient, amount); return true; } @@ -69,10 +48,12 @@ library ERC20Lib { * * - `to` cannot be the zero address. */ - function mint(TokenStorage storage self, address account, uint256 amount) external { - if (account == address(0)) revert InvalidAccount(); + function mint(Token storage token, address account, uint256 amount) external { + if (account == address(0)) { + revert IERC20.InvalidAccount(); + } - _update(self, address(0), account, amount); + _update(token, address(0), account, amount); } /** @@ -86,10 +67,12 @@ library ERC20Lib { * - `account` cannot be the zero address. * - `account` must have at least `amount` tokens. */ - function burn(TokenStorage storage self, address account, uint256 amount) external { - if (account == address(0)) revert InvalidAccount(); + function burn(Token storage token, address account, uint256 amount) external { + if (account == address(0)) { + revert IERC20.InvalidAccount(); + } - _update(self, account, address(0), amount); + _update(token, account, address(0), amount); } /** @@ -107,11 +90,8 @@ library ERC20Lib { * * - `spender` cannot be the zero address. */ - function approve(TokenStorage storage self, address owner, address spender, uint256 amount) - external - returns (bool) - { - _approve(self, owner, spender, amount); + function approve(Token storage token, address owner, address spender, uint256 amount) external returns (bool) { + _approve(token, owner, spender, amount); return true; } @@ -128,22 +108,22 @@ library ERC20Lib { * - the caller must have allowance for ``sender``'s tokens of at least * `amount`. */ - function transferFrom(TokenStorage storage self, address sender, address recipient, uint256 amount) + function transferFrom(Token storage token, address sender, address recipient, uint256 amount) external returns (bool) { - uint256 _allowance = self.allowance[sender][msg.sender]; + uint256 _allowance = token.allowance[sender][msg.sender]; if (_allowance != type(uint256).max) { if (_allowance < amount) { - revert ERC20InsufficientAllowance(msg.sender, _allowance, amount); + revert IERC20.InsufficientAllowance(msg.sender, _allowance, amount); } unchecked { - _approve(self, sender, msg.sender, _allowance - amount); + _approve(token, sender, msg.sender, _allowance - amount); } } - _transfer(self, sender, recipient, amount); + _transfer(token, sender, recipient, amount); return true; } @@ -160,13 +140,10 @@ library ERC20Lib { * * - `spender` cannot be the zero address. */ - function increaseAllowance(TokenStorage storage self, address spender, uint256 addedValue) - external - returns (bool) - { - uint256 _allowance = self.allowance[msg.sender][spender]; + function increaseAllowance(Token storage token, address spender, uint256 addedValue) external returns (bool) { + uint256 _allowance = token.allowance[msg.sender][spender]; if (_allowance != type(uint256).max) { - _approve(self, msg.sender, spender, _allowance + addedValue); + _approve(token, msg.sender, spender, _allowance + addedValue); } return true; } @@ -185,24 +162,22 @@ library ERC20Lib { * - `spender` must have allowance for the caller of at least * `subtractedValue`. */ - function decreaseAllowance(TokenStorage storage self, address spender, uint256 subtractedValue) - external - returns (bool) - { - uint256 _allowance = self.allowance[msg.sender][spender]; + function decreaseAllowance(Token storage token, address spender, uint256 subtractedValue) external returns (bool) { + uint256 _allowance = token.allowance[msg.sender][spender]; if (_allowance != type(uint256).max) { if (_allowance < subtractedValue) { - revert ERC20InsufficientAllowance(msg.sender, _allowance, subtractedValue); + revert IERC20.InsufficientAllowance(msg.sender, _allowance, subtractedValue); } unchecked { - _approve(self, msg.sender, spender, _allowance - subtractedValue); + _approve(token, msg.sender, spender, _allowance - subtractedValue); } } return true; } function permit( - TokenStorage storage self, + Token storage token, + bytes32 domainSeparator, address issuer, address spender, uint256 value, @@ -211,46 +186,28 @@ library ERC20Lib { bytes32 r, bytes32 s ) external { - if (block.timestamp > deadline) revert PermitExpired(); + if (block.timestamp > deadline) revert IERC20Permit.PermitExpired(); - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + revert IERC20Permit.InvalidS(); + } - if (v != 27 && v != 28) revert InvalidV(); + if (v != 27 && v != 28) revert IERC20Permit.InvalidV(); bytes32 digest = keccak256( abi.encodePacked( EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, - self.domainSeparator, - keccak256(abi.encode(PERMIT_SIGNATURE_HASH, issuer, spender, value, self.nonces[issuer]++, deadline)) + domainSeparator, + keccak256(abi.encode(PERMIT_SIGNATURE_HASH, issuer, spender, value, token.nonces[issuer]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); - if (recoveredAddress != issuer) revert InvalidSignature(); + if (recoveredAddress != issuer) revert IERC20Permit.InvalidSignature(); // _approve will revert if issuer is address(0x0) - _approve(self, issuer, spender, value); - } - - function balancesOf(TokenStorage storage self, address account) internal view returns (uint256) { - return self.balanceOf[account]; - } - - function noncesOf(TokenStorage storage self, address account) external view returns (uint256) { - return self.nonces[account]; - } - - function totalSupplyOf(TokenStorage storage self) external view returns (uint256) { - return self.totalSupply; - } - - function allowanceOf(TokenStorage storage self, address owner, address spender) external view returns (uint256) { - return self.allowance[owner][spender]; - } - - function domainSeparatorOf(TokenStorage storage self) external view returns (bytes32) { - return self.domainSeparator; + _approve(token, issuer, spender, value); } /** @@ -267,10 +224,12 @@ library ERC20Lib { * - `recipient` cannot be the zero address. * - `sender` must have a balance of at least `amount`. */ - function _transfer(TokenStorage storage self, address sender, address recipient, uint256 amount) internal { - if (sender == address(0) || recipient == address(0)) revert InvalidAccount(); + function _transfer(Token storage token, address sender, address recipient, uint256 amount) internal { + if (sender == address(0) || recipient == address(0)) { + revert IERC20.InvalidAccount(); + } - _update(self, sender, recipient, amount); + _update(token, sender, recipient, amount); } /** @@ -286,10 +245,12 @@ library ERC20Lib { * - `owner` cannot be the zero address. * - `spender` cannot be the zero address. */ - function _approve(TokenStorage storage self, address owner, address spender, uint256 amount) internal { - if (owner == address(0) || spender == address(0)) revert InvalidAccount(); + function _approve(Token storage token, address owner, address spender, uint256 amount) internal { + if (owner == address(0) || spender == address(0)) { + revert IERC20.InvalidAccount(); + } - self.allowance[owner][spender] = amount; + token.allowance[owner][spender] = amount; emit IERC20.Approval(owner, spender, amount); } @@ -300,30 +261,30 @@ library ERC20Lib { * * Emits a {Transfer} event. */ - function _update(TokenStorage storage self, address from, address to, uint256 value) internal { + function _update(Token storage token, address from, address to, uint256 value) internal { if (from == address(0)) { // Overflow check required: The rest of the code assumes that totalSupply never overflows - self.totalSupply += value; + token.totalSupply += value; } else { - uint256 fromBalance = self.balanceOf[from]; + uint256 fromBalance = token.balance[from]; if (fromBalance < value) { - revert ERC20InsufficientBalance(from, fromBalance, value); + revert IERC20.InsufficientBalance(from, fromBalance, value); } unchecked { // Overflow not possible: value <= fromBalance <= totalSupply. - self.balanceOf[from] = fromBalance - value; + token.balance[from] = fromBalance - value; } } if (to == address(0)) { unchecked { // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. - self.totalSupply -= value; + token.totalSupply -= value; } } else { unchecked { // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. - self.balanceOf[to] += value; + token.balance[to] += value; } } diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 9cb32b7a79..02e8bd0acc 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -85,11 +85,12 @@ enum Command { TransferNativeFromAgent, SetTokenTransferFees, SetPricingParameters, - TransferToken, + TransferNativeToken, RegisterForeignToken, MintForeignToken } +/// @dev DEPRECATED enum AgentExecuteCommand { TransferToken } @@ -110,8 +111,5 @@ struct Ticket { struct TokenInfo { bool isRegistered; - bool isForeign; - bytes32 tokenID; - bytes32 agentID; - address token; + bytes32 foreignID; } diff --git a/contracts/src/interfaces/IERC20.sol b/contracts/src/interfaces/IERC20.sol index 7cbdbe64a8..5e921d62db 100644 --- a/contracts/src/interfaces/IERC20.sol +++ b/contracts/src/interfaces/IERC20.sol @@ -9,6 +9,8 @@ pragma solidity 0.8.25; */ interface IERC20 { error InvalidAccount(); + error InsufficientBalance(address sender, uint256 balance, uint256 needed); + error InsufficientAllowance(address spender, uint256 allowance, uint256 needed); /** * @dev Returns the amount of tokens in existence. diff --git a/contracts/src/interfaces/IERC20Permit.sol b/contracts/src/interfaces/IERC20Permit.sol index 8a91e55cfd..bcd8163669 100644 --- a/contracts/src/interfaces/IERC20Permit.sol +++ b/contracts/src/interfaces/IERC20Permit.sol @@ -5,6 +5,11 @@ pragma solidity 0.8.25; interface IERC20Permit { + error PermitExpired(); + error InvalidS(); + error InvalidV(); + error InvalidSignature(); + function DOMAIN_SEPARATOR() external view returns (bytes32); function nonces(address account) external view returns (uint256); diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 342cd7dbe8..37e725727a 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -36,7 +36,7 @@ interface IGateway { event AgentFundsWithdrawn(bytes32 indexed agentID, address indexed recipient, uint256 amount); // Emitted when foreign token from polkadot registed - event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token); + event ForeignTokenRegistered(bytes32 indexed tokenID, address token); /** * Getters diff --git a/contracts/src/storage/AssetsStorage.sol b/contracts/src/storage/AssetsStorage.sol index 8585555aeb..252c8a0247 100644 --- a/contracts/src/storage/AssetsStorage.sol +++ b/contracts/src/storage/AssetsStorage.sol @@ -16,8 +16,7 @@ library AssetsStorage { uint256 registerTokenFee; // Token registry by token address mapping(address token => TokenInfo) tokenRegistry; - // Token registry by tokenID - mapping(bytes32 tokenID => TokenInfo) tokenRegistryByID; + mapping(bytes32 foreignID => address) tokenAddressOf; } bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets.v2"); diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 2da9da986e..22776dc47e 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -25,7 +25,9 @@ import {Channel, InboundMessage, OperatingMode, ParaID, Command, ChannelID, Mult import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; -import {ERC20Lib} from "../src/ERC20Lib.sol"; +import {IERC20} from "../src/interfaces/IERC20.sol"; +import {TokenLib} from "../src/TokenLib.sol"; +import {Token} from "../src/Token.sol"; import { UpgradeParams, @@ -38,7 +40,7 @@ import { SetTokenTransferFeesParams, SetPricingParametersParams, RegisterForeignTokenParams, - TransferTokenParams, + TransferNativeTokenParams, MintForeignTokenParams } from "../src/Params.sol"; @@ -54,7 +56,6 @@ import { import {WETH9} from "canonical-weth/WETH9.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; -import {ERC20} from "../src/ERC20.sol"; contract GatewayTest is Test { // Emitted when token minted/burnt/transfered @@ -359,11 +360,15 @@ contract GatewayTest is Test { function testAgentExecution() public { token.transfer(address(assetHubAgent), 200); - TransferTokenParams memory params = - TransferTokenParams({agentID: assetHubAgentID, token: address(token), recipient: account2, amount: 10}); + TransferNativeTokenParams memory params = TransferNativeTokenParams({ + agentID: assetHubAgentID, + token: address(token), + recipient: account2, + amount: 10 + }); bytes memory encodedParams = abi.encode(params); - MockGateway(address(gateway)).transferTokenPublic(encodedParams); + MockGateway(address(gateway)).transferNativeTokenPublic(encodedParams); } function testAgentExecutionBadOrigin() public { @@ -864,64 +869,34 @@ contract GatewayTest is Test { ); } - function testAgentRegisterToken() public { - RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ - agentID: assetHubAgentID, - tokenID: dotTokenID, - name: "DOT", - symbol: "DOT", - decimals: 10 - }); + function testRegisterForeignToken() public { + RegisterForeignTokenParams memory params = + RegisterForeignTokenParams({foreignTokenID: dotTokenID, name: "DOT", symbol: "DOT", decimals: 10}); vm.expectEmit(true, true, false, false); - emit IGateway.ForeignTokenRegistered(bytes32(uint256(1)), assetHubAgentID, address(0)); - - MockGateway(address(gateway)).registerForeignTokenPublic(abi.encode(params)); - } - - function testAgentRegisterTokenWithAgentIDNotExistWillFail() public { - testAgentRegisterToken(); - - RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ - agentID: bytes32(0), - tokenID: dotTokenID, - name: "DOT", - symbol: "DOT", - decimals: 10 - }); - - vm.expectRevert(Gateway.AgentDoesNotExist.selector); + emit IGateway.ForeignTokenRegistered(bytes32(uint256(1)), address(0)); MockGateway(address(gateway)).registerForeignTokenPublic(abi.encode(params)); } - function testAgentRegisterSameTokenAgainWillFail() public { - testAgentRegisterToken(); + function testRegisterForeignTokenDuplicateFail() public { + testRegisterForeignToken(); - RegisterForeignTokenParams memory params = RegisterForeignTokenParams({ - agentID: assetHubAgentID, - tokenID: dotTokenID, - name: "DOT", - symbol: "DOT", - decimals: 10 - }); + RegisterForeignTokenParams memory params = + RegisterForeignTokenParams({foreignTokenID: dotTokenID, name: "DOT", symbol: "DOT", decimals: 10}); vm.expectRevert(Assets.TokenAlreadyRegistered.selector); MockGateway(address(gateway)).registerForeignTokenPublic(abi.encode(params)); } - function testAgentMintToken() public { - testAgentRegisterToken(); + function testMintForeignToken() public { + testRegisterForeignToken(); uint256 amount = 1000; - MintForeignTokenParams memory params = MintForeignTokenParams({ - agentID: assetHubAgentID, - tokenID: bytes32(uint256(1)), - recipient: account1, - amount: amount - }); + MintForeignTokenParams memory params = + MintForeignTokenParams({foreignTokenID: bytes32(uint256(1)), recipient: account1, amount: amount}); vm.expectEmit(true, true, false, false); emit Transfer(address(0), account1, 1000); @@ -930,18 +905,14 @@ contract GatewayTest is Test { address dotToken = MockGateway(address(gateway)).tokenAddressOf(dotTokenID); - uint256 balance = ERC20(dotToken).balanceOf(account1); + uint256 balance = Token(dotToken).balanceOf(account1); assertEq(balance, amount); } - function testAgentMintNotRegisteredTokenWillFail() public { - MintForeignTokenParams memory params = MintForeignTokenParams({ - agentID: assetHubAgentID, - tokenID: bytes32(uint256(1)), - recipient: account1, - amount: 1000 - }); + function testMintNotRegisteredTokenWillFail() public { + MintForeignTokenParams memory params = + MintForeignTokenParams({foreignTokenID: bytes32(uint256(1)), recipient: account1, amount: 1000}); vm.expectRevert(Assets.TokenNotRegistered.selector); @@ -950,7 +921,7 @@ contract GatewayTest is Test { function testSendRelayTokenToAssetHub() public { // Register and then mint some DOT to account1 - testAgentMintToken(); + testMintForeignToken(); address dotToken = MockGateway(address(gateway)).tokenAddressOf(dotTokenID); @@ -977,7 +948,7 @@ contract GatewayTest is Test { } function testSendTokenFromNotMintedAccountWillFail() public { - testAgentRegisterToken(); + testRegisterForeignToken(); address dotToken = MockGateway(address(gateway)).tokenAddressOf(dotTokenID); @@ -985,7 +956,7 @@ contract GatewayTest is Test { vm.prank(account1); - vm.expectRevert(abi.encodeWithSelector(ERC20Lib.ERC20InsufficientBalance.selector, account1, 0, 1)); + vm.expectRevert(abi.encodeWithSelector(IERC20.InsufficientBalance.selector, account1, 0, 1)); IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); } diff --git a/contracts/test/mocks/MockGateway.sol b/contracts/test/mocks/MockGateway.sol index 01d6970416..4356bb9876 100644 --- a/contracts/test/mocks/MockGateway.sol +++ b/contracts/test/mocks/MockGateway.sol @@ -85,7 +85,7 @@ contract MockGateway is Gateway { this.mintForeignToken(params); } - function transferTokenPublic(bytes calldata params) external { - this.transferToken(params); + function transferNativeTokenPublic(bytes calldata params) external { + this.transferNativeToken(params); } } From 64b9ef4828f2341495998d2d8a33dd05b073fabd Mon Sep 17 00:00:00 2001 From: ron Date: Thu, 29 Aug 2024 09:19:27 +0800 Subject: [PATCH 49/55] Remove storage migration --- contracts/src/storage/AssetsStorage.sol | 7 +-- contracts/src/storage/LegacyAssetsStorage.sol | 34 ------------- .../rococo/GatewayWithAssetStorageV2.sol | 49 ------------------- 3 files changed, 4 insertions(+), 86 deletions(-) delete mode 100644 contracts/src/storage/LegacyAssetsStorage.sol delete mode 100644 contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol diff --git a/contracts/src/storage/AssetsStorage.sol b/contracts/src/storage/AssetsStorage.sol index 252c8a0247..57b5bab21a 100644 --- a/contracts/src/storage/AssetsStorage.sol +++ b/contracts/src/storage/AssetsStorage.sol @@ -6,6 +6,8 @@ import {TokenInfo, ParaID} from "../Types.sol"; library AssetsStorage { struct Layout { + // Native token registry by token address + mapping(address token => TokenInfo) tokenRegistry; address assetHubAgent; ParaID assetHubParaID; // XCM fee charged by AssetHub for registering a token (DOT) @@ -14,12 +16,11 @@ library AssetsStorage { uint128 assetHubReserveTransferFee; // Extra fee for registering a token, to discourage spamming (Ether) uint256 registerTokenFee; - // Token registry by token address - mapping(address token => TokenInfo) tokenRegistry; + // Foreign token registry by token ID mapping(bytes32 foreignID => address) tokenAddressOf; } - bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets.v2"); + bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets"); function layout() internal pure returns (Layout storage $) { bytes32 slot = SLOT; diff --git a/contracts/src/storage/LegacyAssetsStorage.sol b/contracts/src/storage/LegacyAssetsStorage.sol deleted file mode 100644 index 199c7c9ede..0000000000 --- a/contracts/src/storage/LegacyAssetsStorage.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2023 Snowfork -pragma solidity 0.8.25; - -import {ParaID} from "../Types.sol"; - -library LegacyAssetsStorage { - struct TokenInfoLegacy { - bool isRegistered; - bytes31 __padding; - } - - struct Layout { - // Legacy token registry by token address - mapping(address token => TokenInfoLegacy) tokenRegistry; - address assetHubAgent; - ParaID assetHubParaID; - // XCM fee charged by AssetHub for registering a token (DOT) - uint128 assetHubCreateAssetFee; - // XCM fee charged by AssetHub for receiving a token from the Gateway (DOT) - uint128 assetHubReserveTransferFee; - // Extra fee for registering a token, to discourage spamming (Ether) - uint256 registerTokenFee; - } - - bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.assets"); - - function layout() internal pure returns (Layout storage $) { - bytes32 slot = SLOT; - assembly { - $.slot := slot - } - } -} diff --git a/contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol b/contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol deleted file mode 100644 index 0b4117e04a..0000000000 --- a/contracts/src/upgrades/rococo/GatewayWithAssetStorageV2.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2023 Snowfork -pragma solidity 0.8.25; - -import "../../Gateway.sol"; - -import {AssetsStorage} from "../../storage/AssetsStorage.sol"; -import {LegacyAssetsStorage} from "../../storage/LegacyAssetsStorage.sol"; - -contract GatewayWithAssetStorageV2 is Gateway { - constructor( - address beefyClient, - address agentExecutor, - ParaID bridgeHubParaID, - bytes32 bridgeHubAgentID, - uint8 foreignTokenDecimals, - uint128 destinationMaxTransferFee - ) - Gateway( - beefyClient, - agentExecutor, - bridgeHubParaID, - bridgeHubAgentID, - foreignTokenDecimals, - destinationMaxTransferFee - ) - {} - - function initialize(bytes memory data) external override { - // Prevent initialization of storage in implementation contract - if (ERC1967.load() == address(0)) { - revert Unauthorized(); - } - - address[] memory tokens = abi.decode(data, (address[])); - - LegacyAssetsStorage.Layout storage $ = LegacyAssetsStorage.layout(); - AssetsStorage.Layout storage $v2 = AssetsStorage.layout(); - - $v2.assetHubAgent = $.assetHubAgent; - $v2.assetHubParaID = $.assetHubParaID; - $v2.assetHubCreateAssetFee = $.assetHubCreateAssetFee; - $v2.assetHubReserveTransferFee = $.assetHubReserveTransferFee; - $v2.registerTokenFee = $.registerTokenFee; - for (uint256 i = 0; i < tokens.length; i++) { - $v2.tokenRegistry[tokens[i]].isRegistered = $.tokenRegistry[tokens[i]].isRegistered; - } - } -} From 001c990428bf47ee74ae7c535a22735df3843513 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 30 Aug 2024 21:01:40 +0800 Subject: [PATCH 50/55] Smoke test for register relay token --- relayer/contracts/gateway.go | 15 +++-- smoketest/src/helper.rs | 66 +++++----------------- smoketest/tests/register_polkadot_token.rs | 60 +++++++++++++++++--- 3 files changed, 74 insertions(+), 67 deletions(-) diff --git a/relayer/contracts/gateway.go b/relayer/contracts/gateway.go index 33e550dff8..ff6723fc6c 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\":\"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\":\"agentID\",\"type\":\"bytes32\",\"indexed\":false,\"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\":\"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\":\"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}]", } // GatewayABI is the input ABI used to generate the binding from. @@ -1231,14 +1231,13 @@ func (it *GatewayForeignTokenRegisteredIterator) Close() error { // GatewayForeignTokenRegistered represents a ForeignTokenRegistered event raised by the Gateway contract. type GatewayForeignTokenRegistered struct { TokenID [32]byte - AgentID [32]byte Token common.Address Raw types.Log // Blockchain specific contextual infos } -// FilterForeignTokenRegistered is a free log retrieval operation binding the contract event 0xc4a9ffa44bcfe101c1450dc2e08360693767d951c4a5c726e36cde0371dc4ddb. +// FilterForeignTokenRegistered is a free log retrieval operation binding the contract event 0x57f58171b8777633d03aff1e7408b96a3d910c93a7ce433a8cb7fb837dc306a6. // -// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token) +// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, address token) func (_Gateway *GatewayFilterer) FilterForeignTokenRegistered(opts *bind.FilterOpts, tokenID [][32]byte) (*GatewayForeignTokenRegisteredIterator, error) { var tokenIDRule []interface{} @@ -1253,9 +1252,9 @@ func (_Gateway *GatewayFilterer) FilterForeignTokenRegistered(opts *bind.FilterO return &GatewayForeignTokenRegisteredIterator{contract: _Gateway.contract, event: "ForeignTokenRegistered", logs: logs, sub: sub}, nil } -// WatchForeignTokenRegistered is a free log subscription operation binding the contract event 0xc4a9ffa44bcfe101c1450dc2e08360693767d951c4a5c726e36cde0371dc4ddb. +// WatchForeignTokenRegistered is a free log subscription operation binding the contract event 0x57f58171b8777633d03aff1e7408b96a3d910c93a7ce433a8cb7fb837dc306a6. // -// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token) +// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, address token) func (_Gateway *GatewayFilterer) WatchForeignTokenRegistered(opts *bind.WatchOpts, sink chan<- *GatewayForeignTokenRegistered, tokenID [][32]byte) (event.Subscription, error) { var tokenIDRule []interface{} @@ -1295,9 +1294,9 @@ func (_Gateway *GatewayFilterer) WatchForeignTokenRegistered(opts *bind.WatchOpt }), nil } -// ParseForeignTokenRegistered is a log parse operation binding the contract event 0xc4a9ffa44bcfe101c1450dc2e08360693767d951c4a5c726e36cde0371dc4ddb. +// ParseForeignTokenRegistered is a log parse operation binding the contract event 0x57f58171b8777633d03aff1e7408b96a3d910c93a7ce433a8cb7fb837dc306a6. // -// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, bytes32 agentID, address token) +// Solidity: event ForeignTokenRegistered(bytes32 indexed tokenID, address token) func (_Gateway *GatewayFilterer) ParseForeignTokenRegistered(log types.Log) (*GatewayForeignTokenRegistered, error) { event := new(GatewayForeignTokenRegistered) if err := _Gateway.contract.UnpackLog(event, "ForeignTokenRegistered", log); err != nil { diff --git a/smoketest/src/helper.rs b/smoketest/src/helper.rs index 46a3e0ec01..e0b668e11d 100644 --- a/smoketest/src/helper.rs +++ b/smoketest/src/helper.rs @@ -2,23 +2,8 @@ use crate::{ constants::*, contracts::i_gateway, parachains::{ - bridgehub::{ - self, - api::{ - runtime_types, - runtime_types::{ - snowbridge_core::outbound::v1::OperatingMode, - staging_xcm::v4::junction::NetworkId, xcm::VersionedLocation, - }, - }, - }, - penpal::{ - api::{ - runtime_types as penpalTypes, - runtime_types::xcm::VersionedLocation as penpalVersionLocation, - }, - {self}, - }, + bridgehub::{self, api::runtime_types::snowbridge_core::outbound::v1::OperatingMode}, + penpal::{self, api::runtime_types as penpalTypes}, relaychain, relaychain::api::runtime_types::{ pallet_xcm::pallet::Call as RelaychainPalletXcmCall, @@ -49,13 +34,12 @@ use ethers::{ }; use futures::StreamExt; use penpalTypes::{ - pallet_xcm::pallet::Call, - penpal_runtime::RuntimeCall, - staging_xcm::v3::multilocation::MultiLocation, - xcm::{ - v3::{junction::Junction, junctions::Junctions}, - VersionedXcm, + penpal_runtime::RuntimeCall as PenpalRuntimeCall, + staging_xcm::v4::{ + junction::Junction as PenpalJunction, junctions::Junctions as PenpalJunctions, + location::Location as PenpalLocation, }, + xcm::{VersionedLocation as PenpalVersionedLocation, VersionedXcm as PenpalVersionedXcm}, }; use std::{ops::Deref, sync::Arc, time::Duration}; use subxt::{ @@ -199,16 +183,19 @@ pub struct SudoResult { pub async fn send_sudo_xcm_transact( penpal_client: &Box>, - message: Box, + message: Box, ) -> Result> { - let dest = Box::new(VersionedLocation::V3(MultiLocation { + let dest = Box::new(PenpalVersionedLocation::V4(PenpalLocation { parents: 1, - interior: Junctions::X1(Junction::Parachain(BRIDGE_HUB_PARA_ID)), + interior: PenpalJunctions::X1([PenpalJunction::Parachain(BRIDGE_HUB_PARA_ID)]), })); let sudo_call = penpal::api::sudo::calls::TransactionApi::sudo( &penpal::api::sudo::calls::TransactionApi, - RuntimeCall::PolkadotXcm(Call::send { dest, message }), + PenpalRuntimeCall::PolkadotXcm(penpalTypes::pallet_xcm::pallet::Call::send { + dest, + message, + }), ); let owner = Pair::from_string("//Alice", None).expect("cannot create keypair"); @@ -381,28 +368,3 @@ pub fn print_event_log_for_unit_tests(log: &Log) { println!("}}") } - -pub async fn construct_register_relay_token_call( - bridge_hub_client: &Box>, -) -> Result, Box> { - type Junctions = runtime_types::staging_xcm::v4::junctions::Junctions; - type Junction = runtime_types::staging_xcm::v4::junction::Junction; - let location = VersionedLocation::V4(runtime_types::staging_xcm::v4::location::Location { - parents: 1, - interior: Junctions::X1([Junction::Parachain(ASSET_HUB_PARA_ID)]), - }); - let asset = VersionedLocation::V4(runtime_types::staging_xcm::v4::location::Location { - parents: 1, - interior: Junctions::X1([Junction::GlobalConsensus(NetworkId::Rococo)]), - }); - let metadata = runtime_types::snowbridge_core::AssetRegistrarMetadata { - name: "roc".as_bytes().to_vec(), - symbol: "roc".as_bytes().to_vec(), - decimals: 12, - }; - let call = bridgehub::api::ethereum_system::calls::TransactionApi - .force_register_token(location, asset, metadata) - .encode_call_data(&bridge_hub_client.metadata())?; - - Ok(call) -} diff --git a/smoketest/tests/register_polkadot_token.rs b/smoketest/tests/register_polkadot_token.rs index dd10fb98a7..48271c83fd 100644 --- a/smoketest/tests/register_polkadot_token.rs +++ b/smoketest/tests/register_polkadot_token.rs @@ -1,19 +1,65 @@ use snowbridge_smoketest::{ - contracts::i_gateway::ForeignTokenRegisteredFilter, helper::*, - parachains::bridgehub::api::ethereum_system::events::RegisterToken, + contracts::i_gateway::ForeignTokenRegisteredFilter, + helper::*, + parachains::{ + bridgehub, + bridgehub::api::{ + ethereum_system::events::RegisterToken, + runtime_types, + runtime_types::{ + bounded_collections::bounded_vec::BoundedVec, staging_xcm::v4::junction::NetworkId, + xcm::VersionedLocation, + }, + }, + }, }; +use subxt_signer::sr25519::dev; #[tokio::test] async fn register_polkadot_token() { let test_clients = initial_clients().await.expect("initialize clients"); - let encoded_call = construct_register_relay_token_call(&test_clients.bridge_hub_client) - .await - .expect("construct inner call."); + type Junctions = runtime_types::staging_xcm::v4::junctions::Junctions; + type Junction = runtime_types::staging_xcm::v4::junction::Junction; + let asset = VersionedLocation::V4(runtime_types::staging_xcm::v4::location::Location { + parents: 1, + interior: Junctions::X1([Junction::GlobalConsensus(NetworkId::Westend)]), + }); + let metadata = runtime_types::snowbridge_core::AssetMetadata { + name: BoundedVec( + "wnd" + .as_bytes() + .to_vec() + .iter() + .chain([1_u8; 29].to_vec().iter()) + .map(|v| *v) + .collect::>(), + ), + symbol: BoundedVec( + "wnd" + .as_bytes() + .to_vec() + .iter() + .chain([1_u8; 29].to_vec().iter()) + .map(|v| *v) + .collect::>(), + ), + decimals: 12, + }; + let call = + bridgehub::api::ethereum_system::calls::TransactionApi.register_token(asset, metadata); - governance_bridgehub_call_from_relay_chain(encoded_call) + let result = test_clients + .bridge_hub_client + .tx() + .sign_and_submit_then_watch_default(&call, &dev::bob()) + .await + .expect("send register call.") + .wait_for_finalized_success() .await - .expect("set token fees"); + .expect("call success"); + + println!("call issued at bridgehub block hash {:?}", result.extrinsic_hash()); wait_for_bridgehub_event::(&test_clients.bridge_hub_client).await; From 324cf535f58779071f66cfaaba4316e5783ee948 Mon Sep 17 00:00:00 2001 From: ron Date: Sat, 31 Aug 2024 22:59:37 +0800 Subject: [PATCH 51/55] Fix fee estimation --- contracts/src/Gateway.sol | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index d4ac703db6..07efb9f04c 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -534,17 +534,8 @@ contract Gateway is IGateway, IInitializable, IUpgradable { // Ensure outbound messaging is allowed _ensureOutboundMessagingEnabled(channel); - uint256 fee; - AssetsStorage.Layout storage $ = AssetsStorage.layout(); - if (ticket.dest == $.assetHubParaID) { - fee = _calculateFee(ticket.costs); - } else { - // The assumption here is that no matter what the fee token is, usually the xcm execution cost on substrate chain is very tiny - // as demonstrated in https://www.notion.so/snowfork/Gateway-Parameters-0cf913d089374027a86721883306ee61 - // the DELIVERY_COST on BH and TRANSFER_TOKEN_FEE are all no more than 0.1 DOT so the total cost as 0.2 DOT equals 0.00048 ETH. - // Consider a 5x buffer a hard code 0.002 ETH should be pretty enough to cover both costs - fee = 2000000000000000; - } + // Destination fee always in DOT + uint256 fee = _calculateFee(ticket.costs); // Ensure the user has enough funds for this message to be accepted if (msg.value < fee) { From fe4cb7323784e4e1f50c13938a1a19d68462d1bc Mon Sep 17 00:00:00 2001 From: ron Date: Sat, 31 Aug 2024 23:00:24 +0800 Subject: [PATCH 52/55] Fix binding --- smoketest/make-bindings.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoketest/make-bindings.sh b/smoketest/make-bindings.sh index 525d151e6d..02b96f77da 100755 --- a/smoketest/make-bindings.sh +++ b/smoketest/make-bindings.sh @@ -6,7 +6,7 @@ mkdir -p src/contracts # Generate Rust bindings for contracts forge bind --module --overwrite \ - --select 'IGateway|IUpgradable|WETH9|MockGatewayV2|ERC20' \ + --select 'IGateway|IUpgradable|WETH9|MockGatewayV2|Token' \ --bindings-path src/contracts \ --root ../contracts From 46bc45fcec4f4b279588e270c0603216368bfddd Mon Sep 17 00:00:00 2001 From: ron Date: Sat, 31 Aug 2024 23:00:47 +0800 Subject: [PATCH 53/55] Remove outdated doc --- rfc/polkadot-native-assets.md | 134 ---------------------------------- 1 file changed, 134 deletions(-) delete mode 100644 rfc/polkadot-native-assets.md diff --git a/rfc/polkadot-native-assets.md b/rfc/polkadot-native-assets.md deleted file mode 100644 index 3177733bd1..0000000000 --- a/rfc/polkadot-native-assets.md +++ /dev/null @@ -1,134 +0,0 @@ -# RFC: Introduce Polkadot-native assets to Ethereum - - -## Summary - -This RFC proposes the feature to introduce Polkadot-native assets to Ethereum through our bridge, including two PRs separately with https://github.com/Snowfork/snowbridge/pull/1155 for solidity and https://github.com/Snowfork/polkadot-sdk/pull/128 for substrate. - - -## Explanation - -We use native token on Penpal for the integration and the basic work flow includes steps as following: - -### 1. Register Polkadot-native assets as ERC20 - -First by adding a [dispatchable](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/bridges/snowbridge/pallets/system/src/lib.rs#L604) to `EthereumControl` pallet to register new Polkadot-native assets called via XCM, this dispatchable will send a message over the bridge to the agent of the Parachain. - -On Ethereum the agent will [instantiate a new ERC20 token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L259) representing the Polkadot-native asset. - -There is a E2E test [register_penpal_native_token](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L571) for demonstration. - -### 2. Send Polkadot-native assets via [reserve_transfer_assets](https://github.com/Snowfork/polkadot-sdk/blob/2d8f3b13cf61c3ce8e5ea15438c4cfbfe3a26722/polkadot/xcm/pallet-xcm/src/lib.rs#L1027) - -First it requires the source parachain to extend the `XcmRouter` to route xcm with destination to Ethereum through our bridge on BH, [config](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs#L401) on Penpal for the reference. - -Worth to note that the [fee config in BridgeTable](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs#L465-L468) should cover the total execution cost on BridgeHub and Ethereum in DOT. - -There is a E2E test [send_penpal_native_token_to_ethereum](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L655) for demonstration. - -Check the xcm executed on penpal it shows that the native token has been reserved to the sovereign account of Ethereum. - -``` -2024-04-22T05:08:45.117066Z TRACE xcm::process_instruction: === TransferAsset { assets: Assets([Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(100000000000) }]), beneficiary: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } } -2024-04-22T05:08:45.117087Z TRACE xcm::fungible_adapter: internal_transfer_asset what: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(100000000000) }, from: Location { parents: 0, interior: X1([AccountId32 { network: Some(Rococo), id: [212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125] }]) }, to: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-22T05:08:45.117476Z TRACE xcm::execute: result: Ok(()) -``` - - -The xcm forwarded to BridgeHub as following: -``` -instructions: [ - WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4200000000000) }])), - BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: Here }), fun: Fungible(4200000000000) }, weight_limit: Unlimited }, - SetAppendix(Xcm([DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 1, interior: X1([Parachain(2000)]) } }])), - ExportMessage { - network: Ethereum { chain_id: 11155111 }, - destination: Here, - xcm: Xcm([ - ReserveAssetDeposited(Assets([Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }])), - ClearOrigin, - BuyExecution { fees: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, weight_limit: Unlimited }, - DepositAsset { assets: Wild(AllCounted(1)), beneficiary: Location { parents: 0, interior: X1([AccountKey20 { network: None, key: [68, 165, 126, 226, 242, 252, 203, 133, 253, 162, 176, 177, 142, 189, 13, 141, 35, 51, 112, 14] }]) } }, - SetTopic([156, 140, 10, 35, 194, 14, 239, 123, 235, 56, 179, 99, 185, 189, 107, 206, 228, 222, 106, 10, 227, 75, 47, 41, 171, 186, 195, 157, 172, 237, 251, 50]) - ]) - } -] -``` - -So the top-level `WithdrawAsset` will withdraw relay token from sovereign account of Penpal as fee to pay for the execution cost on both BridgeHub and Ethereum. - -What we really care about is the internal xcm in `ExportMessage` with [the convert logic in outbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5a4f3af6932cfcbae98435cb16f98a2ee8db4812/bridges/snowbridge/primitives/router/src/outbound/mod.rs#L318) it will be converted into a simple `Command` which will be relayed and finally executed on Ethereum. - -On Ethereum side based on the `Command` the Agent will [mint foreign token to the recipient](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L269) to finish the whole flow. - -#### Fee flow - -- User represents a user who kicks off an extrinsic on the parachain. -- Parachain represents the source parachain, its sovereign or its agent depending on context. - -Sequence|Where|Who|What --|-|-|- -1|Penpal|User| For `reserve_transfer_assets` pays(DOT, Native) to node to execute custom extrinsic; pays (DOT) to Treasury for both delivery cost on BH and execution cost on Ethereum(i.e. `EthereumBaseFee`). -2|Bridge Hub|Parachain|Pays(DOT) to Treasury Account for delivery(local fee), pays(DOT) to Parachain sovereign for delivery(remote fee), essentially a refund. Remote fee converted to ETH here. -3|Gateway|Relayer|pays(ETH) to validate and execute message. -4|Gateway|Parachain Agent|pays(ETH) to relayer for delivery(reward+refund) and execution. - - -### 3. Send Polkadot-native assets back from Ethereum to Substrate via [sendToken](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Gateway.sol#L463) - -So first on Ethereum the Agent will [burn foreign token](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L235) and the [payload](https://github.com/Snowfork/snowbridge/blob/07545cf7e8f0321e4ab89d7f5eb52bc85ab3d4c1/contracts/src/Assets.sol#L220) will be relayed and finally executed on BridgeHub. - -Then on BridgeHub with [the convert logic in inbound-router](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L354) it will be converted into a [xcm](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/bridges/snowbridge/primitives/router/src/inbound/mod.rs#L392-L399) which will be sent to the destination chain. - -There is a E2E test [send_penpal_native_token_from_ethereum](https://github.com/Snowfork/polkadot-sdk/blob/5ad44df00259f53bc0dac8ea76f085540fdb23a4/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs#L759) with the xcm forwarded to Penpal as following: - - -``` - instructions: [ - DescendOrigin(X1([PalletInstance(80)])), - UniversalOrigin(GlobalConsensus(Ethereum { chain_id: 11155111 })), - WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }])), - BuyExecution { fees: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, weight_limit: Unlimited }, - ClearOrigin, - DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } -] -``` - -Check the xcm executed on Penpal it shows that the native token has been withdraw from the sovereign account of Ethereum and then deposit to the beneficiary as expected. - -``` -2024-04-22T05:23:54.708694Z TRACE xcm::process: origin: Some(Location { parents: 1, interior: X1([Parachain(1013)]) }), total_surplus/refunded: Weight { ref_time: 0, proof_size: 0 }/Weight { ref_time: 0, proof_size: 0 }, error_handler_weight: Weight { ref_time: 0, proof_size: 0 } -2024-04-22T05:23:54.708707Z TRACE xcm::process_instruction: === DescendOrigin(X1([PalletInstance(80)])) -2024-04-22T05:23:54.708718Z TRACE xcm::process_instruction: === UniversalOrigin(GlobalConsensus(Ethereum { chain_id: 11155111 })) -2024-04-22T05:23:54.708804Z TRACE xcm::process_instruction: === WithdrawAsset(Assets([Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }])) -2024-04-22T05:23:54.708815Z TRACE xcm::ensure_can_subsume_assets: worst_case_holding_len: 2, holding_limit: 64 -2024-04-22T05:23:54.708845Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-22T05:23:54.708956Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-22T05:23:54.708966Z TRACE xcm::fungible_adapter: withdraw_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) } -2024-04-22T05:23:54.709091Z TRACE xcm::process_instruction: === BuyExecution { fees: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(8000000000) }, weight_limit: Limited(Weight { ref_time: 7000000000, proof_size: 458752 }) } -2024-04-22T05:23:54.709110Z TRACE xcm::weight: UsingComponents::buy_weight weight: Weight { ref_time: 7000000000, proof_size: 458752 }, payment: AssetsInHolding { fungible: {AssetId(Location { parents: 0, interior: Here }): 8000000000}, non_fungible: {} }, context: XcmContext { origin: Some(Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) }), message_id: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], topic: None } -2024-04-22T05:23:54.709147Z TRACE xcm::weight: UsingComponents::buy_weight weight: Weight { ref_time: 7000000000, proof_size: 458752 }, payment: AssetsInHolding { fungible: {AssetId(Location { parents: 0, interior: Here }): 8000000000}, non_fungible: {} }, context: XcmContext { origin: Some(Location { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 })]) }), message_id: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], topic: None } -2024-04-22T05:23:54.709166Z TRACE xcm::process_instruction: === ClearOrigin -2024-04-22T05:23:54.709174Z TRACE xcm::process_instruction: === DepositAsset { assets: Wild(AllCounted(2)), beneficiary: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } } -2024-04-22T05:23:54.709191Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(3412480000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } -2024-04-22T05:23:54.709350Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } -2024-04-22T05:23:54.709362Z TRACE xcm::fungible_adapter: deposit_asset what: Asset { id: AssetId(Location { parents: 1, interior: X2([GlobalConsensus(Rococo), Parachain(2000)]) }), fun: Fungible(100000000000) }, who: Location { parents: 0, interior: X1([AccountId32 { network: None, id: [142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72] }]) } -2024-04-22T05:23:54.709459Z TRACE xcm::process_instruction: === SetTopic([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) -2024-04-22T05:23:54.709470Z TRACE xcm::execute: result: Ok(()) -2024-04-22T05:23:54.709477Z TRACE xcm::refund_surplus: total_surplus: Weight { ref_time: 0, proof_size: 0 }, total_refunded: Weight { ref_time: 0, proof_size: 0 }, current_surplus: Weight { ref_time: 0, proof_size: 0 } -2024-04-22T05:23:54.709484Z TRACE xcm::refund_surplus: total_refunded: Weight { ref_time: 0, proof_size: 0 } -2024-04-22T05:23:54.709546Z TRACE xcm::process-message: XCM message execution complete, used weight: Weight(ref_time: 7000000000, proof_size: 458752) -``` - -#### Fee Flow - -- dApp is represents `msg.sender` or its sovereign depending on context. -- Parachain represents the target parachain, its sovereign or its agent depending on context. -- Ethereum Sovereign represents `Location{parent:2,interior:[GlobalConsensus(Ethereum)]}` - -Sequence|Where|Who|What --|-|-|- -1|Gateway|dApp|pays(ETH, converted to DOT here) Parachain Agent for both delivery cost on BH and execution cost on destination(DOT,Native). -2|Bridge Hub|Relayer|pays(DOT) node for execution -3|Bridge Hub|Parachain Sovereign|pays(DOT) Relayer for delivery (refund+reward) -4|Parachain|Ethereum Sovereign|pays(DOT, Native) for execution only. From cf596f3d840b6348df625d384e35fec8f7256b3a Mon Sep 17 00:00:00 2001 From: ron Date: Sat, 31 Aug 2024 23:01:38 +0800 Subject: [PATCH 54/55] Fork upgrade test with sanity checks --- .../src/upgrades/polkadot/GatewayPNA.sol | 33 ++++++++ contracts/test/ForkUpgrade.t.sol | 77 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 contracts/src/upgrades/polkadot/GatewayPNA.sol create mode 100644 contracts/test/ForkUpgrade.t.sol diff --git a/contracts/src/upgrades/polkadot/GatewayPNA.sol b/contracts/src/upgrades/polkadot/GatewayPNA.sol new file mode 100644 index 0000000000..730751e70c --- /dev/null +++ b/contracts/src/upgrades/polkadot/GatewayPNA.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.25; + +import "../../Gateway.sol"; +import {AssetsStorage} from "../../storage/AssetsStorage.sol"; +import {TokenInfo} from "../../Types.sol"; + +contract GatewayPNA is Gateway { + constructor( + address beefyClient, + address agentExecutor, + ParaID bridgeHubParaID, + bytes32 bridgeHubAgentID, + uint8 foreignTokenDecimals, + uint128 destinationMaxTransferFee + ) + Gateway( + beefyClient, + agentExecutor, + bridgeHubParaID, + bridgeHubAgentID, + foreignTokenDecimals, + destinationMaxTransferFee + ) + {} + + function initialize(bytes memory) external override {} + + function tokenInfo(address token) external view returns (TokenInfo memory) { + return AssetsStorage.layout().tokenRegistry[token]; + } +} diff --git a/contracts/test/ForkUpgrade.t.sol b/contracts/test/ForkUpgrade.t.sol new file mode 100644 index 0000000000..b0a325951d --- /dev/null +++ b/contracts/test/ForkUpgrade.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; +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 {GatewayPNA} from "../src/upgrades/polkadot/GatewayPNA.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"; + +contract ForkUpgradeTest is Test { + address private constant GatewayProxy = 0x27ca963C279c93801941e1eB8799c23f407d68e7; + address private constant BeefyClient = 0x6eD05bAa904df3DE117EcFa638d4CB84e1B8A00C; + bytes32 private constant BridgeHubAgent = 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314; + + function setUp() public { + vm.createSelectFork("https://rpc.tenderly.co/fork/b77e07b8-ad6d-4e83-b5be-30a2001964aa", 20645700); + vm.allowCheatcodes(GatewayProxy); + vm.startPrank(GatewayProxy); + forkUpgrade(); + } + + function forkUpgrade() public { + AgentExecutor executor = new AgentExecutor(); + + GatewayPNA newLogic = + new GatewayPNA(BeefyClient, address(executor), ParaID.wrap(1002), BridgeHubAgent, 10, 20000000000); + + UpgradeParams memory params = + UpgradeParams({impl: address(newLogic), implCodeHash: address(newLogic).codehash, initParams: bytes("")}); + + vm.expectEmit(true, false, false, false); + emit IUpgradable.Upgraded(address(newLogic)); + + Gateway(GatewayProxy).upgrade(abi.encode(params)); + } + + function checkLegacyToken() public { + TokenInfo memory weth = GatewayPNA(GatewayProxy).tokenInfo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + assertEq(weth.isRegistered, true); + assertEq(weth.foreignID, bytes32("")); + TokenInfo memory myth = GatewayPNA(GatewayProxy).tokenInfo(0xBA41Ddf06B7fFD89D1267b5A93BFeF2424eb2003); + assertEq(myth.isRegistered, true); + assertEq(myth.foreignID, bytes32("")); + } + + function registerForeignToken() public { + bytes32 dotId = 0xa8f2ec5bdd7a07d844ee3bce83f9ba3881f495d96f07cacbeeb77d9e031db4f0; + RegisterForeignTokenParams memory params = + RegisterForeignTokenParams({foreignTokenID: dotId, name: "DOT", symbol: "DOT", decimals: 10}); + + vm.expectEmit(true, true, false, false); + emit IGateway.ForeignTokenRegistered(dotId, address(0x0)); + + GatewayPNA(GatewayProxy).registerForeignToken(abi.encode(params)); + TokenInfo memory dot = GatewayPNA(GatewayProxy).tokenInfo(0x70D9d338A6b17957B16836a90192BD8CDAe0b53d); + assertEq(dot.isRegistered, true); + assertEq(dot.foreignID, dotId); + } + + 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(); + } +} From 6efc7a11f9d5fc4ad0dba2cfb9f2ed8596eaeaea Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 2 Sep 2024 07:44:56 +0800 Subject: [PATCH 55/55] Fix test --- smoketest/tests/transfer_polkadot_token.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smoketest/tests/transfer_polkadot_token.rs b/smoketest/tests/transfer_polkadot_token.rs index f9c3b5a763..277ec2666c 100644 --- a/smoketest/tests/transfer_polkadot_token.rs +++ b/smoketest/tests/transfer_polkadot_token.rs @@ -7,7 +7,7 @@ use ethers::{ use futures::StreamExt; use snowbridge_smoketest::{ constants::*, - contracts::{erc20, erc20::TransferFilter}, + contracts::{token, token::TransferFilter}, helper::AssetHubConfig, parachains::assethub::{ api::runtime_types::{ @@ -73,7 +73,7 @@ async fn transfer_polkadot_token() { .expect("call success"); let erc20_dot_address: Address = ERC20_DOT_CONTRACT.into(); - let erc20_dot = erc20::ERC20::new(erc20_dot_address, ethereum_client.clone()); + let erc20_dot = token::Token::new(erc20_dot_address, ethereum_client.clone()); let wait_for_blocks = 500; let mut stream = ethereum_client.subscribe_blocks().await.unwrap().take(wait_for_blocks);