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

feat: erc20 wrapper #75

Merged
merged 6 commits into from
Oct 22, 2024
Merged
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
66 changes: 60 additions & 6 deletions app/ibc-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This module is copied from [osmosis](https://github.com/osmosis-labs/osmosis) and changed to execute evm contract with ICS-20 token transfer calls.

## Move Hooks
## EVM Hooks

The evm hook is an IBC middleware which is used to allow ICS-20 token transfers to initiate contract calls.
This allows cross-chain contract calls, that involve token movement.
Expand All @@ -12,7 +12,7 @@ One of primary importance is cross-chain swaps, which is an extremely powerful p
The mechanism enabling this is a `memo` field on every ICS20 and ICS721 transfer packet as of [IBC v3.4.0](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b).
Move hooks is an IBC middleware that parses an ICS20 transfer, and if the `memo` field is of a particular form, executes a evm contract call. We now detail the `memo` format for `evm` contract calls, and the execution guarantees provided.

### Move Contract Execution Format
### EVM Contract Execution Format

Before we dive into the IBC metadata format, we show the hook data format, so the reader has a sense of what are the fields we need to be setting in.
The evm `MsgCall` is defined [here](../../x/evm/types/tx.pb.go) and other types are defined [here](./message.go) as the following type:
Expand Down Expand Up @@ -48,6 +48,18 @@ type MsgCall struct {
ContractAddr string `protobuf:"bytes,2,opt,name=contract_addr,json=contractAddr,proto3" json:"contract_addr,omitempty"`
// Hex encoded execution input bytes.
Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"`
// Value is the amount of fee denom token to transfer to the contract.
Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
// AccessList is a predefined list of Ethereum addresses and their corresponding storage slots that a transaction will interact with during its execution. can be none
AccessList []AccessTuple `protobuf:"bytes,5,rep,name=access_list,json=accessList,proto3" json:"access_list"`
}

type AccessTuple struct {
// Address of the contract that will be accessed during the transaction execution.
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
// A list of storage keys that the transaction will interact with within the specified contract.
// These keys represent specific storage slots in the contract's storage that are accessed or modified.
StorageKeys []string `protobuf:"bytes,2,rep,name=storage_keys,json=storageKeys,proto3" json:"storage_keys,omitempty"`
}
```

Expand All @@ -71,6 +83,10 @@ msg := MsgCall{
ContractAddr: packet.data.memo["evm"]["message"]["contract_addr"],
// Hex encoded execution input bytes.
Input: packet.data.memo["evm"]["message"]["input"],
// Value is the amount of fee denom token to transfer to the contract.
Value: packet.data.memo["evm"]["message"]["value"]
// Value is the amount of fee denom token to transfer to the contract.
AccessList: packet.data.memo["evm"]["message"]["access_list"]
}
```

Expand All @@ -93,6 +109,11 @@ ICS20 is JSON native, so we use JSON for the memo format.
"message": {
"contract_addr": "0x1",
"input": "hex encoded byte string",
"value": "0",
"access_list": {
"address" : "0x1", // contract address
"storage_keys": ["0xabc","0xdef"] // storage keys of contract
}
},
// optional field to get async callback (ack and timeout)
"async_callback": {
Expand All @@ -103,15 +124,14 @@ ICS20 is JSON native, so we use JSON for the memo format.
}
}
}

```

An ICS20 packet is formatted correctly for evmhooks iff the following all hold:

- `memo` is not blank
- `memo` is valid JSON
- `memo` has at least one key, with value `"evm"`
- `memo["evm"]["message"]` has exactly five entries, `"contract_addr"` and `"input"`
- `memo["evm"]["message"]` has exactly 4 entries, `"contract_addr"`, `"input"`, `"value"`, `"access_list"`
- `receiver` == "" || `receiver` == "module_address::module_name::function_name"

We consider an ICS20 packet as directed towards evmhooks iff all of the following hold:
Expand Down Expand Up @@ -160,5 +180,39 @@ interface IIBCAsyncCallback {

Also when a contract make IBC transfer request, it should provide async callback data through memo field.

- `memo['evm']['async_callback']['id']`: the async callback id is assigned from the contract. so later it will be passed as argument of `ibc_ack` and `ibc_timeout`.
- `memo['evm']['async_callback']['contract_addr']`: The address of module which defines the callback function.
- `memo['evm']['async_callback']['id']`: the async callback id is assigned from the contract. so later it will be passed as argument of `ibc_ack` and `ibc_timeout`.
- `memo['evm']['async_callback']['contract_addr']`: The address of contract which defines the callback function.

### IBC Transfer using ERC20Wrapper

`src -> dst`: Execute the ERC20Wrapper contract to wrap and do ibc-transfer

`dst -> src`: ibc-trasfer and execute the ERC20Wrapper contract via ibc-hook

- data example

```json
{
//... other ibc fields that we don't care about
"data": {
"denom": "wrapped token denom", // will be transformed to the local denom (ibc/...)
"amount": "1000",
"sender": "addr on counterparty chain", // will be transformed
"receiver": "0xcontractaddr",
"memo": {
"evm": {
// execute message on receive packet
"message": {
"contract_addr": "0xerc20_wrapper_contract", // should query erc20 wrapper contract addr
"input": "pack(unwrap, denom, recipient, amount)", // function selector(fc078758) + abiCoder.encode([string,address,address],denom,recipient,amount) ref) https://docs.ethers.org/v6/api/abi/abi-coder/#AbiCoder-encode
"value": "0",
"access_list": {
"address" : "...", // contract address
"storage_keys": ["...","..."] // storage keys of contract
}
}
}
}
}
}
```
3 changes: 3 additions & 0 deletions proto/minievm/evm/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ message GenesisState {

// erc20 factory contract address
bytes erc20_factory = 8;

// erc20 wrapper contract address
bytes erc20_wrapper = 9;
}

// GenesisKeyValue defines store KV values.
Expand Down
17 changes: 17 additions & 0 deletions proto/minievm/evm/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ service Query {
option (google.api.http).get = "/minievm/evm/v1/contracts/erc20_factory";
}

// ERC20Wrapper gets the ERC20Wrapper contract address.
rpc ERC20Wrapper(QueryERC20WrapperRequest) returns (QueryERC20WrapperResponse) {
option (google.api.http).get = "/minievm/evm/v1/contracts/erc20_wrapper";
}

// ContractAddrByDenom gets the contract address by denom.
rpc ContractAddrByDenom(QueryContractAddrByDenomRequest) returns (QueryContractAddrByDenomResponse) {
option (google.api.http).get = "/minievm/evm/v1/contracts/by_denom";
Expand Down Expand Up @@ -94,6 +99,18 @@ message QueryERC20FactoryResponse {
string address = 1;
}

// QueryERC20WrapperRequest is the request type for the Query/ERC20Wrapper RPC
// method
message QueryERC20WrapperRequest {}

// QueryERC20WrapperResponse is the response type for the Query/ERC20Wrapper RPC
// method
message QueryERC20WrapperResponse {
option (gogoproto.equal) = true;
// 0x prefixed hex address
string address = 1;
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

// QueryContractAddrByDenomRequest is the request type for the Query/ContractAddrByDenom RPC
// method
message QueryContractAddrByDenomRequest {
Expand Down
790 changes: 790 additions & 0 deletions x/evm/contracts/erc20_wrapper/ERC20Wrapper.go

Large diffs are not rendered by default.

203 changes: 203 additions & 0 deletions x/evm/contracts/erc20_wrapper/ERC20Wrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "../util/Strings.sol";
import "../i_cosmos/ICosmos.sol";
import "../erc20_factory/ERC20Factory.sol";
import "../i_erc20/IERC20.sol";
import "../ownable/Ownable.sol";
import "../erc20_registry/ERC20Registry.sol";
import "../erc20_acl/ERC20ACL.sol";
import {ERC165, IERC165} from "../erc165/ERC165.sol";

contract ERC20Wrapper is
Ownable,
ERC20Registry,
ERC165,
ERC20ACL,
ERC20Factory
{
struct IbcCallBack {
address sender;
string wrappedTokenDenom;
uint wrappedAmt;
}

uint8 constant WRAPPED_DECIMAL = 6;
string constant NAME_PREFIX = "Wrapped";
string constant SYMBOL_PREFIX = "W";
uint64 callBackId = 0;
ERC20Factory public immutable factory;
mapping(address => address) public wrappedTokens; // origin -> wrapped
mapping(address => address) public originTokens; // wrapped -> origin
mapping(uint64 => IbcCallBack) private ibcCallBack; // id -> CallBackInfo

constructor(address erc20Factory) {
factory = ERC20Factory(erc20Factory);
}

/**
* @notice This function wraps the tokens and transfer the tokens by ibc transfer
* @dev A sender requires sender approve to this contract to transfer the tokens.
*/
function wrap(
string memory channel,
address token,
string memory receiver,
uint amount,
uint timeout
) public {
_ensureWrappedTokenExists(token);

// lock origin token
IERC20(token).transferFrom(msg.sender, address(this), amount);
djm07073 marked this conversation as resolved.
Show resolved Hide resolved
uint wrappedAmt = _convertDecimal(
amount,
IERC20(token).decimals(),
WRAPPED_DECIMAL
);
// mint wrapped token
ERC20(wrappedTokens[token]).mint(address(this), wrappedAmt);

callBackId += 1;

// store the callback data
ibcCallBack[callBackId] = IbcCallBack({
sender: msg.sender,
wrappedTokenDenom: COSMOS_CONTRACT.to_denom(wrappedTokens[token]),
wrappedAmt: wrappedAmt
});

// do ibc transfer wrapped token
COSMOS_CONTRACT.execute_cosmos(
_ibc_transfer(
channel,
wrappedTokens[token],
wrappedAmt,
timeout,
receiver
)
);
}
djm07073 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice This function is executed as an IBC hook to unwrap the wrapped tokens.
* @dev This function is used by a hook and requires sender approve to this contract to burn wrapped tokens.
*/
function unwrap(
string memory wrappedTokenDenom,
address receiver,
uint wrappedAmt
) public {
address wrappedToken = COSMOS_CONTRACT.to_erc20(wrappedTokenDenom);
require(
originTokens[wrappedToken] != address(0),
"origin token doesn't exist"
);

// burn wrapped token
ERC20(wrappedToken).burnFrom(msg.sender, wrappedAmt);

// unlock origin token and transfer to receiver
uint amount = _convertDecimal(
wrappedAmt,
WRAPPED_DECIMAL,
IERC20(originTokens[wrappedToken]).decimals()
);

ERC20(originTokens[wrappedToken]).transfer(receiver, amount);
djm07073 marked this conversation as resolved.
Show resolved Hide resolved
}
djm07073 marked this conversation as resolved.
Show resolved Hide resolved

function ibc_ack(uint64 callback_id, bool success) external {
if (success) {
return;
}
_handleFailedIbcTransfer(callback_id);
}

function ibc_timeout(uint64 callback_id) external {
_handleFailedIbcTransfer(callback_id);
}
djm07073 marked this conversation as resolved.
Show resolved Hide resolved

// internal functions //

function _handleFailedIbcTransfer(uint64 callback_id) internal {
IbcCallBack memory callback = ibcCallBack[callback_id];
unwrap(
callback.wrappedTokenDenom,
callback.sender,
callback.wrappedAmt
);
}
djm07073 marked this conversation as resolved.
Show resolved Hide resolved
djm07073 marked this conversation as resolved.
Show resolved Hide resolved

function _ensureWrappedTokenExists(address token) internal {
if (wrappedTokens[token] == address(0)) {
address wrappedToken = factory.createERC20(
string.concat(NAME_PREFIX, IERC20(token).name()),
string.concat(SYMBOL_PREFIX, IERC20(token).symbol()),
WRAPPED_DECIMAL
);
wrappedTokens[token] = wrappedToken;
originTokens[wrappedToken] = token;
}
}

function _convertDecimal(
uint amount,
uint8 decimal,
uint8 newDecimal
) internal pure returns (uint convertedAmount) {
if (decimal > newDecimal) {
uint factor = 10 ** uint(decimal - newDecimal);
convertedAmount = amount / factor;
} else if (decimal < newDecimal) {
uint factor = 10 ** uint(newDecimal - decimal);
convertedAmount = amount * factor;
} else {
convertedAmount = amount;
}
require(convertedAmount != 0, "converted amount is zero");
}
djm07073 marked this conversation as resolved.
Show resolved Hide resolved

function _ibc_transfer(
string memory channel,
address token,
uint amount,
uint timeout,
string memory receiver
) internal returns (string memory message) {
// Construct the IBC transfer message
message = string(
abi.encodePacked(
'{"@type": "/ibc.applications.transfer.v1.MsgTransfer",',
'"source_port": "transfer",',
'"source_channel": "',
channel,
'",',
'"token": { "denom": "',
COSMOS_CONTRACT.to_denom(token),
'",',
'"amount": "',
Strings.toString(amount),
'"},',
'"sender": "',
COSMOS_CONTRACT.to_cosmos_address(address(this)),
'",',
'"receiver": "',
receiver,
'",',
'"timeout_height": {"revision_number": "0","revision_height": "0"},',
'"timeout_timestamp": "',
Strings.toString(timeout),
'",',
'"memo": "",',
'"async_callback": {"id": "',
Strings.toString(callBackId),
'",',
'"contract_address": "',
Strings.toHexString(address(this)),
'"}}'
)
);
}
djm07073 marked this conversation as resolved.
Show resolved Hide resolved
}
Loading
Loading