Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer NFT from Ethereum #1185

Open
wants to merge 3 commits into
base: bridge-next-gen
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion contracts/src/Agent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// SPDX-FileCopyrightText: 2023 Snowfork <[email protected]>
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
Expand All @@ -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;
}
}
9 changes: 9 additions & 0 deletions contracts/src/AgentExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}
}

Expand All @@ -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));
}
}
71 changes: 66 additions & 5 deletions contracts/src/Assets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,11 +44,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) {
Expand Down Expand Up @@ -188,4 +191,62 @@ 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);
}

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;
}
}
20 changes: 18 additions & 2 deletions contracts/src/Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
)
);
}

Expand Down Expand Up @@ -613,4 +615,18 @@ contract Gateway is IGateway, IInitializable, IUpgradable {
assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee;
assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee;
}

// Register an Ethereum-native nft token 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];
}

// 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));
}
}
36 changes: 36 additions & 0 deletions contracts/src/SubstrateTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -133,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)
);
}
}
6 changes: 4 additions & 2 deletions contracts/src/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ enum Command {
}

enum AgentExecuteCommand {
TransferToken
TransferToken,
TransferNftToken
}

/// @dev Application-level costs for a message
Expand All @@ -107,5 +108,6 @@ struct Ticket {

struct TokenInfo {
bool isRegistered;
bytes31 __padding;
bool isNft;
bytes30 __padding;
}
6 changes: 6 additions & 0 deletions contracts/src/interfaces/IGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,10 @@ 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;

/// @dev Send Nft tokens to parachain `destinationChain` and deposit into account `destinationAddress`
function sendNftToken(address token, uint128 tokenId, MultiAddress calldata destinationAddress) external payable;
}
76 changes: 67 additions & 9 deletions contracts/test/Gateway.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -51,8 +50,11 @@ 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";
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;
Expand All @@ -70,6 +72,7 @@ contract GatewayTest is Test {
GatewayProxy public gateway;

WETH9 public token;
MockNft public nftToken;

address public account1;
address public account2;
Expand Down Expand Up @@ -99,12 +102,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,
Expand Down Expand Up @@ -145,6 +143,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) {
Expand Down Expand Up @@ -177,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
*/
Expand Down Expand Up @@ -857,6 +861,60 @@ 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);
}

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);
}

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);
}
}
9 changes: 1 addition & 8 deletions contracts/test/mocks/MockGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/mocks/MockGatewayV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading