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: map ics20 denoms to erc20 #23

Merged
merged 10 commits into from
Sep 25, 2023
106 changes: 106 additions & 0 deletions contracts/apps/20-transfer/ICS20TransferERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.9;

import "./ICS20Transfer.sol";
import "../../core/25-handler/IBCHandler.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";

// An ICS20 implementation that maps sink denoms to ERC20 contracts that's deployed and managed by this contract.
//
// Source denom is interpreted as ERC20 contract address in hex with the 0x prefix.
contract ICS20TransferERC20 is ICS20Transfer {
// Map sink denom to ERC20.
mapping(string => ERC20PresetMinterPauser) public denomTokenContract;

constructor(IBCHandler ibcHandler_) ICS20Transfer(ibcHandler_) {
}

function _transferFrom(address sender, address receiver, string memory denom, uint256 amount)
internal
override
returns (bool)
{
IERC20 tokenContract = IERC20(parseAddr(denom));
// transferFrom returns a bool but it may also revert.
try tokenContract.transferFrom(sender, receiver, amount) returns (bool succeeded) {
return succeeded;
} catch (bytes memory) {
return false;
}
}

function _mint(address account, string memory denom, uint256 amount) internal virtual override returns (bool) {
// Deploy an ERC20 contract for each (sink zone) denom seen.
if (address(denomTokenContract[denom]) == address(0)) {
string memory name = string.concat("IBC/", hexEncode(abi.encodePacked(sha256(bytes(denom)))));
denomTokenContract[denom] = new ERC20PresetMinterPauser(name, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

how to associate this non-human-readable token name with a human-readable token name, like ETC, BTC, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a concern for UI/explorer etc. Cosmos uses https://github.com/cosmos/chain-registry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or the allowlist version allows setting custom token name, symbol and decimal.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do not set symbol for the ERC20?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What should the symbol be?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am no sure if we can use denom as the symbol, but use an empty string is strange

Copy link
Contributor

@ashuralyk ashuralyk Sep 23, 2023

Choose a reason for hiding this comment

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

I am no sure if we can use denom as the symbol, but use an empty string is strange

it's not an empty string, but just an encoded string which isn't recognized for human-beings and differs from symbol of ERC20, like BTC, ETH, and so on

Copy link
Collaborator

@jjyr jjyr Sep 25, 2023

Choose a reason for hiding this comment

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

I understand the first arg.

            denomTokenContract[denom] = new ERC20PresetMinterPauser(name, "");

I am discussing the second one here

}
denomTokenContract[denom].mint(account, amount);
return true;
}

function _burn(address account, string memory denom, uint256 amount) internal override returns (bool) {
if (address(denomTokenContract[denom]) == address(0)) {
return false;
}
try denomTokenContract[denom].burnFrom(account, amount) {
return true;
} catch (bytes memory) {
return false;
}
}

function hexEncode(bytes memory buffer) internal pure returns (string memory) {
jjyr marked this conversation as resolved.
Show resolved Hide resolved
// Fixed buffer size for hexadecimal convertion
bytes memory converted = new bytes(buffer.length * 2);

bytes memory _base = "0123456789ABCDEF";

for (uint256 i = 0; i < buffer.length; i++) {
converted[i * 2] = _base[uint8(buffer[i]) / _base.length];
converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length];
}

return string(converted);
}

// a copy from https://github.com/provable-things/ethereum-api/blob/161552ebd4f77090d86482cff8c863cf903c6f5f/oraclizeAPI_0.6.sol
blckngm marked this conversation as resolved.
Show resolved Hide resolved
function parseAddr(string memory _a) internal pure returns (address _parsedAddress) {
bytes memory tmp = bytes(_a);
uint160 iaddr = 0;
uint160 b1;
uint160 b2;
for (uint256 i = 2; i < 2 + 2 * 20; i += 2) {
iaddr *= 256;
b1 = uint160(uint8(tmp[i]));
b2 = uint160(uint8(tmp[i + 1]));
if ((b1 >= 97) && (b1 <= 102)) {
b1 -= 87;
} else if ((b1 >= 65) && (b1 <= 70)) {
b1 -= 55;
} else if ((b1 >= 48) && (b1 <= 57)) {
b1 -= 48;
}
if ((b2 >= 97) && (b2 <= 102)) {
b2 -= 87;
} else if ((b2 >= 65) && (b2 <= 70)) {
b2 -= 55;
} else if ((b2 >= 48) && (b2 <= 57)) {
b2 -= 48;
}
iaddr += (b1 * 16 + b2);
}
return address(iaddr);
}
}

// Make external wrappers for testing.
contract ICS20TransferERC20Test is ICS20TransferERC20 {
constructor(IBCHandler ibcHandler_) ICS20TransferERC20(ibcHandler_) {
}

function mint(address account, string memory denom, uint256 amount) external {
require(_mint(account, denom, amount));
}
}
63 changes: 63 additions & 0 deletions contracts/apps/20-transfer/ICS20TransferERC20Allowlist.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.9;

import "./ICS20TransferERC20.sol";
import "../../core/25-handler/IBCHandler.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";

// An ICS20 implementation that maps sink denoms to administrator designated ERC20 contracts.
//
// Source denom is interpreted as ERC20 contract address in hex with the 0x prefix.
contract ICS20TransferERC20Allowlist is ICS20TransferERC20, AccessControl {
constructor(IBCHandler ibcHandler_) ICS20TransferERC20(ibcHandler_) {
_setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
blckngm marked this conversation as resolved.
Show resolved Hide resolved
}

function setDenomTokenContract(string calldata denom, ERC20PresetMinterPauser tokenContract) onlyRole(DEFAULT_ADMIN_ROLE) external {
require(tokenContract.hasRole(tokenContract.MINTER_ROLE(), address(this)));
denomTokenContract[denom] = tokenContract;
}

function _mint(address account, string memory denom, uint256 amount) internal override returns (bool) {
if (address(denomTokenContract[denom]) == address(0)) {
return false;
}
try denomTokenContract[denom].mint(account, amount) {
return true;
} catch (bytes memory) {
return false;
}
}
}

// Make external wrappers for testing.
contract ICS20TransferERC20AllowlistTest is ICS20TransferERC20Allowlist {
constructor(IBCHandler ibcHandler_) ICS20TransferERC20Allowlist(ibcHandler_) {
}

function transferFrom(address sender, address receiver, string memory denom, uint256 amount) external {
require(_transferFrom(sender, receiver, denom, amount));
}

function transferFromShouldFail(address sender, address receiver, string memory denom, uint256 amount) external {
require(!_transferFrom(sender, receiver, denom, amount));
}

function mint(address account, string memory denom, uint256 amount) external {
require(_mint(account, denom, amount));
}

function mintShouldFail(address account, string memory denom, uint256 amount) external {
require(!_mint(account, denom, amount));
}

function burn(address account, string memory denom, uint256 amount) external {
require(_burn(account, denom, amount));
}

function burnShouldFail(address account, string memory denom, uint256 amount) external {
require(!_burn(account, denom, amount));
}
}
4 changes: 4 additions & 0 deletions migrations/1_deploy_contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ module.exports = async function (deployer, network) {
packetAddress
);
const mockTransferAddress = await deployContract("MockTransfer", ibcAddress);
const transferAddress = await deployContract("ICS20TransferERC20", ibcAddress);
const mockClient = await deployContract("MockClient");
const ibcHandler = await IBCHandler.at(ibcAddress);

Expand All @@ -83,6 +84,9 @@ module.exports = async function (deployer, network) {
// Register Module (optional, just for the cooperation of test on Axon endpoint)
await ibcHandler.bindPort("port-0", mockTransferAddress);
console.log("Register Mock Transfer: port-0");

await ibcHandler.bindPort("transfer", transferAddress);
console.log("Register Transfer: transfer");
}
};

Expand Down
60 changes: 60 additions & 0 deletions scripts/check_balance_and_send_back.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This is part of ckb <-> axon SUDT <-> ERC20 transfer test.

async function main() {
// First provider account.
const [receiver] = await web3.eth.getAccounts();
console.log("Receiver should be:", receiver);
const sender = process.env.SENDER;

const ICS20TransferERC20 = await artifacts.require("ICS20TransferERC20");
const IERC20 = await artifacts.require("IERC20");

const transfer = await ICS20TransferERC20.at(
process.env.TRANSFER_CONTRACT_ADDRESS
);

const port = "transfer";
const channel = process.env.CHANNEL;

const denom = `${port}/${channel}/${process.env.DENOM}`;

// Wait till the token is deployed.
let tokenAddr;
while (true) {
tokenAddr = await transfer.denomTokenContract(denom);
if (tokenAddr == "0x0000000000000000000000000000000000000000") {
console.log("token not deployed yet");
await sleep(1000);
continue;
} else {
break;
}
}

// Check balance.
const token = await IERC20.at(tokenAddr);
if ((await token.balanceOf(receiver)) != 999) {
throw "balance should be 999";
}

// Send back: ERC20 approve and ICS20 sendTransfer.
await token.approve(transfer.address, 499, {
from: receiver,
});
await transfer.sendTransfer(denom, 499, sender, port, channel, 0, {
from: receiver,
});
}

module.exports = (callback) => {
main().then(callback).catch(e => {
console.log("Error:", e.message, e);
callback();
});
};

function sleep(millis) {
return new Promise((resolve) => {
setTimeout(resolve, millis);
});
}
26 changes: 26 additions & 0 deletions test/ICS20TransferERC20.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const IBCHandler = artifacts.require("IBCMockHandler");
const ICS20TransferERC20 = artifacts.require("ICS20TransferERC20Test");
const ERC20PresetMinterPauser = artifacts.require("ERC20PresetMinterPauser");

contract("ICS20TransferERC20", ([account]) => {
it("should be able to mint ERC20", async () => {
const ibcHandler = await IBCHandler.deployed();
const transfer = await ICS20TransferERC20.new(ibcHandler.address);

const denom = "/port-2/transfer-8/MY-TOKEN-TYPE-SCRIPT-HASH";

await transfer.mint(account, denom, 100);
await transfer.mint(account, denom, 51);
const myToken = await ERC20PresetMinterPauser.at(
await transfer.denomTokenContract(denom)
);

assert.equal(
await myToken.name(),
"IBC/A74473C8545D36443C16874E2A336A00016EF1C0EA489CE552A76EE1709CE50D"
);
assert.equal(await myToken.balanceOf(account), 151);
assert.equal(await myToken.totalSupply(), 151);
});
// Other functions are tested in ICS20TransferERC20Allowlist
});
52 changes: 52 additions & 0 deletions test/ICS20TransferERC20Allowlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const IBCHandler = artifacts.require("IBCMockHandler");
const ICS20TransferERC20Allowlist = artifacts.require(
"ICS20TransferERC20AllowlistTest"
);
const ERC20PresetMinterPauser = artifacts.require("ERC20PresetMinterPauser");

contract("ICS20TransferERC20Allowlist", ([account]) => {
it("should be able to mint/burn ERC20", async () => {
const ibcHandler = await IBCHandler.deployed();
const transfer = await ICS20TransferERC20Allowlist.new(ibcHandler.address);
const myToken = await ERC20PresetMinterPauser.new("MyToken", "MT");

const denom = "/port-2/transfer-8/MY-TOKEN-TYPE-SCRIPT-HASH";

await myToken.grantRole(await myToken.MINTER_ROLE(), transfer.address);
await transfer.setDenomTokenContract(denom, myToken.address);

await transfer.mintShouldFail(account, "/port/channel/unknown-denom", 100);
await transfer.mint(account, denom, 100);

assert.equal(await myToken.balanceOf(account), 100);
assert.equal(await myToken.totalSupply(), 100);

await transfer.burnShouldFail(account, "/port/channel/unknown-denom", 49);
// Burn without allowance should fail.
await transfer.burnShouldFail(account, denom, 49);

await myToken.approve(transfer.address, 49, { from: account });
await transfer.burn(account, denom, 49);

assert.equal(await myToken.balanceOf(account), 51);
assert.equal(await myToken.totalSupply(), 51);
});

it("should be able to transfer ERC20", async () => {
const ibcHandler = await IBCHandler.deployed();
const transfer = await ICS20TransferERC20Allowlist.new(ibcHandler.address);
const myToken = await ERC20PresetMinterPauser.new("MyToken", "MT");

await myToken.mint(account, 100);
// Transfer without allowance should fail.
await transfer.transferFromShouldFail(
account,
transfer.address,
myToken.address,
51
);
await myToken.approve(transfer.address, 51, { from: account });
await transfer.transferFrom(account, transfer.address, myToken.address, 51);
assert.equal(await myToken.balanceOf(account), 49);
});
});