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:
Robert Pirtle 2023-05-19 16:39:50 -07:00 committed by GitHub
parent ff709d73e1
commit 6da31bd662
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 11197 additions and 0 deletions

View File

@ -3,6 +3,7 @@ out/
.git/
docs/
tests/
contracts/
go.work
go.work.sum

View File

@ -1 +1,2 @@
golang 1.20
nodejs 18.16.0

4
contracts/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
artifacts
cache
coverage

10
contracts/.eslintrc.cjs Normal file
View 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
View File

@ -0,0 +1,11 @@
node_modules
.env
coverage
coverage.json
typechain
typechain-types
# Hardhat files
cache
artifacts

View File

@ -0,0 +1,6 @@
node_modules
package-lock.json
artifacts
cache
coverage*

7
contracts/.solhint.json Normal file
View 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
View File

@ -0,0 +1 @@
node_modules

45
contracts/README.md Normal file
View 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
```

View 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);
}
}

View 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

File diff suppressed because it is too large Load Diff

39
contracts/package.json Normal file
View 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"
}
}

View 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;
});

View 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
View 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