Skip to content

Commit

Permalink
Send polkadot tokens back
Browse files Browse the repository at this point in the history
  • Loading branch information
yrong committed Mar 22, 2024
1 parent a543f0b commit 6212597
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 9 deletions.
13 changes: 12 additions & 1 deletion contracts/src/AgentExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}
81 changes: 78 additions & 3 deletions contracts/src/Assets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
9 changes: 8 additions & 1 deletion contracts/src/ERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
Expand Down
19 changes: 19 additions & 0 deletions contracts/src/Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
38 changes: 38 additions & 0 deletions contracts/src/SubstrateTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}
1 change: 1 addition & 0 deletions contracts/src/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ struct Ticket {

struct TokenInfo {
bool isRegistered;
bool isForeign;
bytes32 tokenID;
bytes32 agentID;
address token;
Expand Down
22 changes: 22 additions & 0 deletions contracts/src/interfaces/IGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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
);
}
36 changes: 32 additions & 4 deletions contracts/test/Gateway.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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
);
}
}
15 changes: 15 additions & 0 deletions contracts/test/mocks/GatewayUpgradeMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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 {}
}

0 comments on commit 6212597

Please sign in to comment.