-
Notifications
You must be signed in to change notification settings - Fork 4
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
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
eb0804b
feat: map ics20 denoms to erc20
blckngm 54a22f8
Unit test ICS20TransferERC20
blckngm 916cd03
Rename to ICS20TransferERC20Allowlist
blckngm 9551368
Support source ERC20
blckngm 6b8cfe0
Add permissionless ICS20TransferERC20
blckngm ad46062
ICS20TransferERC20Allowlist inherits ICS20TransferERC20
blckngm 567f006
Deploy ICS20TransferERC20 and bind to port transfer
blckngm fe226b2
Add script for transfer test
blckngm 4116ee3
Update contracts/apps/20-transfer/ICS20TransferERC20.sol
blckngm aad24b1
Use _grantRole insteadof _setupRole
blckngm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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, ""); | ||
} | ||
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)); | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
contracts/apps/20-transfer/ICS20TransferERC20Allowlist.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.9; | ||
|
||
import "./ICS20Transfer.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 ICS20Transfer, AccessControl { | ||
// Map sink denom to ERC20. | ||
mapping(string => ERC20PresetMinterPauser) public denomTokenContract; | ||
|
||
constructor(IBCHandler ibcHandler_) ICS20Transfer(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 _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 override returns (bool) { | ||
if (address(denomTokenContract[denom]) == address(0)) { | ||
return false; | ||
} | ||
try denomTokenContract[denom].mint(account, amount) { | ||
return true; | ||
} catch (bytes memory) { | ||
return false; | ||
} | ||
} | ||
|
||
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; | ||
} | ||
} | ||
|
||
// a copy from https://github.com/provable-things/ethereum-api/blob/161552ebd4f77090d86482cff8c863cf903c6f5f/oraclizeAPI_0.6.sol | ||
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 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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
const IBCHandler = artifacts.require("IBCMockHandler"); | ||
const ICS20TransferERC20 = artifacts.require("ICS20TransferERC20Test"); | ||
const ERC20PresetMinterPauser = artifacts.require("ERC20PresetMinterPauser"); | ||
|
||
contract("ICS20TransferERC20", ([account]) => { | ||
it("should be able to mint/burn 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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 strangeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
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.
I am discussing the second one here