mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25:18 +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/
|
||||
docs/
|
||||
tests/
|
||||
contracts/
|
||||
|
||||
go.work
|
||||
go.work.sum
|
||||
|
@ -1 +1,2 @@
|
||||
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