mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-20 15:05:21 +00:00
feat: add contract for ERC20KavaWrappedNativeCoin (#1594)
* setup empty hardhat project for evm contract dev * setup eslint * setup prettier * setup solhint * ignore contracts dir in docker * add ERC20KavaWrappedNativeCoin contract * add unit tests for ERC20KavaWrappedNativeCoin * use solidity 0.8.18 * configure solc with optimization and evm target * compile ERC20KavaWrappedNativeCoin for evmutil * setup script for deploying directly to a network * fix burn test for ERC20KavaWrappedNativeCoin Co-authored-by: drklee3 <derrick@dlee.dev> --------- Co-authored-by: drklee3 <derrick@dlee.dev>
This commit is contained in:
parent
ff709d73e1
commit
6da31bd662
@ -3,6 +3,7 @@ out/
|
|||||||
.git/
|
.git/
|
||||||
docs/
|
docs/
|
||||||
tests/
|
tests/
|
||||||
|
contracts/
|
||||||
|
|
||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
@ -1 +1,2 @@
|
|||||||
golang 1.20
|
golang 1.20
|
||||||
|
nodejs 18.16.0
|
||||||
|
4
contracts/.eslintignore
Normal file
4
contracts/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
artifacts
|
||||||
|
cache
|
||||||
|
coverage
|
10
contracts/.eslintrc.cjs
Normal file
10
contracts/.eslintrc.cjs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
root: true,
|
||||||
|
};
|
11
contracts/.gitignore
vendored
Normal file
11
contracts/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
coverage
|
||||||
|
coverage.json
|
||||||
|
typechain
|
||||||
|
typechain-types
|
||||||
|
|
||||||
|
# Hardhat files
|
||||||
|
cache
|
||||||
|
artifacts
|
||||||
|
|
6
contracts/.prettierignore
Normal file
6
contracts/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
artifacts
|
||||||
|
cache
|
||||||
|
coverage*
|
7
contracts/.solhint.json
Normal file
7
contracts/.solhint.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "solhint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"compiler-version": ["error", "^0.8.0"],
|
||||||
|
"func-visibility": ["warn", { "ignoreConstructors": true }]
|
||||||
|
}
|
||||||
|
}
|
1
contracts/.solhintignore
Normal file
1
contracts/.solhintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
45
contracts/README.md
Normal file
45
contracts/README.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Kava EVM contracts
|
||||||
|
|
||||||
|
Contracts for the Kava EVM used by the Kava blockchain.
|
||||||
|
Includes an ERC20 contract for wrapping native cosmos sdk.Coins.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```
|
||||||
|
# Watch contract + tests
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Watch tests only
|
||||||
|
npm run test-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying contracts to test networks
|
||||||
|
|
||||||
|
A deploy script is included in this hardhat project to deploy a contract directly to a network.
|
||||||
|
To deploy the contracts to different networks:
|
||||||
|
```
|
||||||
|
npx hardhat run --network <network-name> scripts/deploy.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration for various `<network-name>`s above are setup in the [hardhat config](./hardhat.config.ts).
|
||||||
|
|
||||||
|
## Production compiling & Ethermint JSON
|
||||||
|
|
||||||
|
Ethermint uses its own json format that includes the ABI and bytecode in a single file. The bytecode should have no `0x` prefix and should be under the property name `bin`. This structure is built from the compiled code with `npm ethermint-json`.
|
||||||
|
|
||||||
|
The following compiles the contract, builds the ethermint json and copies the file to the evmutil:
|
||||||
|
```
|
||||||
|
npm build
|
||||||
|
```
|
46
contracts/contracts/ERC20KavaWrappedNativeCoin.sol
Normal file
46
contracts/contracts/ERC20KavaWrappedNativeCoin.sol
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.18;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||||
|
|
||||||
|
/// @title An ERC20 token contract owned and deployed by the evmutil module of Kava.
|
||||||
|
/// Tokens are backed one-for-one by sdk coins held in the module account.
|
||||||
|
/// @author Kava Labs, LLC
|
||||||
|
/// @custom:security-contact security@kava.io
|
||||||
|
contract ERC20KavaWrappedNativeCoin is ERC20, Ownable {
|
||||||
|
/// @notice The decimals places of the token. For display purposes only.
|
||||||
|
uint8 private immutable _decimals;
|
||||||
|
|
||||||
|
/// @notice Registers the ERC20 token with mint and burn permissions for the
|
||||||
|
/// contract owner, by default the account that deploys this contract.
|
||||||
|
/// @param name The name of the ERC20 token.
|
||||||
|
/// @param symbol The symbol of the ERC20 token.
|
||||||
|
/// @param decimals_ The number of decimals of the ERC20 token.
|
||||||
|
constructor(
|
||||||
|
string memory name,
|
||||||
|
string memory symbol,
|
||||||
|
uint8 decimals_
|
||||||
|
) ERC20(name, symbol) {
|
||||||
|
_decimals = decimals_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Query the decimal places of the token for display purposes.
|
||||||
|
function decimals() public view override returns (uint8) {
|
||||||
|
return _decimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Mints new tokens to an address. Can only be called by token owner.
|
||||||
|
/// @param to Address to which new tokens are minted.
|
||||||
|
/// @param amount Number of tokens to mint.
|
||||||
|
function mint(address to, uint256 amount) public onlyOwner {
|
||||||
|
_mint(to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burns tokens from an address. Can only be called by token owner.
|
||||||
|
/// @param from Address from which tokens are burned.
|
||||||
|
/// @param amount Number of tokens to burn.
|
||||||
|
function burn(address from, uint256 amount) public onlyOwner {
|
||||||
|
_burn(from, amount);
|
||||||
|
}
|
||||||
|
}
|
41
contracts/hardhat.config.ts
Normal file
41
contracts/hardhat.config.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { HardhatUserConfig } from "hardhat/config";
|
||||||
|
import "@nomicfoundation/hardhat-toolbox";
|
||||||
|
|
||||||
|
const config: HardhatUserConfig = {
|
||||||
|
solidity: {
|
||||||
|
version: "0.8.18",
|
||||||
|
settings: {
|
||||||
|
// istanbul upgrade occurred before the london hardfork, so is compatible with kava's evm
|
||||||
|
evmVersion: "istanbul",
|
||||||
|
// optimize build for deployment to mainnet!
|
||||||
|
optimizer: {
|
||||||
|
enabled: true,
|
||||||
|
runs: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
// kvtool's local network
|
||||||
|
kava: {
|
||||||
|
url: "http://127.0.0.1:8545",
|
||||||
|
accounts: [
|
||||||
|
// kava keys unsafe-export-eth-key whale2
|
||||||
|
"AA50F4C6C15190D9E18BF8B14FC09BFBA0E7306331A4F232D10A77C2879E7966",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
protonet: {
|
||||||
|
url: "https://evm.app.protonet.us-east.production.kava.io:443",
|
||||||
|
accounts: [
|
||||||
|
"247069F0BC3A5914CB2FD41E4133BBDAA6DBED9F47A01B9F110B5602C6E4CDD9",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
internal_testnet: {
|
||||||
|
url: "https://evm.data.internal.testnet.us-east.production.kava.io:443",
|
||||||
|
accounts: [
|
||||||
|
"247069F0BC3A5914CB2FD41E4133BBDAA6DBED9F47A01B9F110B5602C6E4CDD9",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
10827
contracts/package-lock.json
generated
Normal file
10827
contracts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
contracts/package.json
Normal file
39
contracts/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "kava-contracts",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": "Kava Labs",
|
||||||
|
"private": true,
|
||||||
|
"description": "Solidity contracts for Kava Blockchain",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run clean && npm run compile && npm run ethermint-json",
|
||||||
|
"clean": "hardhat clean",
|
||||||
|
"compile": "hardhat compile",
|
||||||
|
"coverage": "hardhat coverage",
|
||||||
|
"ethermint-json": "jq '{ abi: .abi | tostring, bin: .bytecode | ltrimstr(\"0x\")}' artifacts/contracts/ERC20KavaWrappedNativeCoin.sol/ERC20KavaWrappedNativeCoin.json > ../x/evmutil/types/ethermint_json/ERC20KavaWrappedNativeCoin.json",
|
||||||
|
"gen-ts-types": "hardhat typechain",
|
||||||
|
"lint": "eslint '**/*.{js,ts}'",
|
||||||
|
"lint-fix": "eslint '**/*.{js,ts}' --fix",
|
||||||
|
"prettier": "prettier '**/*.{json,sol,md}' --check",
|
||||||
|
"prettier-fix": "prettier '**/*.{json,sol,md}' --write",
|
||||||
|
"solhint": "solhint 'contracts/**/*.sol' --max-warnings 0",
|
||||||
|
"solhint-fix": "solhint 'contracts/**/*.sol' --fix",
|
||||||
|
"test": "hardhat test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nomicfoundation/hardhat-toolbox": "^2.0.2",
|
||||||
|
"@openzeppelin/contracts": "4.8.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||||
|
"@typescript-eslint/parser": "^5.59.6",
|
||||||
|
"eslint": "^8.40.0",
|
||||||
|
"eslint-config-prettier": "8.8.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"hardhat": "^2.14.0",
|
||||||
|
"prettier": "2.8.8",
|
||||||
|
"prettier-plugin-solidity": "^1.1.3",
|
||||||
|
"solhint": "^3.4.1",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
}
|
||||||
|
}
|
27
contracts/scripts/deploy.ts
Normal file
27
contracts/scripts/deploy.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ethers } from "hardhat";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const tokenName = "Kava-wrapped ATOM";
|
||||||
|
const tokenSymbol = "kATOM";
|
||||||
|
const tokenDecimals = 6;
|
||||||
|
|
||||||
|
const ERC20KavaWrappedNativeCoin = await ethers.getContractFactory(
|
||||||
|
"ERC20KavaWrappedNativeCoin"
|
||||||
|
);
|
||||||
|
const token = await ERC20KavaWrappedNativeCoin.deploy(
|
||||||
|
tokenName,
|
||||||
|
tokenSymbol,
|
||||||
|
tokenDecimals
|
||||||
|
);
|
||||||
|
|
||||||
|
await token.deployed();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Token "${tokenName}" (${tokenSymbol}) with ${tokenDecimals} decimals is deployed to ${token.address}!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
116
contracts/test/ERC20KavaWrappedNativeCoin.test.ts
Normal file
116
contracts/test/ERC20KavaWrappedNativeCoin.test.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
import { Signer } from "ethers";
|
||||||
|
import { ethers } from "hardhat";
|
||||||
|
import {
|
||||||
|
ERC20KavaWrappedNativeCoin,
|
||||||
|
ERC20KavaWrappedNativeCoin__factory as ERC20KavaWrappedNativeCoinFactory,
|
||||||
|
} from "../typechain-types";
|
||||||
|
|
||||||
|
const decimals = 6n;
|
||||||
|
|
||||||
|
describe("ERC20KavaWrappedNativeCoin", function () {
|
||||||
|
let erc20: ERC20KavaWrappedNativeCoin;
|
||||||
|
let erc20Factory: ERC20KavaWrappedNativeCoinFactory;
|
||||||
|
let owner: Signer;
|
||||||
|
let sender: Signer;
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
erc20Factory = await ethers.getContractFactory(
|
||||||
|
"ERC20KavaWrappedNativeCoin"
|
||||||
|
);
|
||||||
|
erc20 = await erc20Factory.deploy("Wrapped ATOM", "ATOM", decimals);
|
||||||
|
[owner, sender] = await ethers.getSigners();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decimals", function () {
|
||||||
|
it("should be the same as deployed", async function () {
|
||||||
|
expect(await erc20.decimals()).to.be.equal(decimals);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mint", function () {
|
||||||
|
it("should reject non-owner", async function () {
|
||||||
|
const tx = erc20.connect(sender).mint(await sender.getAddress(), 10n);
|
||||||
|
await expect(tx).to.be.revertedWith("Ownable: caller is not the owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be callable by owner", async function () {
|
||||||
|
const amount = 10n;
|
||||||
|
|
||||||
|
const tx = erc20.connect(owner).mint(await sender.getAddress(), amount);
|
||||||
|
await expect(tx).to.not.be.reverted;
|
||||||
|
|
||||||
|
const bal = await erc20.balanceOf(await sender.getAddress());
|
||||||
|
expect(bal).to.equal(amount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("burn", function () {
|
||||||
|
it("should reject non-owner", async function () {
|
||||||
|
const tx = erc20.connect(sender).burn(await sender.getAddress(), 10n);
|
||||||
|
await expect(tx).to.be.revertedWith("Ownable: caller is not the owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should let owner burn some of the tokens", async function () {
|
||||||
|
const amount = 10n;
|
||||||
|
|
||||||
|
// give tokens to an account
|
||||||
|
const fundTx = erc20
|
||||||
|
.connect(owner)
|
||||||
|
.mint(await sender.getAddress(), amount);
|
||||||
|
await expect(fundTx).to.not.be.reverted;
|
||||||
|
const balBefore = await erc20.balanceOf(await sender.getAddress());
|
||||||
|
expect(balBefore).to.equal(amount);
|
||||||
|
|
||||||
|
// burn the tokens!
|
||||||
|
const burnTx = erc20
|
||||||
|
.connect(owner)
|
||||||
|
.burn(await sender.getAddress(), amount);
|
||||||
|
await expect(burnTx).to.not.be.reverted;
|
||||||
|
const balAfter = await erc20.balanceOf(await sender.getAddress());
|
||||||
|
expect(balAfter).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should let owner burn all the tokens", async function () {
|
||||||
|
const amount = 10n;
|
||||||
|
|
||||||
|
// give tokens to an account
|
||||||
|
const fundTx = erc20
|
||||||
|
.connect(owner)
|
||||||
|
.mint(await sender.getAddress(), amount);
|
||||||
|
await expect(fundTx).to.not.be.reverted;
|
||||||
|
const balBefore = await erc20.balanceOf(await sender.getAddress());
|
||||||
|
expect(balBefore).to.equal(amount);
|
||||||
|
|
||||||
|
// burn the tokens!
|
||||||
|
const burnAmount = 7n;
|
||||||
|
const burnTx = erc20
|
||||||
|
.connect(owner)
|
||||||
|
.burn(await sender.getAddress(), burnAmount);
|
||||||
|
await expect(burnTx).to.not.be.reverted;
|
||||||
|
const balAfter = await erc20.balanceOf(await sender.getAddress());
|
||||||
|
expect(balAfter).to.equal(amount - burnAmount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error when trying to burn more than balance", async function () {
|
||||||
|
const amount = 10n;
|
||||||
|
|
||||||
|
// give tokens to an account
|
||||||
|
const fundTx = erc20
|
||||||
|
.connect(owner)
|
||||||
|
.mint(await sender.getAddress(), amount);
|
||||||
|
await expect(fundTx).to.not.be.reverted;
|
||||||
|
const balBefore = await erc20.balanceOf(await sender.getAddress());
|
||||||
|
expect(balBefore).to.equal(amount);
|
||||||
|
|
||||||
|
// burn the tokens!
|
||||||
|
const burnAmount = amount + 1n;
|
||||||
|
const burnTx = erc20
|
||||||
|
.connect(owner)
|
||||||
|
.burn(await sender.getAddress(), burnAmount);
|
||||||
|
await expect(burnTx).to.be.revertedWith(
|
||||||
|
"ERC20: burn amount exceeds balance"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
11
contracts/tsconfig.json
Normal file
11
contracts/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user