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 {} }