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

Asset from polkadot #1155

Merged
merged 64 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
7501630
Asset from polkadot
yrong Mar 13, 2024
e0c3581
Remove params unnecessary
yrong Mar 19, 2024
f070e16
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Mar 20, 2024
780c7bd
Experiment: return calldata from Agent instead of call Gateway directly
yrong Mar 20, 2024
27353f0
To store registries also on agent
yrong Mar 20, 2024
a543f0b
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Mar 21, 2024
6212597
Send polkadot tokens back
yrong Mar 22, 2024
d28d9df
Cleanup for not exceed the size limit
yrong Mar 25, 2024
c23f8bb
Add smoke test for register polkadot token
yrong Mar 26, 2024
8c8afac
Transfer relay token to Ethereum
yrong Mar 26, 2024
0b3344c
Reuse IGateway.sendToken for polkadot native asset
yrong Mar 26, 2024
2d17fca
More cleanup
yrong Mar 26, 2024
047becf
Fix encode substrate types
yrong Mar 26, 2024
3fc6cab
Add smoke test send relay token back
yrong Mar 26, 2024
e53afd0
Rename to sendForeignToken
yrong Mar 27, 2024
5e9820b
Improve ERC20.sol
yrong Mar 27, 2024
fddb9cf
Revert the change double register the token
yrong Mar 28, 2024
e5d9dd8
Migration for AssetsStorage
yrong Mar 29, 2024
9f7e99b
Rename to _sendForeignTokenCosts
yrong Mar 29, 2024
724c649
Use storage for less gas
yrong Mar 29, 2024
6eb8344
Rename to tokenAddressOf
yrong Mar 29, 2024
7c29684
Rename as _burnToken
yrong Mar 29, 2024
edccccd
_ensureAgent in Gateway
yrong Mar 29, 2024
c170eea
Remove unused
yrong Mar 29, 2024
953245b
Clean up smoke test
yrong Mar 29, 2024
adc73a3
Terminate register foreign token in Gateway
yrong Apr 4, 2024
28f513d
Mint foreign token without the callback
yrong Apr 4, 2024
9b7811d
Burn token without the callback
yrong Apr 4, 2024
b00538f
constructor ERC20 with the agent as owner
yrong Apr 4, 2024
3da6be4
Move handler to Assets.sol reduce contract size
yrong Apr 4, 2024
e051f4c
Refactoring to move transferToken to Assets
yrong Apr 4, 2024
79c1ba0
Remove TokenMinted&TokenBurned event
yrong Apr 4, 2024
fdf15aa
Move Command.RegisterToken to top level
yrong Apr 4, 2024
1e6dac0
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Apr 5, 2024
3439213
Fix smoke tests
yrong Apr 6, 2024
c2adf8f
Move the check before interactions
yrong Apr 8, 2024
b0b582d
Polish
yrong Apr 8, 2024
985115f
Introduce ERC20Lib.sol
yrong Apr 9, 2024
ec33548
Remove unused
yrong Apr 9, 2024
32f8d86
More test
yrong Apr 9, 2024
b2a666a
Remove AgentExecuteCommand
yrong Apr 9, 2024
17f328b
Merge branch 'bridge-next-gen' into ron/polkadot-assets-on-ethereum
yrong Apr 9, 2024
6148c03
More contract tests
yrong Apr 9, 2024
4075c11
Update contract address
yrong Apr 10, 2024
fcd7539
Add AgentExecuteCommand back for compatibility
yrong Apr 11, 2024
dc78276
Cleanup
yrong Apr 11, 2024
07545cf
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Apr 16, 2024
03cf01c
Add rfc
yrong Apr 16, 2024
e9013cd
Update polkadot-native-assets.md
yrong Apr 16, 2024
a028d5c
Merge branch 'bridge-next-gen' into ron/polkadot-assets-on-ethereum
yrong Apr 19, 2024
3b7e178
Update rfc
yrong Apr 22, 2024
63e8322
Update rfc with fee section
yrong Apr 22, 2024
c887e7e
Fix _calculateFee for polkadot native token
yrong May 3, 2024
f892da4
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Aug 22, 2024
84bb52c
Improve solidity code for PNA (#1275)
vgeddes Aug 28, 2024
64b9ef4
Remove storage migration
yrong Aug 29, 2024
de3863c
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Aug 29, 2024
25092dd
Merge branch 'main' into ron/polkadot-assets-on-ethereum
yrong Aug 29, 2024
001c990
Smoke test for register relay token
yrong Aug 30, 2024
324cf53
Fix fee estimation
yrong Aug 31, 2024
fe4cb73
Fix binding
yrong Aug 31, 2024
46bc45f
Remove outdated doc
yrong Aug 31, 2024
cf596f3
Fork upgrade test with sanity checks
yrong Aug 31, 2024
6efc7a1
Fix test
yrong Sep 1, 2024
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
26 changes: 13 additions & 13 deletions contracts/src/AgentExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,20 @@
// SPDX-FileCopyrightText: 2023 Snowfork <[email protected]>
pragma solidity 0.8.23;

import {AgentExecuteCommand, ParaID} from "./Types.sol";
import {ParaID} from "./Types.sol";
import {SubstrateTypes} from "./SubstrateTypes.sol";

import {IERC20} from "./interfaces/IERC20.sol";
import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol";
import {ERC20} from "./ERC20.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.
///
Expand All @@ -33,7 +24,16 @@ contract AgentExecutor {
}

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

/// @dev Mint ERC20 token to `recipient`.
function mintToken(address token, address recipient, uint256 amount) external {
ERC20(token).mint(recipient, amount);
}

function burnToken(address token, address sender, uint256 amount) external {
ERC20(token).burn(sender, amount);
}
}
122 changes: 117 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 {ERC20} from "./ERC20.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 Down Expand Up @@ -83,11 +93,6 @@ library Assets {
) 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
_transferToAgent($.assetHubAgent, token, sender, amount);

Expand Down Expand Up @@ -174,4 +179,111 @@ library Assets {

emit IGateway.TokenRegistrationSent(token);
}

// @dev Transfer polkadot native tokens back
function sendForeignToken(
address agent,
address executor,
TokenInfo storage info,
address sender,
ParaID destinationChain,
MultiAddress calldata destinationAddress,
uint128 destinationChainFee,
uint128 amount
) external returns (Ticket memory ticket) {
if (destinationChainFee == 0) {
revert InvalidDestinationFee();
}
// Polkadot-native token: burn wrapped token
_burnToken(executor, agent, info.token, sender, amount);

ticket.dest = destinationChain;
ticket.costs = _sendForeignTokenCosts(destinationChainFee);

if (destinationAddress.isAddress32()) {
// The receiver has a 32-byte account ID
ticket.payload = SubstrateTypes.SendForeignTokenToAddress32(
info.tokenID, destinationChain, destinationAddress.asAddress32(), destinationChainFee, amount
);
} else if (destinationAddress.isAddress20()) {
// The receiver has a 20-byte account ID
ticket.payload = SubstrateTypes.SendForeignTokenToAddress20(
info.tokenID, destinationChain, destinationAddress.asAddress20(), destinationChainFee, amount
);
} else {
revert Unsupported();
}

emit IGateway.TokenSent(info.token, sender, destinationChain, destinationAddress, amount);
}

function _burnToken(address agentExecutor, address agent, address token, address sender, uint256 amount) internal {
bytes memory call = abi.encodeCall(AgentExecutor.burnToken, (token, sender, amount));
(bool success, bytes memory returndata) = (Agent(payable(agent)).invoke(agentExecutor, call));
Call.verifyResult(success, returndata);
}

function _sendForeignTokenCosts(uint128 destinationChainFee) internal pure returns (Costs memory costs) {
costs.foreign = destinationChainFee;
costs.native = 0;
}

// @dev Register a new fungible Polkadot token for an agent
function registerForeignToken(
bytes32 agentID,
address agent,
bytes32 tokenID,
string memory name,
string memory symbol,
uint8 decimals
) external {
AssetsStorage.Layout storage $ = AssetsStorage.layout();
if ($.tokenRegistryByID[tokenID].isRegistered == true) {
revert TokenAlreadyRegistered();
}
ERC20 foreignToken = new ERC20(agent, name, symbol, decimals);
address token = address(foreignToken);
TokenInfo memory info =
TokenInfo({isRegistered: true, isForeign: true, tokenID: tokenID, agentID: agentID, token: token});
$.tokenRegistry[token] = info;
$.tokenRegistryByID[tokenID] = info;
emit IGateway.ForeignTokenRegistered(tokenID, agentID, token);
}

// @dev Mint foreign token from Polkadot
function mintForeignToken(address executor, address agent, bytes32 tokenID, address recipient, uint256 amount)
external
{
address token = _tokenAddressOf(tokenID);
bytes memory call = abi.encodeCall(AgentExecutor.mintToken, (token, recipient, amount));
(bool success,) = Agent(payable(agent)).invoke(executor, call);
if (!success) {
revert TokenMintFailed();
}
}

// @dev Transfer ERC20 to `recipient`
function transferToken(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) {
return _tokenAddressOf(tokenID);
}

// @dev Get token address by tokenID
function _tokenAddressOf(bytes32 tokenID) internal view returns (address) {
AssetsStorage.Layout storage $ = AssetsStorage.layout();
if ($.tokenRegistryByID[tokenID].isRegistered == false) {
revert TokenNotRegistered();
}
return $.tokenRegistryByID[tokenID].token;
}
}
191 changes: 191 additions & 0 deletions contracts/src/ERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 Axelar Network
// SPDX-FileCopyrightText: 2023 Snowfork <[email protected]>

pragma solidity 0.8.23;

import {IERC20} from "./interfaces/IERC20.sol";
import {IERC20Permit} from "./interfaces/IERC20Permit.sol";
import {ERC20Lib} from "./ERC20Lib.sol";

/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* This supply mechanism has been added in {ERC20Permit-mint}.
*
* We have followed general OpenZeppelin guidelines: functions revert instead
* of returning `false` on failure. This behavior is conventional and does
* not conflict with the expectations of ERC20 applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to these events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract ERC20 is IERC20, IERC20Permit {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a better name for this would be something like WrappedPolkadotAsset or something more descriptive. This name feels extremely generic. We can also specialize this contract with the setOwner function and events for minting, burning, transfer, etc.

Copy link
Collaborator

@vgeddes vgeddes Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the name ERC20 at least for the base contract.

However, along the lines you suggest, we could add a child contract WrappedToken that inherits from ERC20, with specialized functions for burning and anything else needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think we should just leave the naming alone for now while we sort out the more important protocol-level issues you raised in other comments.

using ERC20Lib for ERC20Lib.TokenStorage;

error Unauthorized();

ERC20Lib.TokenStorage token;

address public immutable OWNER;

uint8 public immutable decimals;

string public name;
string public symbol;

/**
* @dev Sets the values for {name}, {symbol}, and {decimals}.
*/
constructor(address _owner, string memory name_, string memory symbol_, uint8 decimals_) {
OWNER = _owner;
name = name_;
symbol = symbol_;
decimals = decimals_;
token.init(name_);
}

modifier onlyOwner() {
if (msg.sender != OWNER) {
revert Unauthorized();
}
_;
}

/**
* @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply. Can only be called by the owner.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function mint(address account, uint256 amount) external virtual onlyOwner {
token.mint(account, amount);
}

/**
* @dev Destroys `amount` tokens from the account.
*/
function burn(address account, uint256 amount) external virtual onlyOwner {
token.burn(account, amount);
}

/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `recipient` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address recipient, uint256 amount) external virtual override returns (bool) {
return token.transfer(msg.sender, recipient, amount);
}

/**
* @dev See {IERC20-approve}.
*
* NOTE: Prefer the {increaseAllowance} and {decreaseAllowance} methods, as
* they aren't vulnerable to the frontrunning attack described here:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) external virtual override returns (bool) {
return token.approve(msg.sender, spender, amount);
}

/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* Requirements:
*
* - `sender` and `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
* - the caller must have allowance for ``sender``'s tokens of at least
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
Copy link
Collaborator

@vgeddes vgeddes Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move most of these external functions, especially the larger ones, to a common library, to save on deployment costs. Especially since this code is going to be deployed hundreds of times. It will save on bridging fees.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should just be deploying simple proxies for each token rather than a full erc20 contract each time? but it's not a big deal i guess, even hundreds of times of redeploys won't cost too much

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe we can delegate these advanced features(allowance/permit) to something like https://github.com/Uniswap/permit2

Currently, the ERC20.sol is only 4KB so may not be a big issue.

Copy link
Collaborator

@vgeddes vgeddes Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should just be deploying simple proxies for each token rather than a full erc20 contract each time? but it's not a big deal i guess, even hundreds of times of redeploys won't cost too much

Yeah, I was thinking about this yesterday actually. It seems ERC-6909 is the emerging standard for multi-token contracts (replacing ERC-1155). Its being used in Uniswap-v4 And so I was thinking Gateway can maintain a single ERC-6909 contract, and then instantiate thin ERC20 proxies over it.

I am ambivalent about it, which is why I didn't suggest it previously.

Positives:

  • Significant gas savings, though I argue that this is of lesser importance.
  • Simplified protocol as it would no longer involve agents (the contract would be owned by the gateway rather than the agent of the origin parachain).

Negatives:

  • Gav really wanted Agents to own the wrapped tokens, as its fits with his particular vision of XCM as a cross-chain protocol. So that may be a blocker.
  • ERC-6909 is fairly opinionated. For example, it doesn't support EIP-2612 signed approvals, and instead has an approved operator mechanism (similar to ERC-777)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make a decision here? maybe best to keep it simple and stick with existing standards?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am favor the current approach - just brought the idea up in case anyone had strong opinions.

@yrong lets go with the current approach, but with most of ERC20.sol factored out into a library ERC20Lib.sol

Copy link
Contributor Author

@yrong yrong Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vincent, so which functions you'd like to move to a library?

I would assume the contract size may not be a big issue here and will not make a difference for the bridging fees?

If we do want to slim the contract somehow, I'd prefer to remove all token permit/approve functions to make ERC20.sol minimal.

For all EIP-2612, EIP-1271 features we delegate to a separate contract like permit2.

so WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per this permit2 explainer, permit2 is really meant to be compatibility layer for older tokens that don't support EIP-2612. Its not meant to be a replacement for it.

And the user still has to manually approve the permit2 contract to spend the tokens. So there is an initial setup step that detracts from the UX experience.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it will take long to introduce ERC20Lib.sol. The hardest part will be moving the token storage into a struct like https://github.com/aragon/zeppelin-solidity/blob/23540364330f6b109980933c032238dd4d1308d1/contracts/token/ERC20Lib.sol#L8

After that's done, I would suggest moving _update into the library first, as its the largest function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return token.transferFrom(sender, recipient, amount);
}

/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) {
return token.increaseAllowance(spender, addedValue);
}

/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) {
return token.decreaseAllowance(spender, subtractedValue);
}

function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
{
token.permit(issuer, spender, value, deadline, v, r, s);
}

function balanceOf(address account) external view returns (uint256) {
return token.balancesOf(account);
}

function nonces(address account) external view returns (uint256) {
return token.noncesOf(account);
}

function totalSupply() external view returns (uint256) {
return token.totalSupplyOf();
}

function allowance(address owner, address spender) external view returns (uint256) {
return token.allowanceOf(owner, spender);
}

function DOMAIN_SEPARATOR() external view returns (bytes32) {
return token.domainSeparatorOf();
}
}
Loading
Loading