Skip to content

Commit eb25938

Browse files
jewei1997udpatil
authored andcommitted
ERC20 Wrapper for CW20 (#1217)
* ERC20 Wrapper for CW20 * fix gitignore * in progress
1 parent 21dd42a commit eb25938

12 files changed

+809
-2
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,7 @@ coverage.out
4949

5050
# example artifacts
5151
example/cosmwasm/echo/target
52-
example/cosmwasm/cw20/target
52+
example/cosmwasm/cw20/target
53+
54+
# Solidity contracts artifacts
55+
contracts/artifacts

contracts/hardhat.config.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require('dotenv').config({path:__dirname+'/.env'})
2+
require("@nomicfoundation/hardhat-toolbox");
3+
4+
/** @type import('hardhat/config').HardhatUserConfig */
5+
module.exports = {
6+
solidity: {
7+
version: "0.8.20",
8+
settings: {
9+
optimizer: {
10+
enabled: true,
11+
runs: 1000,
12+
},
13+
},
14+
},
15+
paths: {
16+
sources: "./src", // contracts are in ./src
17+
},
18+
networks: {
19+
goerli: {
20+
url: "https://eth-goerli.g.alchemy.com/v2/NHwLuOObixEHj3aKD4LzN5y7l21bopga", // Replace with your JSON-RPC URL
21+
address: ["0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52"],
22+
accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e"], // Replace with your private key
23+
},
24+
// sei: {
25+
// url: "https://evm-warroom-test.seinetwork.io:18545", // Replace with your JSON-RPC URL
26+
// address: ["0x07dc55085b721947d5c1645a07929eac9f1cc750"],
27+
// accounts: [process.env.TEST_PRIVATE_KEY], // Replace with your private key
28+
// },
29+
seilocal: {
30+
url: "http://127.0.0.1:8545",
31+
address: ["0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"],
32+
accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"], // Replace with your private key
33+
}
34+
},
35+
};

contracts/package.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "sei-chain",
3+
"version": "1.0.0",
4+
"description": "\"Contracts for Sei Chain\"",
5+
"main": "index.js",
6+
"directories": {
7+
"lib": "lib",
8+
"test": "test"
9+
},
10+
"scripts": {
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"author": "",
14+
"license": "ISC",
15+
"devDependencies": {
16+
"@nomicfoundation/hardhat-foundry": "^1.1.1",
17+
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
18+
"dotenv": "^16.3.1",
19+
"ethers": "^6.9.0",
20+
"hardhat": "^2.19.4"
21+
},
22+
"dependencies": {
23+
"@openzeppelin/contracts": "^5.0.1",
24+
"dotenv": "^16.3.1"
25+
}
26+
}

contracts/scripts/deploy.js

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
const hre = require("hardhat");
2+
const { exec } = require("child_process");
3+
4+
const CW20_BASE_WASM_LOCATION = "../cw20_base.wasm";
5+
6+
async function main() {
7+
let codeId = await deployWasm();
8+
console.log(`codeId: ${codeId}`);
9+
let adminAddr = await getAdmin();
10+
console.log(`adminAddr: ${adminAddr}`);
11+
let contractAddress = await instantiateWasm(codeId, adminAddr);
12+
console.log(`contractAddress: ${contractAddress}`)
13+
// deploy the CW20ERC20Wrapper solidity contract with the contractAddress passed in
14+
const [deployer] = await hre.ethers.getSigners();
15+
await fundDeployer(deployer.address);
16+
17+
console.log(
18+
"Deploying contracts with the account:",
19+
deployer.address
20+
);
21+
const CW20ERC20Wrapper = await ethers.getContractFactory("CW20ERC20Wrapper");
22+
let cW20ERC20Wrapper = await CW20ERC20Wrapper.deploy(contractAddress, "BTOK", "TOK");
23+
await cW20ERC20Wrapper.waitForDeployment()
24+
let addressToCheck = await deployer.getAddress();
25+
let balance = await cW20ERC20Wrapper.balanceOf(addressToCheck);
26+
console.log(`Balance of ${addressToCheck}: ${balance}`);
27+
}
28+
29+
async function fundDeployer(deployerAddress) {
30+
// Wrap the exec function in a Promise
31+
await new Promise((resolve, reject) => {
32+
exec(`seid tx evm send ${deployerAddress} 10000000000000000000 --from admin`, (error, stdout, stderr) => {
33+
if (error) {
34+
console.log(`error: ${error.message}`);
35+
reject(error);
36+
return;
37+
}
38+
if (stderr) {
39+
console.log(`stderr: ${stderr}`);
40+
reject(new Error(stderr));
41+
return;
42+
}
43+
resolve();
44+
});
45+
});
46+
}
47+
48+
49+
50+
async function deployWasm() {
51+
// Wrap the exec function in a Promise
52+
let codeId = await new Promise((resolve, reject) => {
53+
exec(`seid tx wasm store ${CW20_BASE_WASM_LOCATION} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block`, (error, stdout, stderr) => {
54+
if (error) {
55+
console.log(`error: ${error.message}`);
56+
reject(error);
57+
return;
58+
}
59+
if (stderr) {
60+
console.log(`stderr: ${stderr}`);
61+
reject(new Error(stderr));
62+
return;
63+
}
64+
65+
// Regular expression to find the 'code_id' value
66+
const regex = /key: code_id\s+value: "(\d+)"/;
67+
68+
// Searching for the pattern in the string
69+
const match = stdout.match(regex);
70+
71+
let cId = null;
72+
if (match && match[1]) {
73+
// The captured group is the code_id value
74+
cId = match[1];
75+
}
76+
77+
console.log(`cId: ${cId}`);
78+
resolve(cId);
79+
});
80+
});
81+
82+
return codeId;
83+
}
84+
85+
async function getAdmin() {
86+
// Wrap the exec function in a Promise
87+
let adminAddr = await new Promise((resolve, reject) => {
88+
exec(`seid keys show admin -a`, (error, stdout, stderr) => {
89+
if (error) {
90+
console.log(`error: ${error.message}`);
91+
reject(error);
92+
return;
93+
}
94+
if (stderr) {
95+
console.log(`stderr: ${stderr}`);
96+
reject(new Error(stderr));
97+
return;
98+
}
99+
resolve(stdout.trim());
100+
});
101+
});
102+
return adminAddr;
103+
}
104+
105+
async function instantiateWasm(codeId, adminAddr) {
106+
// Wrap the exec function in a Promise
107+
let contractAddress = await new Promise((resolve, reject) => {
108+
exec(`seid tx wasm instantiate ${codeId} '{ "name": "BTOK", "symbol": "BTOK", "decimals": 6, "initial_balances": [ { "address": "${adminAddr}", "amount": "1000000" } ], "mint": { "minter": "${adminAddr}", "cap": "99900000000" } }' --label cw20-test --admin ${adminAddr} --from admin --gas=5000000 --fees=1000000usei -y --broadcast-mode block`, (error, stdout, stderr) => {
109+
if (error) {
110+
console.log(`error: ${error.message}`);
111+
reject(error);
112+
return;
113+
}
114+
if (stderr) {
115+
console.log(`stderr: ${stderr}`);
116+
reject(new Error(stderr));
117+
return;
118+
}
119+
const regex = /_contract_address\s*value:\s*(\w+)/;
120+
const match = stdout.match(regex);
121+
if (match && match[1]) {
122+
resolve(match[1]);
123+
} else {
124+
reject(new Error('Contract address not found'));
125+
}
126+
});
127+
});
128+
return contractAddress;
129+
}
130+
131+
// We recommend this pattern to be able to use async/await everywhere
132+
// and properly handle errors.
133+
main().catch((error) => {
134+
console.error(error);
135+
process.exitCode = 1;
136+
});

contracts/src/CW20ERC20Wrapper.sol

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import "@openzeppelin/contracts/utils/Strings.sol";
7+
import {IWasmd} from "./precompiles/IWasmd.sol";
8+
import {IJson} from "./precompiles/IJson.sol";
9+
import {IAddr} from "./precompiles/IAddr.sol";
10+
11+
contract CW20ERC20Wrapper is ERC20 {
12+
13+
address constant WASMD_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001002;
14+
address constant JSON_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001003;
15+
address constant ADDR_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001004;
16+
17+
string public Cw20Address;
18+
IWasmd public WasmdPrecompile;
19+
IJson public JsonPrecompile;
20+
IAddr public AddrPrecompile;
21+
22+
constructor(string memory Cw20Address_, string memory name_, string memory symbol_) ERC20(name_, symbol_) {
23+
WasmdPrecompile = IWasmd(WASMD_PRECOMPILE_ADDRESS);
24+
JsonPrecompile = IJson(JSON_PRECOMPILE_ADDRESS);
25+
AddrPrecompile = IAddr(ADDR_PRECOMPILE_ADDRESS);
26+
Cw20Address = Cw20Address_;
27+
}
28+
29+
// Queries
30+
function decimals() public view override returns (uint8) {
31+
string memory req = _curlyBrace(_formatPayload("token_info", "{}"));
32+
bytes memory response = WasmdPrecompile.query(Cw20Address, bytes(req));
33+
return uint8(JsonPrecompile.extractAsUint256(response, "decimals"));
34+
}
35+
36+
function balanceOf(address owner) public view override returns (uint256) {
37+
require(owner != address(0), "ERC20: balance query for the zero address");
38+
string memory ownerAddr = _formatPayload("address", _doubleQuotes(AddrPrecompile.getSeiAddr(owner)));
39+
string memory req = _curlyBrace(_formatPayload("balance", _curlyBrace(ownerAddr)));
40+
bytes memory response = WasmdPrecompile.query(Cw20Address, bytes(req));
41+
return JsonPrecompile.extractAsUint256(response, "balance");
42+
}
43+
44+
function totalSupply() public view override returns (uint256) {
45+
string memory req = _curlyBrace(_formatPayload("token_info", "{}"));
46+
bytes memory response = WasmdPrecompile.query(Cw20Address, bytes(req));
47+
return JsonPrecompile.extractAsUint256(response, "total_supply");
48+
}
49+
50+
function allowance(address owner, address spender) public view override returns (uint256) {
51+
string memory o = _formatPayload("owner", _doubleQuotes(AddrPrecompile.getSeiAddr(owner)));
52+
string memory s = _formatPayload("spender", _doubleQuotes(AddrPrecompile.getSeiAddr(spender)));
53+
string memory req = _curlyBrace(_formatPayload("allowance", _curlyBrace(_join(o, s, ","))));
54+
bytes memory response = WasmdPrecompile.query(Cw20Address, bytes(req));
55+
return JsonPrecompile.extractAsUint256(response, "allowance");
56+
}
57+
58+
// Transactions
59+
function approve(address spender, uint256 amount) public override returns (bool) {
60+
uint256 currentAllowance = allowance(msg.sender, spender);
61+
if (currentAllowance > amount) {
62+
string memory spenderAddr = _formatPayload("spender", _doubleQuotes(AddrPrecompile.getSeiAddr(spender)));
63+
string memory amt = _formatPayload("amount", _doubleQuotes(Strings.toString(currentAllowance - amount)));
64+
string memory req = _curlyBrace(_formatPayload("decrease_allowance", _curlyBrace(_join(spenderAddr, amt, ","))));
65+
_execute(bytes(req));
66+
} else if (currentAllowance < amount) {
67+
string memory spenderAddr = _formatPayload("spender", _doubleQuotes(AddrPrecompile.getSeiAddr(spender)));
68+
string memory amt = _formatPayload("amount", _doubleQuotes(Strings.toString(amount - currentAllowance)));
69+
string memory req = _curlyBrace(_formatPayload("increase_allowance", _curlyBrace(_join(spenderAddr, amt, ","))));
70+
_execute(bytes(req));
71+
}
72+
emit Approval(msg.sender, spender, amount);
73+
return true;
74+
}
75+
76+
function transfer(address to, uint256 amount) public override returns (bool) {
77+
require(to != address(0), "ERC20: transfer to the zero address");
78+
string memory recipient = _formatPayload("recipient", _doubleQuotes(AddrPrecompile.getSeiAddr(to)));
79+
string memory amt = _formatPayload("amount", _doubleQuotes(Strings.toString(amount)));
80+
string memory req = _curlyBrace(_formatPayload("transfer", _curlyBrace(_join(recipient, amt, ","))));
81+
_execute(bytes(req));
82+
emit Transfer(msg.sender, to, amount);
83+
return true;
84+
}
85+
86+
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
87+
require(to != address(0), "ERC20: transfer to the zero address");
88+
string memory sender = _formatPayload("owner", _doubleQuotes(AddrPrecompile.getSeiAddr(from)));
89+
string memory recipient = _formatPayload("recipient", _doubleQuotes(AddrPrecompile.getSeiAddr(to)));
90+
string memory amt = _formatPayload("amount", _doubleQuotes(Strings.toString(amount)));
91+
string memory req = _curlyBrace(_formatPayload("transfer_from", _curlyBrace(_join(_join(sender, recipient, ","), amt, ","))));
92+
_execute(bytes(req));
93+
emit Transfer(from, to, amount);
94+
return true;
95+
}
96+
97+
function _execute(bytes memory req) internal returns (bytes memory) {
98+
(bool success, bytes memory ret) = WASMD_PRECOMPILE_ADDRESS.delegatecall(
99+
abi.encodeWithSignature(
100+
"execute(string,bytes,bytes)",
101+
Cw20Address,
102+
bytes(req),
103+
bytes("[]")
104+
)
105+
);
106+
require(success, "CosmWasm execute failed");
107+
return ret;
108+
}
109+
110+
function _formatPayload(string memory key, string memory value) internal pure returns (string memory) {
111+
return _join(_doubleQuotes(key), value, ":");
112+
}
113+
114+
function _curlyBrace(string memory s) internal pure returns (string memory) {
115+
return string.concat("{", string.concat(s, "}"));
116+
}
117+
118+
function _doubleQuotes(string memory s) internal pure returns (string memory) {
119+
return string.concat("\"", string.concat(s, "\""));
120+
}
121+
122+
function _join(string memory a, string memory b, string memory separator) internal pure returns (string memory) {
123+
return string.concat(a, string.concat(separator, b));
124+
}
125+
}

contracts/src/precompiles/IJson.sol

+2
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ interface IJson {
1212
function extractAsBytes(bytes memory input, string memory key) external view returns (bytes memory response);
1313

1414
function extractAsBytesList(bytes memory input, string memory key) external view returns (bytes[] memory response);
15+
16+
function extractAsUint256(bytes memory input, string memory key) external view returns (uint256 response);
1517
}

0 commit comments

Comments
 (0)