From 5cb4c91e0f7635302b0139be3a03ad051c899cec Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 23 Apr 2024 22:24:52 +0800 Subject: [PATCH 1/3] Register nft token --- contracts/src/Assets.sol | 31 +++++++++++++++++++++----- contracts/src/Gateway.sol | 15 +++++++++++-- contracts/src/SubstrateTypes.sol | 15 +++++++++++++ contracts/src/Types.sol | 3 ++- contracts/src/interfaces/IGateway.sol | 3 +++ contracts/test/Gateway.t.sol | 30 ++++++++++++++++++------- contracts/test/mocks/MockGateway.sol | 9 +------- contracts/test/mocks/MockGatewayV2.sol | 2 +- contracts/test/mocks/MockNft.sol | 9 ++++++++ 9 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 contracts/test/mocks/MockNft.sol diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 53a79eaed..928fd5c8d 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -42,11 +42,12 @@ library Assets { IERC20(token).safeTransferFrom(sender, agent, amount); } - function sendTokenCosts(address token, ParaID destinationChain, uint128 destinationChainFee, uint128 maxDestinationChainFee) - external - view - returns (Costs memory costs) - { + function sendTokenCosts( + address token, + ParaID destinationChain, + uint128 destinationChainFee, + uint128 maxDestinationChainFee + ) external view returns (Costs memory costs) { AssetsStorage.Layout storage $ = AssetsStorage.layout(); TokenInfo storage info = $.tokenRegistry[token]; if (!info.isRegistered) { @@ -188,4 +189,24 @@ library Assets { emit IGateway.TokenRegistrationSent(token); } + + /// @dev Registers a nft token + /// @param token The Nft token address. + function registerNftToken(address token) external returns (Ticket memory ticket) { + if (!token.isContract()) { + revert InvalidToken(); + } + + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + + TokenInfo storage info = $.tokenRegistry[token]; + info.isRegistered = true; + info.isNft = true; + + ticket.dest = $.assetHubParaID; + ticket.costs = _registerTokenCosts(); + ticket.payload = SubstrateTypes.RegisterNftToken(token, $.assetHubCreateAssetFee); + + emit IGateway.TokenRegistrationSent(token); + } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 130fee235..a5e1c7dd7 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -44,7 +44,7 @@ import { import {CoreStorage} from "./storage/CoreStorage.sol"; import {PricingStorage} from "./storage/PricingStorage.sol"; -import {AssetsStorage} from "./storage/AssetsStorage.sol"; +import {AssetsStorage, TokenInfo} from "./storage/AssetsStorage.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; @@ -416,7 +416,9 @@ contract Gateway is IGateway, IInitializable, IUpgradable { uint128 amount ) external payable { _submitOutbound( - Assets.sendToken(token, msg.sender, destinationChain, destinationAddress, destinationFee, MAX_DESTINATION_FEE, amount) + Assets.sendToken( + token, msg.sender, destinationChain, destinationAddress, destinationFee, MAX_DESTINATION_FEE, amount + ) ); } @@ -613,4 +615,13 @@ contract Gateway is IGateway, IInitializable, IUpgradable { assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee; assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee; } + + // Register an Ethereum-native token in the gateway and on AssetHub + function registerNftToken(address token) external payable { + _submitOutbound(Assets.registerNftToken(token)); + } + + function tokenInfo(address token) external view returns (TokenInfo memory) { + return AssetsStorage.layout().tokenRegistry[token]; + } } diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index af817ac1a..d33c3bee0 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -66,6 +66,21 @@ library SubstrateTypes { ); } + /** + * @dev SCALE-encodes `router_primitives::inbound::VersionedMessage` containing payload + * `RegisterNftToken::Create` + */ + // solhint-disable-next-line func-name-mixedcase + function RegisterNftToken(address token, uint128 fee) internal view returns (bytes memory) { + return bytes.concat( + bytes1(0x00), + ScaleCodec.encodeU64(uint64(block.chainid)), + bytes1(0x02), + SubstrateTypes.H160(token), + ScaleCodec.encodeU128(fee) + ); + } + /** * @dev SCALE-encodes `router_primitives::inbound::VersionedMessage` containing payload * `NativeTokensMessage::Mint` diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 93d41bc0f..93ecbe9d7 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -107,5 +107,6 @@ struct Ticket { struct TokenInfo { bool isRegistered; - bytes31 __padding; + bool isNft; + bytes30 __padding; } diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 5bcb2fd0d..9c9576ba6 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -102,4 +102,7 @@ interface IGateway { uint128 destinationFee, uint128 amount ) external payable; + + /// @dev Register an Nft token and create a wrapped derivative on AssetHub in the `ForeignUniques` pallet. + function registerNftToken(address token) external payable; } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 74fb60a9c..b55037d93 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -23,7 +23,6 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {MultiAddress} from "../src/MultiAddress.sol"; import {Channel, InboundMessage, OperatingMode, ParaID, Command, ChannelID, MultiAddress} from "../src/Types.sol"; - import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; @@ -51,6 +50,8 @@ import { import {WETH9} from "canonical-weth/WETH9.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; +import {MockNft} from "./mocks/MockNft.sol"; +import {TokenInfo} from "../src/Types.sol"; contract GatewayTest is Test { ParaID public bridgeHubParaID = ParaID.wrap(1001); @@ -70,6 +71,7 @@ contract GatewayTest is Test { GatewayProxy public gateway; WETH9 public token; + MockNft public nftToken; address public account1; address public account2; @@ -99,12 +101,7 @@ contract GatewayTest is Test { function setUp() public { AgentExecutor executor = new AgentExecutor(); gatewayLogic = new MockGateway( - address(0), - address(executor), - bridgeHubParaID, - bridgeHubAgentID, - foreignTokenDecimals, - maxDestinationFee + address(0), address(executor), bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals, maxDestinationFee ); Gateway.Config memory config = Gateway.Config({ mode: OperatingMode.Normal, @@ -145,6 +142,8 @@ contract GatewayTest is Test { recipientAddress32 = multiAddressFromBytes32(keccak256("recipient")); recipientAddress20 = multiAddressFromBytes20(bytes20(keccak256("recipient"))); + + nftToken = new MockNft(); } function makeCreateAgentCommand() public pure returns (Command, bytes memory) { @@ -857,6 +856,21 @@ contract GatewayTest is Test { IGateway(address(gateway)).quoteSendTokenFee(address(token), destPara, maxDestinationFee + 1); vm.expectRevert(Assets.InvalidDestinationFee.selector); - IGateway(address(gateway)).sendToken{value: fee}(address(token), destPara, recipientAddress32, maxDestinationFee + 1, 1); + IGateway(address(gateway)).sendToken{value: fee}( + address(token), destPara, recipientAddress32, maxDestinationFee + 1, 1 + ); + } + + function testRegisterNftToken() public { + vm.expectEmit(false, false, false, true); + emit IGateway.TokenRegistrationSent(address(nftToken)); + + vm.expectEmit(true, false, false, false); + emit IGateway.OutboundMessageAccepted(assetHubParaID.into(), 1, messageID, bytes("")); + + IGateway(address(gateway)).registerNftToken{value: 2 ether}(address(nftToken)); + + TokenInfo memory info = MockGateway(address(gateway)).tokenInfo(address(nftToken)); + assertEq(info.isNft, true); } } diff --git a/contracts/test/mocks/MockGateway.sol b/contracts/test/mocks/MockGateway.sol index 2dbe3e53a..f6fe62f84 100644 --- a/contracts/test/mocks/MockGateway.sol +++ b/contracts/test/mocks/MockGateway.sol @@ -20,14 +20,7 @@ contract MockGateway is Gateway { uint8 foreignTokenDecimals, uint128 maxDestinationFee ) - Gateway( - beefyClient, - agentExecutor, - bridgeHubParaID, - bridgeHubHubAgentID, - foreignTokenDecimals, - maxDestinationFee - ) + Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubHubAgentID, foreignTokenDecimals, maxDestinationFee) {} function agentExecutePublic(bytes calldata params) external { diff --git a/contracts/test/mocks/MockGatewayV2.sol b/contracts/test/mocks/MockGatewayV2.sol index 31329282e..f71a2518c 100644 --- a/contracts/test/mocks/MockGatewayV2.sol +++ b/contracts/test/mocks/MockGatewayV2.sol @@ -19,7 +19,7 @@ library AdditionalStorage { } // Used to test upgrades. -contract MockGatewayV2 is IInitializable { +contract MockGatewayV2 is IInitializable { // Reinitialize gateway with some additional storage fields function initialize(bytes memory params) external { AdditionalStorage.Layout storage $ = AdditionalStorage.layout(); diff --git a/contracts/test/mocks/MockNft.sol b/contracts/test/mocks/MockNft.sol new file mode 100644 index 000000000..37d879c16 --- /dev/null +++ b/contracts/test/mocks/MockNft.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import "openzeppelin/token/ERC721/ERC721.sol"; +import "openzeppelin/access/Ownable.sol"; + +contract MockNft is ERC721, Ownable { + constructor() ERC721("MyToken", "MTK") Ownable() {} +} From 81f7c1c944c095f468e8b51384febdcc8dfdcde4 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 29 Apr 2024 08:13:43 +0800 Subject: [PATCH 2/3] Send ethereum nft token to substrate --- contracts/src/Agent.sol | 8 ++++- contracts/src/Assets.sol | 40 +++++++++++++++++++++ contracts/src/Gateway.sol | 7 +++- contracts/src/SubstrateTypes.sol | 51 +++++++++++++++++++-------- contracts/src/interfaces/IGateway.sol | 3 ++ contracts/test/Gateway.t.sol | 28 ++++++++++++++- contracts/test/mocks/MockNft.sol | 8 +++++ 7 files changed, 127 insertions(+), 18 deletions(-) diff --git a/contracts/src/Agent.sol b/contracts/src/Agent.sol index 687306413..3239661d9 100644 --- a/contracts/src/Agent.sol +++ b/contracts/src/Agent.sol @@ -2,10 +2,12 @@ // SPDX-FileCopyrightText: 2023 Snowfork pragma solidity 0.8.23; +import {IERC721Receiver} from "openzeppelin/token/ERC721/IERC721Receiver.sol"; + /// @title An agent contract that acts on behalf of a consensus system on Polkadot /// @dev Instances of this contract act as an agents for arbitrary consensus systems on Polkadot. These consensus systems /// can include toplevel parachains as as well as nested consensus systems within a parachain. -contract Agent { +contract Agent is IERC721Receiver { error Unauthorized(); /// @dev The unique ID for this agent, derived from the MultiLocation of the corresponding consensus system on Polkadot @@ -32,4 +34,8 @@ contract Agent { } return executor.delegatecall(data); } + + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } } diff --git a/contracts/src/Assets.sol b/contracts/src/Assets.sol index 928fd5c8d..22f0af68d 100644 --- a/contracts/src/Assets.sol +++ b/contracts/src/Assets.sol @@ -12,6 +12,8 @@ import {SubstrateTypes} from "./SubstrateTypes.sol"; import {ParaID, MultiAddress, Ticket, Costs} from "./Types.sol"; import {Address} from "./utils/Address.sol"; +import {IERC721} from "openzeppelin/token/ERC721/IERC721.sol"; + /// @title Library for implementing Ethereum->Polkadot ERC20 transfers. library Assets { using Address for address; @@ -209,4 +211,42 @@ library Assets { emit IGateway.TokenRegistrationSent(token); } + + function sendNftToken(address token, uint128 tokenId, address sender, MultiAddress calldata destinationAddress) + external + returns (Ticket memory ticket) + { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + + TokenInfo storage info = $.tokenRegistry[token]; + if (!info.isRegistered) { + revert TokenNotRegistered(); + } + + // Lock the funds into AssetHub's agent contract + _transferNftToAgent($.assetHubAgent, token, sender, tokenId); + + ticket.dest = $.assetHubParaID; + ticket.costs = _sendNftTokenCosts(); + + ticket.payload = SubstrateTypes.SendNftTokenToAssetHubAddress32( + token, destinationAddress.asAddress32(), tokenId, $.assetHubReserveTransferFee + ); + emit IGateway.TokenSent(token, sender, $.assetHubParaID, destinationAddress, tokenId); + } + + /// @dev transfer Nft token from the sender to the specified agent + function _transferNftToAgent(address agent, address token, address sender, uint256 tokenId) internal { + if (!token.isContract()) { + revert InvalidToken(); + } + + IERC721(token).safeTransferFrom(sender, agent, tokenId); + } + + function _sendNftTokenCosts() internal view returns (Costs memory costs) { + AssetsStorage.Layout storage $ = AssetsStorage.layout(); + costs.foreign = $.assetHubReserveTransferFee; + costs.native = 0; + } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index a5e1c7dd7..a62ea10dc 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -616,7 +616,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable { assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee; } - // Register an Ethereum-native token in the gateway and on AssetHub + // Register an Ethereum-native nft token on AssetHub function registerNftToken(address token) external payable { _submitOutbound(Assets.registerNftToken(token)); } @@ -624,4 +624,9 @@ contract Gateway is IGateway, IInitializable, IUpgradable { function tokenInfo(address token) external view returns (TokenInfo memory) { return AssetsStorage.layout().tokenRegistry[token]; } + + // Send an Ethereum-native token to AssetHub + function sendNftToken(address token, uint128 tokenId, MultiAddress calldata destinationAddress) external payable { + _submitOutbound(Assets.sendNftToken(token, tokenId, msg.sender, destinationAddress)); + } } diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index d33c3bee0..5eac5ea82 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -66,21 +66,6 @@ library SubstrateTypes { ); } - /** - * @dev SCALE-encodes `router_primitives::inbound::VersionedMessage` containing payload - * `RegisterNftToken::Create` - */ - // solhint-disable-next-line func-name-mixedcase - function RegisterNftToken(address token, uint128 fee) internal view returns (bytes memory) { - return bytes.concat( - bytes1(0x00), - ScaleCodec.encodeU64(uint64(block.chainid)), - bytes1(0x02), - SubstrateTypes.H160(token), - ScaleCodec.encodeU128(fee) - ); - } - /** * @dev SCALE-encodes `router_primitives::inbound::VersionedMessage` containing payload * `NativeTokensMessage::Mint` @@ -148,4 +133,40 @@ library SubstrateTypes { ScaleCodec.encodeU128(xcmFee) ); } + + /** + * @dev SCALE-encodes `router_primitives::inbound::VersionedMessage` containing payload + * `RegisterNftToken::Create` + */ + // solhint-disable-next-line func-name-mixedcase + function RegisterNftToken(address token, uint128 fee) internal view returns (bytes memory) { + return bytes.concat( + bytes1(0x00), + ScaleCodec.encodeU64(uint64(block.chainid)), + bytes1(0x02), + SubstrateTypes.H160(token), + ScaleCodec.encodeU128(fee) + ); + } + + /** + * @dev SCALE-encodes `router_primitives::inbound::VersionedMessage` containing payload + * `NftTokensMessage::Mint` + */ + // destination is AccountID32 address on AssetHub + function SendNftTokenToAssetHubAddress32(address token, bytes32 recipient, uint128 tokenId, uint128 fee) + internal + view + returns (bytes memory) + { + return bytes.concat( + bytes1(0x00), + ScaleCodec.encodeU64(uint64(block.chainid)), + bytes1(0x03), + SubstrateTypes.H160(token), + ScaleCodec.encodeU128(tokenId), + recipient, + ScaleCodec.encodeU128(fee) + ); + } } diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 9c9576ba6..87b8ff209 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -105,4 +105,7 @@ interface IGateway { /// @dev Register an Nft token and create a wrapped derivative on AssetHub in the `ForeignUniques` pallet. function registerNftToken(address token) external payable; + + /// @dev Send Nft tokens to parachain `destinationChain` and deposit into account `destinationAddress` + function sendNftToken(address token, uint128 tokenId, MultiAddress calldata destinationAddress) external payable; } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index b55037d93..8062ffbf0 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -52,8 +52,9 @@ import {WETH9} from "canonical-weth/WETH9.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; import {MockNft} from "./mocks/MockNft.sol"; import {TokenInfo} from "../src/Types.sol"; +import {IERC721Receiver} from "openzeppelin/token/ERC721/IERC721Receiver.sol"; -contract GatewayTest is Test { +contract GatewayTest is Test, IERC721Receiver { ParaID public bridgeHubParaID = ParaID.wrap(1001); bytes32 public bridgeHubAgentID = keccak256("1001"); address public bridgeHubAgent; @@ -176,6 +177,10 @@ contract GatewayTest is Test { fallback() external payable {} receive() external payable {} + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } + /** * Message Verification */ @@ -873,4 +878,25 @@ contract GatewayTest is Test { TokenInfo memory info = MockGateway(address(gateway)).tokenInfo(address(nftToken)); assertEq(info.isNft, true); } + + function testSendNftTokenToAssetHub() public { + // Mint token(id:0) and approve gateway to use + uint128 tokenId = 0; + nftToken.mint(address(this)); + nftToken.approve(address(gateway), uint256(tokenId)); + + // register token first + uint256 fee = IGateway(address(gateway)).quoteRegisterTokenFee(); + IGateway(address(gateway)).registerNftToken{value: fee}(address(nftToken)); + + // Expect the gateway to emit `TokenSent` & `OutboundMessageAccepted` + ParaID destPara = assetHubParaID; + fee = IGateway(address(gateway)).quoteSendTokenFee(address(nftToken), destPara, 1); + vm.expectEmit(true, true, false, true); + emit IGateway.TokenSent(address(nftToken), address(this), destPara, recipientAddress32, tokenId); + vm.expectEmit(true, false, false, false); + emit IGateway.OutboundMessageAccepted(assetHubParaID.into(), 1, messageID, bytes("")); + + IGateway(address(gateway)).sendNftToken{value: fee}(address(nftToken), tokenId, recipientAddress32); + } } diff --git a/contracts/test/mocks/MockNft.sol b/contracts/test/mocks/MockNft.sol index 37d879c16..790452f61 100644 --- a/contracts/test/mocks/MockNft.sol +++ b/contracts/test/mocks/MockNft.sol @@ -5,5 +5,13 @@ import "openzeppelin/token/ERC721/ERC721.sol"; import "openzeppelin/access/Ownable.sol"; contract MockNft is ERC721, Ownable { + uint256 public totalMints = 0; + constructor() ERC721("MyToken", "MTK") Ownable() {} + + function mint(address to) public { + uint256 tokenId = totalMints; + totalMints++; + _safeMint(to, tokenId); + } } From 16e01981972b204bc508eaff9182b18feb83dc56 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 29 Apr 2024 17:23:17 +0800 Subject: [PATCH 3/3] Transfer nft back from AssetHub to Ethereum --- contracts/src/AgentExecutor.sol | 9 +++++++++ contracts/src/Types.sol | 3 ++- contracts/test/Gateway.t.sol | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index c9cdaa885..aeda3f2d5 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -7,6 +7,7 @@ import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; +import {IERC721} from "openzeppelin/token/ERC721/IERC721.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,6 +23,9 @@ 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.TransferNftToken) { + (address token, address recipient, uint128 tokenId) = abi.decode(params, (address, address, uint128)); + _transferNftToken(token, recipient, tokenId); } } @@ -36,4 +40,9 @@ contract AgentExecutor { function _transferToken(address token, address recipient, uint128 amount) internal { IERC20(token).safeTransfer(recipient, amount); } + + /// @dev Transfer Nft to `recipient`. Only callable via `execute`. + function _transferNftToken(address token, address recipient, uint128 tokenId) internal { + IERC721(token).safeTransferFrom(address(this), recipient, uint256(tokenId)); + } } diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index 93ecbe9d7..bd20e5a7d 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -88,7 +88,8 @@ enum Command { } enum AgentExecuteCommand { - TransferToken + TransferToken, + TransferNftToken } /// @dev Application-level costs for a message diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 8062ffbf0..3a24fb976 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -899,4 +899,22 @@ contract GatewayTest is Test, IERC721Receiver { IGateway(address(gateway)).sendNftToken{value: fee}(address(nftToken), tokenId, recipientAddress32); } + + function testAgentTransferNft() public { + testSendNftTokenToAssetHub(); + uint128 tokenId = 0; + + AgentExecuteParams memory params = AgentExecuteParams({ + agentID: assetHubAgentID, + payload: abi.encode( + AgentExecuteCommand.TransferNftToken, abi.encode(address(nftToken), address(account1), tokenId) + ) + }); + + bytes memory encodedParams = abi.encode(params); + MockGateway(address(gateway)).agentExecutePublic(encodedParams); + // assert token transfer to account1 + address owner = nftToken.ownerOf(tokenId); + assertEq(owner, account1); + } }