Skip to content

Commit

Permalink
Asset from polkadot (#1155)
Browse files Browse the repository at this point in the history
* Asset from polkadot

* Remove params unnecessary

* Experiment: return calldata from Agent instead of call Gateway directly

* To store registries also on agent

* Send polkadot tokens back

* Cleanup for not exceed the size limit

* Add smoke test for register polkadot token

* Transfer relay token to Ethereum

* Reuse IGateway.sendToken for polkadot native asset

* More cleanup

* Fix encode substrate types

* Add smoke test send relay token back

* Rename to sendForeignToken

* Improve ERC20.sol

* Revert the change double register the token

* Migration for AssetsStorage

* Rename to _sendForeignTokenCosts

* Use storage for less gas

* Rename to tokenAddressOf

* Rename as _burnToken

* _ensureAgent in Gateway

* Remove unused

* Clean up smoke test

* Terminate register foreign token in Gateway

* Mint foreign token without the callback

* Burn token without the callback

* constructor ERC20 with the agent as owner

* Move handler to Assets.sol reduce contract size

* Refactoring to move transferToken to Assets

* Remove TokenMinted&TokenBurned event

* Move Command.RegisterToken to top level

* Fix smoke tests

* Move the check before interactions

* Polish

* Introduce ERC20Lib.sol

* Remove unused

* More test

* Remove AgentExecuteCommand

* More contract tests

* Update contract address

* Add AgentExecuteCommand back for compatibility

* Cleanup

* Add rfc

* Update polkadot-native-assets.md

* Update rfc

* Update rfc with fee section

* Fix _calculateFee for polkadot native token

* 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 <[email protected]>

* Remove storage migration

* Smoke test for register relay token

* Fix fee estimation

* Fix binding

* Remove outdated doc

* Fork upgrade test with sanity checks

* Fix test

---------

Co-authored-by: Vincent Geddes <[email protected]>
  • Loading branch information
yrong and vgeddes authored Sep 2, 2024
1 parent 49420df commit 07bea54
Show file tree
Hide file tree
Showing 26 changed files with 1,565 additions and 129 deletions.
17 changes: 6 additions & 11 deletions contracts/src/AgentExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,26 @@ import {SubstrateTypes} from "./SubstrateTypes.sol";

import {IERC20} from "./interfaces/IERC20.sol";
import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.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.
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.
///
function transferNative(address payable recipient, uint256 amount) external {
recipient.safeNativeTransfer(amount);
}

/// @dev Transfer ERC20 to `recipient`. Only callable via `execute`.
function transferToken(address token, address recipient, uint128 amount) external {
_transferToken(token, recipient, amount);
}

/// @dev Transfer ERC20 to `recipient`. Only callable via `execute`.
function _transferToken(address token, address recipient, uint128 amount) internal {
IERC20(token).safeTransfer(recipient, amount);
Expand Down
197 changes: 192 additions & 5 deletions contracts/src/Assets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ 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";
import {Token} from "./Token.sol";

/// @title Library for implementing Ethereum->Polkadot ERC20 transfers.
library Assets {
Expand All @@ -24,6 +30,10 @@ library Assets {
error TokenNotRegistered();
error Unsupported();
error InvalidDestinationFee();
error AgentDoesNotExist();
error TokenAlreadyRegistered();
error TokenMintFailed();
error TokenTransferFailed();

function isTokenRegistered(address token) external view returns (bool) {
return AssetsStorage.layout().tokenRegistry[token].isRegistered;
Expand All @@ -42,11 +52,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 @@ -98,10 +109,40 @@ library Assets {
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);

Expand Down Expand Up @@ -153,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();
}
Expand Down Expand Up @@ -188,4 +321,58 @@ library Assets {

emit IGateway.TokenRegistrationSent(token);
}

// @dev Register a new fungible Polkadot token for an agent
function registerForeignToken(bytes32 foreignTokenID, string memory name, string memory symbol, uint8 decimals)
external
{
AssetsStorage.Layout storage $ = AssetsStorage.layout();
if ($.tokenAddressOf[foreignTokenID] != address(0)) {
revert TokenAlreadyRegistered();
}
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(bytes32 foreignTokenID, address recipient, uint256 amount) external {
address token = _ensureTokenAddressOf(foreignTokenID);
Token(token).mint(recipient, amount);
}

// @dev Transfer ERC20 to `recipient`
function transferNativeToken(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) {
AssetsStorage.Layout storage $ = AssetsStorage.layout();
return $.tokenAddressOf[tokenID];
}

// @dev Get token address by tokenID
function _ensureTokenAddressOf(bytes32 tokenID) internal view returns (address) {
AssetsStorage.Layout storage $ = AssetsStorage.layout();
if ($.tokenAddressOf[tokenID] == address(0)) {
revert TokenNotRegistered();
}
return $.tokenAddressOf[tokenID];
}

function _isTokenRegistered(address token) internal view returns (bool) {
AssetsStorage.Layout storage $ = AssetsStorage.layout();
return $.tokenRegistry[token].isRegistered;
}
}
Loading

0 comments on commit 07bea54

Please sign in to comment.