Commit 3d4d988c authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

package: contracts-governance (#2670)

* package: contracts-governance

* contracts-governance: update deps

kelvin magic touch

* contracts-governance: fix tsconfig

* deps: more fixes

* ci: add contracts-governance tests

* ops: install contracts-governance

* Create light-parrots-yell.md

* contracts-governance: delete nvmrc

* contracts-governance: package.json cleanup
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 2d791301
---
"@eth-optimism/contracts-governance": patch
"@eth-optimism/contracts": patch
---
package: contracts-governance
...@@ -179,6 +179,24 @@ jobs: ...@@ -179,6 +179,24 @@ jobs:
command: yarn test:coverage command: yarn test:coverage
working_directory: packages/contracts-periphery working_directory: packages/contracts-periphery
contracts-governance-tests:
docker:
- image: ethereumoptimism/js-builder:latest
resource_class: xlarge
steps:
- restore_cache:
keys:
- v2-cache-yarn-build-{{ .Revision }}
- checkout
- run:
name: Lint
command: yarn lint:check
working_directory: packages/contracts-governance
- run:
name: Test
command: yarn test
working_directory: packages/contracts-governance
dtl-tests: dtl-tests:
docker: docker:
- image: ethereumoptimism/js-builder:latest - image: ethereumoptimism/js-builder:latest
...@@ -519,6 +537,9 @@ workflows: ...@@ -519,6 +537,9 @@ workflows:
- contracts-bedrock-tests: - contracts-bedrock-tests:
requires: requires:
- yarn-monorepo - yarn-monorepo
- contracts-governance-tests:
requires:
- yarn-monorepo
- js-lint-test: - js-lint-test:
name: dtl-tests name: dtl-tests
package_name: data-transport-layer package_name: data-transport-layer
......
...@@ -22,6 +22,7 @@ COPY packages/common-ts/package.json ./packages/common-ts/package.json ...@@ -22,6 +22,7 @@ COPY packages/common-ts/package.json ./packages/common-ts/package.json
COPY packages/contracts/package.json ./packages/contracts/package.json COPY packages/contracts/package.json ./packages/contracts/package.json
COPY packages/contracts-bedrock/package.json ./packages/contracts-bedrock/package.json COPY packages/contracts-bedrock/package.json ./packages/contracts-bedrock/package.json
COPY packages/contracts-periphery/package.json ./packages/contracts-periphery/package.json COPY packages/contracts-periphery/package.json ./packages/contracts-periphery/package.json
COPY packages/contracts-governance/package.json ./packages/contracts-governance/package.json
COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json
COPY packages/message-relayer/package.json ./packages/message-relayer/package.json COPY packages/message-relayer/package.json ./packages/message-relayer/package.json
COPY packages/fault-detector/package.json ./packages/fault-detector/package.json COPY packages/fault-detector/package.json ./packages/fault-detector/package.json
......
ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
PRIVATE_KEY=0x...
PRIVATE_KEY_DEPLOYER=
PRIVATE_KEY_TOKEN_DEPLOYER=0x...
L1_PROVIDER_URL=http://localhost:9545
L2_PROVIDER_URL=http://localhost:8545
PRIVATE_KEY_DISTRIBUTOR_DEPLOYER=ABC123
node_modules
artifacts
cache
coverage
module.exports = {
extends: '../../.eslintrc.js',
}
hardhat.config.ts
scripts
test
node_modules
artifacts
cache
coverage*
gasReporterOutput.json
module.exports = {
...require('../../.prettierrc.js'),
}
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", "^0.8.0"],
"func-visibility": ["warn", { "ignoreConstructors": true }]
}
}
<div align="center">
<a href="https://community.optimism.io"><img alt="Optimism" src="https://user-images.githubusercontent.com/14298799/122151157-0b197500-ce2d-11eb-89d8-6240e3ebe130.png" width=280></a>
<br />
<h1> Optimism Governance Contracts</h1>
</div>
## TL;DR
The token and governance smart contracts for the Optimism DAO. Built using [OpenZeppelin libraries](https://docs.openzeppelin.com/contracts/4.x/) with some customisations. The token is an [ERC20](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20) that is [permissible](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20Permit) and allows for [delegate voting](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20Votes). The token is also [burnable](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20Burnable). See more in the [Specification section](#specification).
Governance will initially be handled by [Snapshot](https://snapshot.org/#/) before moving to an on chain governance system like [OpenZeppelins Governance contracts](https://docs.openzeppelin.com/contracts/4.x/api/governance).
## Getting set up
### Requirements
You will need the following dependancies installed:
```
nvm
node
yarn
npx
```
Instal the required packages by running:
```
nvm use
yarn
```
#### Compile
To compile the smart contracts run:
```
yarn build
```
#### Test
To run the tests run:
```
yarn test
```
#### Lint
To run the linter run:
```
yarn lint
```
#### Coverage
For coverage run:
```
yarn test:coverage
```
#### Deploying
To deploy the contracts you will first need to set up the environment variables.
Duplicate the [`.env.example`](./.env.example) file. Rename the duplicate to `.env`.
Fill in the missing environment variables, take care with the specified required formatting of secrets.
Then run the command for your desired network:
```
# To deploy on Optimism Kovan
yarn deploy-op-kovan
# To deploy on Optimism
yarn deploy-op-main
```
---
## Specification
Below we will cover the specifications for the various elements of this repository.
### Governance Token
The [`GovernanceToken.sol`](./contracts/GovernanceToken.sol) contract is a basic ERC20 token, with the following modifications:
***Non-upgradable**
* This token is not upgradable.
***Ownable**
* This token has an owner role to allow for permissioned minting functionality.
***Mintable**
* The `OP` token is an inflationary token. We allow for up to 2% annual inflation supply to be minted by the token `MintManager`.
***Burnable**
* The token allows for tokens to be burnt, as well as allowing approved spenders to burn tokens from users.
* 🛠 **Permittable**
* This token is permittable as defined by [EIP2612](https://eips.ethereum.org/EIPS/eip-2612). This allows users to approve a spender without submitting an onchain transaction through the use of signed messages.
* **Delegate voting**
* This token inherits Open Zeppelins ERC20Votes.sol to allow users to delegate voting power. This requires the token be permittable.
### Mint Manager
The [`MintManager.sol`](./contracts/MintManager.sol) contract is set as the `owner` of the OP token and is responsible for the token inflation schedule. It acts as the token "mint manager" with permission to the `mint` function only.
The current implementation allows minting once per year of up to 2% of the total token supply.
The contract is also upgradable to allow changes in the inflation schedule.
### Snapshot Voting Strategy
(WIP)
### Governance (DAO) Contracts
(WIP)
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @dev The Optimism token used in governance and supporting voting and delegation.
* Implements EIP 2612 allowing signed approvals.
* Contract is "owned" by a `MintManager` instance with permission to the `mint` function only,
* for the purposes of enforcing the token inflation schedule.
*/
contract GovernanceToken is ERC20Burnable, ERC20Votes, Ownable {
/**
* @dev Constructor.
*/
constructor() ERC20("Optimism", "OP") ERC20Permit("Optimism") {}
function mint(address _account, uint256 _amount) public onlyOwner {
_mint(_account, _amount);
}
// The following functions are overrides required by Solidity.
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20, ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._mint(to, amount);
}
function _burn(address account, uint256 amount) internal override(ERC20, ERC20Votes) {
super._burn(account, amount);
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.12;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "./interfaces/IMerkleDistributor.sol";
contract MerkleDistributor is IMerkleDistributor {
address public immutable override token;
bytes32 public immutable override merkleRoot;
uint256 public constant ONE_YEAR_IN_SECONDS = 31_536_000;
uint256 public immutable activationTimestamp;
address public immutable airdropTreasury;
bool public isActive;
// This is a packed array of booleans.
mapping(uint256 => uint256) private claimedBitMap;
event Finalised(address indexed calledBy, uint256 timestamp, uint256 unclaimedAmount);
constructor(
address token_,
bytes32 merkleRoot_,
address _treasury
) {
token = token_;
merkleRoot = merkleRoot_;
activationTimestamp = block.timestamp;
isActive = true;
airdropTreasury = _treasury;
}
function isClaimed(uint256 index) public view override returns (bool) {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}
function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex);
}
function claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external override {
require(!isClaimed(index), "MerkleDistributor: Drop already claimed.");
// Verify the merkle proof.
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
require(
MerkleProof.verify(merkleProof, merkleRoot, node),
"MerkleDistributor: Invalid proof."
);
// Mark it claimed and send the token.
_setClaimed(index);
require(IERC20(token).transfer(account, amount), "MerkleDistributor: Transfer failed.");
emit Claimed(index, account, amount);
}
/**
* @dev Finalises the airdrop and sweeps unclaimed tokens into the Optimism multisig
*/
function clawBack() external {
// Airdrop can only be finalised once
require(isActive, "Airdrop: Already finalised");
// Airdrop will remain open for one year
require(
block.timestamp >= activationTimestamp + ONE_YEAR_IN_SECONDS,
"Airdrop: Drop should remain open for one year"
);
// Deactivate airdrop
isActive = false;
// Sweep unclaimed tokens
uint256 amount = IERC20(token).balanceOf(address(this));
require(
IERC20(token).transfer(airdropTreasury, amount),
"Airdrop: Finalise transfer failed"
);
emit Finalised(msg.sender, block.timestamp, amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./GovernanceToken.sol";
/**
* @dev Set as `owner` of the OP token and responsible for the token inflation schedule.
* Contract acts as the token "mint manager" with permission to the `mint` function only.
* Currently permitted to mint once per year of up to 2% of the total token supply.
* Upgradable to allow changes in the inflation schedule.
*/
contract MintManager is Ownable {
GovernanceToken public governanceToken;
uint256 public constant MINT_CAP = 200; // 2%
uint256 public constant MINT_PERIOD = 365 days;
uint256 public mintPermittedAfter;
constructor(address _upgrader, address _governanceToken) {
transferOwnership(_upgrader);
governanceToken = GovernanceToken(_governanceToken);
}
/**
* @param _account Address to mint new tokens to.
* @param _amount Amount of tokens to be minted.
* @notice Only the token owner is allowed to mint.
*/
function mint(address _account, uint256 _amount) public onlyOwner {
if (mintPermittedAfter > 0) {
require(mintPermittedAfter <= block.timestamp, "OP: minting not permitted yet");
require(
_amount <= (governanceToken.totalSupply() * MINT_CAP) / 1000,
"OP: mint amount exceeds cap"
);
}
governanceToken.mint(_account, _amount);
mintPermittedAfter = block.timestamp + MINT_PERIOD;
}
function upgrade(address _newMintManager) public onlyOwner {
require(_newMintManager != address(0), "OP: Mint manager cannot be empty");
governanceToken.transferOwnership(_newMintManager);
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.12;
// Allows anyone to claim a token if they exist in a merkle root.
interface IMerkleDistributor {
// Returns the address of the token distributed by this contract.
function token() external view returns (address);
// Returns the merkle root of the merkle tree containing account balances available to claim.
function merkleRoot() external view returns (bytes32);
// Returns true if the index has been marked claimed.
function isClaimed(uint256 index) external view returns (bool);
// Claim the given amount of the token to the given address. Reverts if the inputs are invalid.
function claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external;
// This event is triggered whenever a call to #claim succeeds.
event Claimed(uint256 index, address account, uint256 amount);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.12;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TestERC20 is ERC20 {
constructor(
string memory name_,
string memory symbol_,
uint256 amountToMint
) ERC20(name_, symbol_) {
setBalance(msg.sender, amountToMint);
}
// sets the balance of the address
// this mints/burns the amount depending on the current balance
function setBalance(address to, uint256 amount) public {
uint256 old = balanceOf(to);
if (old < amount) {
_mint(to, amount - old);
} else if (old > amount) {
_burn(to, old - amount);
}
}
}
import dotenv from 'dotenv'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-etherscan'
import '@nomiclabs/hardhat-waffle'
import 'hardhat-gas-reporter'
import 'solidity-coverage'
import { task, types } from 'hardhat/config'
import { providers, utils, Wallet } from 'ethers'
import { CrossChainMessenger } from '@eth-optimism/sdk'
import './scripts/deploy-token'
import './scripts/multi-send'
import './scripts/mint-initial-supply'
import './scripts/generate-merkle-root'
import './scripts/create-airdrop-json'
import './scripts/deploy-distributor'
import './scripts/test-claims'
import './scripts/create-distributor-json'
dotenv.config()
task('accounts', 'Prints the list of accounts').setAction(async (args, hre) => {
const accounts = await hre.ethers.getSigners()
for (const account of accounts) {
console.log(account.address)
}
})
task('deposit', 'Deposits funds onto Optimism.')
.addParam('to', 'Recipient address.', null, types.string)
.addParam('amountEth', 'Amount in ETH to send.', null, types.string)
.addParam('l1ProviderUrl', '', process.env.L1_PROVIDER_URL, types.string)
.addParam('l2ProviderUrl', '', process.env.L2_PROVIDER_URL, types.string)
.addParam('privateKey', '', process.env.PRIVATE_KEY, types.string)
.setAction(async (args) => {
const { to, amountEth, l1ProviderUrl, l2ProviderUrl, privateKey } = args
if (!l1ProviderUrl || !l2ProviderUrl || !privateKey) {
throw new Error(
'You must define --l1-provider-url, --l2-provider-url, --private-key in your environment.'
)
}
const l1Provider = new providers.JsonRpcProvider(l1ProviderUrl)
const l1Wallet = new Wallet(privateKey, l1Provider)
const messenger = new CrossChainMessenger({
l1SignerOrProvider: l1Wallet,
l2SignerOrProvider: l2ProviderUrl,
l1ChainId: (await l1Provider.getNetwork()).chainId,
})
const amountWei = utils.parseEther(amountEth)
console.log(`Depositing ${amountEth} ETH to ${to}...`)
const tx = await messenger.depositETH(amountWei, {
recipient: to,
})
console.log(`Got TX hash ${tx.hash}. Waiting...`)
await tx.wait()
const l2Provider = new providers.JsonRpcProvider(l2ProviderUrl)
const l1WalletOnL2 = new Wallet(privateKey, l2Provider)
await l1WalletOnL2.sendTransaction({
to,
value: utils.parseEther(amountEth),
})
const balance = await l2Provider.getBalance(to)
console.log('Funded account balance', balance.toString())
console.log('Done.')
})
const privKey = process.env.PRIVATE_KEY || '0x' + '11'.repeat(32)
/**
* @type import("hardhat/config").HardhatUserConfig
*/
module.exports = {
solidity: '0.8.12',
networks: {
optimism: {
chainId: 17,
url: 'http://localhost:8545',
saveDeployments: false,
},
'optimism-kovan': {
chainId: 69,
url: 'https://kovan.optimism.io',
accounts: [privKey],
},
'optimism-nightly': {
chainId: 421,
url: 'https://goerli-nightly-us-central1-a-sequencer.optimism.io',
saveDeployments: true,
accounts: [privKey],
},
'optimism-mainnet': {
chainId: 10,
url: 'https://mainnet.optimism.io',
accounts: [privKey],
},
'hardhat-node': {
url: 'http://localhost:9545',
saveDeployments: false,
},
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: 'USD',
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
}
{
"name": "@eth-optimism/contracts-governance",
"version": "0.1.0",
"author": "Optimism PBC",
"license": "MIT",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"dist/types/*.ts",
"artifacts/**/*.json",
"deployments/**/*.json"
],
"scripts": {
"build": "npx hardhat compile",
"test": "npx hardhat test",
"test:coverage": "IS_COVERAGE=true npx hardhat coverage",
"lint:js:check": "eslint . --max-warnings=0",
"lint:contracts:check": "yarn solhint -f table 'contracts/**/*.sol'",
"lint:check": "yarn lint:contracts:check && yarn lint:js:check",
"lint:js:fix": "eslint --fix .",
"lint:contracts:fix": "yarn prettier --write 'contracts/**/*.sol'",
"lint:fix": "yarn lint:contracts:fix && yarn lint:js:fix",
"lint": "yarn lint:fix && yarn lint:check",
"deploy:test": "hardhat deploy-token --network optimism",
"deploy:kovan": "hardhat deploy-token --network 'optimism-kovan'",
"deploy:mainnet": "hardhat deploy-token --network 'optimism-mainnet'"
},
"dependencies": {
"@eth-optimism/sdk": "^1.0.1",
"@ethersproject/hardware-wallets": "^5.6.1",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-etherscan": "^3.0.1",
"@nomiclabs/hardhat-waffle": "^2.0.2",
"@nomiclabs/hardhat-web3": "^2.0.0",
"@openzeppelin/contracts": "4.5.0",
"commander": "^9.3.0",
"csv-parse": "^5.0.4",
"ethereumjs-util": "^7.1.4",
"eth-sig-util": "^3.0.1",
"ethers": "^5.6.8",
"hardhat": "^2.9.6"
},
"devDependencies": {
"@types/mocha": "^9.1.1",
"chai": "^4.3.6",
"dotenv": "^16.0.0",
"eslint": "^8.9.0",
"ethereum-waffle": "^3.4.0",
"hardhat-gas-reporter": "^1.0.7",
"prettier": "^2.3.1",
"prettier-plugin-solidity": "^1.0.0-beta.18",
"solhint": "^3.3.6",
"solidity-coverage": "^0.7.19",
"ts-node": "^10.0.0",
"typescript": "^4.6.2"
}
}
import fs from 'fs'
import { task } from 'hardhat/config'
import { parse } from 'csv-parse'
import { BigNumber } from 'ethers'
task('create-airdrop-json')
.addParam('inFile', 'Location of the airdrop CSV')
.addParam('outFile', 'Where to write the outputted JSON')
.setAction(async (args, hre) => {
const out: { [k: string]: BigNumber } = {}
let total = BigNumber.from(0)
console.log('Reading...')
const parser = fs.createReadStream(args.inFile).pipe(parse())
let isHeader = true
for await (const record of parser) {
if (isHeader) {
isHeader = false
continue
}
const addr = record[0]
const amount = record[record.length - 1]
total = total.add(amount)
out[addr] = amount
}
console.log('Writing...')
fs.writeFileSync(args.outFile, JSON.stringify(out, null, ' '))
console.log(
`Total airdrop tokens: ${hre.ethers.utils.formatEther(
total.toString()
)} (${total.toString()})`
)
console.log(`Total airdrop addrs: ${Object.keys(out).length}`)
console.log('Verifying...')
let verTotal = BigNumber.from(0)
const data = JSON.parse(fs.readFileSync(args.outFile).toString('utf-8'))
for (const [addr, amount] of Object.entries(data)) {
if (out[addr] !== amount) {
throw new Error('Value mismatch!')
}
verTotal = verTotal.add(amount as any)
}
if (!total.eq(verTotal)) {
throw new Error('Total mismatch!')
}
console.log('OK')
})
import fs from 'fs'
import { task } from 'hardhat/config'
import { parse } from 'csv-parse'
import { BigNumber } from 'ethers'
task('create-distributor-json')
.addParam('inFile', 'CSV to read')
.addParam('outFile', 'JSON to create')
.addOptionalParam(
'mnemonic',
'Mnemonic',
process.env.DISTRIBUTOR_FALLBACK_MNEMONIC
)
.setAction(async (args, hre) => {
const parser = fs.createReadStream(args.inFile).pipe(parse())
const records = []
let total = BigNumber.from(0)
for await (const record of parser) {
const name = record[0].trim()
const amt = record[record.length - 4].trim().replace(/,/gi, '')
const address = record[record.length - 3].trim()
records.push({
name,
amountHuman: amt,
amount: hre.ethers.utils.parseEther(amt).toString(),
address,
fallbackIndex: -1,
})
total = total.add(amt)
}
records.sort((a, b) => {
if (a.name > b.name) {
return 1
}
if (a.name < b.name) {
return -1
}
return 0
})
for (let i = 0; i < records.length; i++) {
const record = records[i]
if (record.address.slice(0, 2) !== '0x') {
console.log(
`Generating fallback address for ${record.name}. Account index: ${i}`
)
const wallet = hre.ethers.Wallet.fromMnemonic(
args.mnemonic,
`m/44'/60'/0'/0/${i}`
)
record.address = wallet.address
record.fallbackIndex = i
}
}
fs.writeFileSync(args.outFile, JSON.stringify(records, null, ' '))
console.log(`Total: ${total.toString()}`)
if (total.eq(1_434_262_041)) {
console.log('AMOUNTS VERIFIED')
} else {
throw new Error('AMOUNTS INVALID')
}
})
import fs from 'fs'
import { task } from 'hardhat/config'
import dotenv from 'dotenv'
import { BigNumber } from 'ethers'
import { MerkleDistributorInfo } from '../src/parse-balance-map'
import { prompt } from '../src/prompt'
dotenv.config()
task('deploy-distributor')
.addParam(
'pkDeployer',
'Private key of the minter',
process.env.PRIVATE_KEY_DISTRIBUTOR_DEPLOYER
)
.addParam(
'treasuryAddr',
'Address airdrops can be swept to if left unclaimed for a year. Defaults to the OP multisig',
'0x2e128664036fa6AAdFEA521fd2Ce192309c25242'
)
.addParam('inFile', 'Location of the Merkle roots JSON file')
.setAction(async (args, hre) => {
const file = fs.readFileSync(args.inFile).toString()
const data = JSON.parse(file) as MerkleDistributorInfo
const deployer = new hre.ethers.Wallet(args.pkDeployer).connect(
hre.ethers.provider
)
console.log(
`About to deploy the MerkleDistributor with the following parameters:`
)
console.log(`Network: ${hre.network.name}`)
console.log('Token addr: 0x4200000000000000000000000000000000000042')
console.log(`Merkle root: ${data.merkleRoot}`)
console.log(`Treasury addr: ${args.treasuryAddr}`)
console.log(`Deployer addr: ${deployer.address}`)
console.log(
`Deployer balance: ${hre.ethers.utils.formatEther(
await deployer.getBalance()
)}`
)
await prompt('Is this OK?')
const factory = await hre.ethers.getContractFactory('MerkleDistributor')
const contract = await factory
.connect(deployer)
.deploy(
'0x4200000000000000000000000000000000000042',
data.merkleRoot,
args.treasuryAddr,
{
gasLimit: 3000000,
}
)
console.log(
`Deploying distributor in ${contract.deployTransaction.hash}...`
)
await contract.deployed()
console.log(
`Deployed distributor at ${
contract.address
}. Please fund the contract with ${BigNumber.from(
data.tokenTotal
).toString()} OP.`
)
})
import { task, types } from 'hardhat/config'
import { ethers } from 'ethers'
import { LedgerSigner } from '@ethersproject/hardware-wallets'
import dotenv from 'dotenv'
import { prompt } from '../src/prompt'
dotenv.config()
// Hardcode the expected addresse
const addresses = {
governanceToken: '0x4200000000000000000000000000000000000042',
}
task('deploy-token', 'Deploy governance token and its mint manager contracts')
.addParam('mintManagerOwner', 'Owner of the mint manager')
.addOptionalParam('useLedger', 'User ledger hardware wallet as signer')
.addOptionalParam(
'ledgerTokenDeployerPath',
'Ledger key derivation path for the token deployer account',
ethers.utils.defaultPath,
types.string
)
.addParam(
'pkDeployer',
'Private key for main deployer account',
process.env.PRIVATE_KEY_DEPLOYER
)
.addOptionalParam(
'pkTokenDeployer',
'Private key for the token deployer account',
process.env.PRIVATE_KEY_TOKEN_DEPLOYER
)
.setAction(async (args, hre) => {
console.log('Deploying token to', hre.network.name, 'network')
// There cannot be two ledgers at the same time
let tokenDeployer
// Deploy the token
if (args.useLedger) {
// Token is deployed to a system address at `0x4200000000000000000000000000000000000042`
// For that a dedicated deployer account is used
tokenDeployer = new LedgerSigner(
hre.ethers.provider,
'default',
args.ledgerTokenDeployerPath
)
} else {
tokenDeployer = new hre.ethers.Wallet(args.pkTokenDeployer).connect(
hre.ethers.provider
)
}
// Create the MintManager Deployer
const deployer = new hre.ethers.Wallet(args.pkDeployer).connect(
hre.ethers.provider
)
// Get the sizes of the bytecode to check if the contracts
// have already been deployed. Useful for an error partway through
// the script
const governanceTokenCode = await hre.ethers.provider.getCode(
addresses.governanceToken
)
const addrTokenDeployer = await tokenDeployer.getAddress()
console.log(`Using token deployer: ${addrTokenDeployer}`)
const tokenDeployerBalance = await tokenDeployer.getBalance()
if (tokenDeployerBalance.eq(0)) {
throw new Error(`Token deployer has no balance`)
}
console.log(`Token deployer balance: ${tokenDeployerBalance.toString()}`)
const nonceTokenDeployer = await tokenDeployer.getTransactionCount()
console.log(`Token deployer nonce: ${nonceTokenDeployer}`)
const GovernanceToken = await hre.ethers.getContractFactory(
'GovernanceToken'
)
let governanceToken = GovernanceToken.attach(
addresses.governanceToken
).connect(tokenDeployer)
if (nonceTokenDeployer === 0 && governanceTokenCode === '0x') {
await prompt('Ready to deploy. Does everything look OK?')
// Deploy the GovernanceToken
governanceToken = await GovernanceToken.connect(tokenDeployer).deploy()
const tokenReceipt = await governanceToken.deployTransaction.wait()
console.log('GovernanceToken deployed to:', tokenReceipt.contractAddress)
if (tokenReceipt.contractAddress !== addresses.governanceToken) {
console.log(
`Expected governance token address ${addresses.governanceToken}`
)
console.log(`Got ${tokenReceipt.contractAddress}`)
throw new Error(`Fatal error! Mismatch of governance token address`)
}
} else {
console.log(
`GovernanceToken already deployed at ${addresses.governanceToken}, skipping`
)
console.log(`Deployer nonce: ${nonceTokenDeployer}`)
console.log(`Code size: ${governanceTokenCode.length}`)
}
const { mintManagerOwner } = args
// Do the deployer things
console.log('Deploying MintManager')
const addr = await deployer.getAddress()
console.log(`Using MintManager deployer: ${addr}`)
const deployerBalance = await deployer.getBalance()
if (deployerBalance.eq(0)) {
throw new Error('Deployer has no balance')
}
console.log(`Deployer balance: ${deployerBalance.toString()}`)
const deployerNonce = await deployer.getTransactionCount()
console.log(`Deployer nonce: ${deployerNonce}`)
await prompt('Does this look OK?')
const MintManager = await hre.ethers.getContractFactory('MintManager')
// Deploy the MintManager
console.log(
`Deploying MintManager with (${mintManagerOwner}, ${addresses.governanceToken})`
)
const mintManager = await MintManager.connect(deployer).deploy(
mintManagerOwner,
addresses.governanceToken
)
const receipt = await mintManager.deployTransaction.wait()
console.log(`Deployed mint manager to ${receipt.contractAddress}`)
let mmOwner = await mintManager.owner()
const currTokenOwner = await governanceToken
.attach(addresses.governanceToken)
.owner()
console.log(
'About to transfer ownership of the token to the mint manager! This is irreversible.'
)
console.log(`Current token owner: ${currTokenOwner}`)
console.log(`Mint manager address: ${mintManager.address}`)
console.log(`Mint manager owner: ${mmOwner}`)
await prompt('Is this OK?')
console.log('Transferring ownership...')
// Transfer ownership of the token to the MintManager instance
const tx = await governanceToken
.attach(addresses.governanceToken)
.transferOwnership(mintManager.address)
await tx.wait()
console.log(
`Transferred ownership of governance token to ${mintManager.address}`
)
console.log('MintManager deployed to:', receipt.contractAddress)
console.log('MintManager owner set to:', mintManagerOwner)
console.log(
'MintManager governanceToken set to:',
addresses.governanceToken
)
console.log('### Token deployment complete ###')
const tokOwner = await governanceToken
.attach(addresses.governanceToken)
.owner()
if (tokOwner !== mintManager.address) {
throw new Error(`GovernanceToken owner not set correctly`)
}
// Check that the deployment went as expected
const govToken = await mintManager.governanceToken()
if (govToken !== addresses.governanceToken) {
throw new Error(`MintManager governance token not set correctly`)
}
mmOwner = await mintManager.owner()
if (mmOwner !== mintManagerOwner) {
throw new Error(`MintManager owner not set correctly`)
}
console.log('Validated MintManager config')
})
import fs from 'fs'
import { task } from 'hardhat/config'
import { parseBalanceMap } from '../src/parse-balance-map'
task('generate-merkle-root')
.addParam(
'inFile',
'Input JSON file location containing a map of account addresses to string balances'
)
.addParam('outFile', 'Output JSON file location for the Merkle data.')
.setAction(async (args, hre) => {
console.log('Reading balances map...')
const json = JSON.parse(fs.readFileSync(args.inFile, { encoding: 'utf8' }))
if (typeof json !== 'object') {
throw new Error('Invalid JSON')
}
console.log('Parsing balances map...')
const data = parseBalanceMap(json)
console.log('Writing claims...')
fs.writeFileSync(args.outFile, JSON.stringify(data, null, ' '))
console.log(`Merkle root: ${data.merkleRoot}`)
console.log(`Token total: ${hre.ethers.utils.formatEther(data.tokenTotal)}`)
console.log(`Num claims: ${Object.keys(data.claims).length}`)
})
import { task } from 'hardhat/config'
import { ethers } from 'ethers'
import dotenv from 'dotenv'
import { prompt } from '../src/prompt'
dotenv.config()
task('mint-initial-supply', 'Mints the initial token supply')
.addParam('mintManagerAddr', 'Address of the mint manager')
.addParam('amount', 'Amount to mint (IN WHOLE OP)', '4294967296')
.addParam(
'pkMinter',
'Private key of the minter',
process.env.PRIVATE_KEY_INITIAL_MINTER
)
.setAction(async (args, hre) => {
const minter = new hre.ethers.Wallet(args.pkMinter).connect(
hre.ethers.provider
)
const amount = args.amount
const amountBase = ethers.utils.parseEther(amount)
console.log('Please verify initial mint amount and recipient.')
console.log('!!! THIS IS A ONE-WAY ACTION !!!')
console.log('')
console.log(`Amount: ${args.amount}`)
console.log(`Amount (base units): ${amountBase.toString()}`)
console.log(`Recipient: ${minter.address}`)
console.log('')
const govToken = await hre.ethers.getContractAt(
'GovernanceToken',
'0x4200000000000000000000000000000000000042'
)
const mintManager = (
await hre.ethers.getContractAt('MintManager', args.mintManagerAddr)
).connect(minter)
const permittedAfter = await mintManager.mintPermittedAfter()
if (!permittedAfter.eq(0)) {
throw new Error('Mint manager has already executed.')
}
const owner = await mintManager.owner()
if (minter.address !== owner) {
throw new Error(
`Mint manager is owned by ${owner}, not ${minter.address}`
)
}
const tokOwner = await govToken.owner()
if (mintManager.address !== tokOwner) {
throw new Error(
`Gov token is owned by ${tokOwner}, not ${mintManager.address}`
)
}
await prompt('Is this OK?')
const tx = await mintManager.mint(minter.address, amountBase, {
gasLimit: 3_000_000,
})
console.log(`Sent transaction ${tx.hash}`)
await tx.wait()
console.log('Successfully minted. Verifying...')
const supply = await govToken.totalSupply()
if (supply.eq(amountBase)) {
console.log('Total supply verified.')
} else {
console.log(
`Total supply invalid! Have: ${supply.toString()}, want: ${amountBase.toString()}.`
)
}
const bal = await govToken.balanceOf(minter.address)
if (bal.eq(amountBase)) {
console.log('Balance verified.')
} else {
console.log(
`Minter balance invalid! Have: ${bal.toString()}, want: ${amountBase.toString()}.`
)
}
})
import fs from 'fs'
import { task } from 'hardhat/config'
import dotenv from 'dotenv'
import { prompt } from '../src/prompt'
dotenv.config()
task('multi-send', 'Send tokens to multiple addresses')
.addOptionalParam(
'privateKey',
'Private Key for deployer account',
process.env.PRIVATE_KEY_MULTI_SEND
)
.addParam('inFile', 'Distribution file')
.setAction(async (args, hre) => {
console.log(`Starting multi send on ${hre.network.name} network`)
// Load the distribution setup
const distributionJson = fs.readFileSync(args.inFile).toString()
const distribution = JSON.parse(distributionJson)
const sender = new hre.ethers.Wallet(args.privateKey).connect(
hre.ethers.provider
)
const addr = await sender.getAddress()
console.log(`Using deployer: ${addr}`)
console.log('Performing multi send to the following addresses:')
for (const [address, amount] of Object.entries(distribution)) {
console.log(
`${address}: ${amount} (${hre.ethers.utils.parseEther(
amount as string
)})`
)
}
await prompt('Is this OK?')
const governanceToken = (
await hre.ethers.getContractAt(
'GovernanceToken',
'0x4200000000000000000000000000000000000042'
)
).connect(sender)
for (const [address, amount] of Object.entries(distribution)) {
const amountBase = hre.ethers.utils.parseEther(amount as string)
console.log(`Transferring ${amountBase} tokens to ${address}...`)
const transferTx = await governanceToken.transfer(address, amountBase)
console.log(`Waiting for tx ${transferTx.hash}`)
await transferTx.wait()
}
console.log('Done.')
})
import fs from 'fs'
import { task } from 'hardhat/config'
import { MerkleDistributorInfo } from '../src/parse-balance-map'
task('test-claims')
.addParam('inFile', 'Input claims file')
.addParam('distributorAddress', 'Address of the distributor')
.setAction(async (args, hre) => {
const distrib = (
await hre.ethers.getContractAt(
'MerkleDistributor',
args.distributorAddress
)
).connect(hre.ethers.provider)
console.log('Reading claims...')
const json = JSON.parse(
fs.readFileSync(args.inFile, { encoding: 'utf8' })
) as MerkleDistributorInfo
console.log('Smoke testing 100 random claims.')
const addresses = Object.keys(json.claims)
for (let i = 0; i < 100; i++) {
const index = Math.floor(addresses.length * Math.random())
const addr = addresses[index]
const claim = json.claims[addr]
process.stdout.write(`Attempting claim for ${addr} [${i + 1}/100]... `)
await distrib.callStatic.claim(
claim.index,
addr,
claim.amount,
claim.proof
)
process.stdout.write('OK\n')
}
console.log('Smoke test passed.')
})
import fs from 'fs'
import { program } from 'commander'
import { BigNumber, utils } from 'ethers'
program
.version('0.0.0')
.requiredOption(
'-i, --input <path>',
'input JSON file location containing the merkle proofs for each account and the merkle root'
)
program.parse(process.argv)
const json = JSON.parse(fs.readFileSync(program.input, { encoding: 'utf8' }))
const combinedHash = (first: Buffer, second: Buffer): Buffer => {
if (!first) {
return second
}
if (!second) {
return first
}
return Buffer.from(
utils
.solidityKeccak256(
['bytes32', 'bytes32'],
[first, second].sort(Buffer.compare)
)
.slice(2),
'hex'
)
}
const toNode = (
index: number | BigNumber,
account: string,
amount: BigNumber
): Buffer => {
const pairHex = utils.solidityKeccak256(
['uint256', 'address', 'uint256'],
[index, account, amount]
)
return Buffer.from(pairHex.slice(2), 'hex')
}
const verifyProof = (
index: number | BigNumber,
account: string,
amount: BigNumber,
proof: Buffer[],
expected: Buffer
): boolean => {
let pair = toNode(index, account, amount)
for (const item of proof) {
pair = combinedHash(pair, item)
}
return pair.equals(expected)
}
const getNextLayer = (elements: Buffer[]): Buffer[] => {
return elements.reduce<Buffer[]>((layer, el, idx, arr) => {
if (idx % 2 === 0) {
// Hash the current element with its pair element
layer.push(combinedHash(el, arr[idx + 1]))
}
return layer
}, [])
}
const getRoot = (
_balances: { account: string; amount: BigNumber; index: number }[]
): Buffer => {
let nodes = _balances
.map(({ account, amount, index }) => toNode(index, account, amount))
// sort by lexicographical order
.sort(Buffer.compare)
// deduplicate any eleents
nodes = nodes.filter((el, idx) => {
return idx === 0 || !nodes[idx - 1].equals(el)
})
const layers = []
layers.push(nodes)
// Get next layer until we reach the root
while (layers[layers.length - 1].length > 1) {
layers.push(getNextLayer(layers[layers.length - 1]))
}
return layers[layers.length - 1][0]
}
if (typeof json !== 'object') {
throw new Error('Invalid JSON')
}
const merkleRootHex = json.merkleRoot
const merkleRoot = Buffer.from(merkleRootHex.slice(2), 'hex')
const balances: { index: number; account: string; amount: BigNumber }[] = []
let valid = true
Object.keys(json.claims).forEach((address) => {
const claim = json.claims[address]
const proof = claim.proof.map((p: string) => Buffer.from(p.slice(2), 'hex'))
balances.push({
index: claim.index,
account: address,
amount: BigNumber.from(claim.amount),
})
if (verifyProof(claim.index, address, claim.amount, proof, merkleRoot)) {
console.log('Verified proof for', claim.index, address)
} else {
console.log('Verification for', address, 'failed')
valid = false
}
})
if (!valid) {
console.error('Failed validation for 1 or more proofs')
process.exit(1)
}
console.log('Done!')
// Root
const root = getRoot(balances).toString('hex')
console.log('Reconstructed merkle root', root)
console.log(
'Root matches the one read from the JSON?',
root === merkleRootHex.slice(2)
)
import { BigNumber, utils } from 'ethers'
import MerkleTree from './merkle-tree'
export default class BalanceTree {
private readonly tree: MerkleTree
constructor(balances: { account: string; amount: BigNumber }[]) {
this.tree = new MerkleTree(
balances.map(({ account, amount }, index) => {
return BalanceTree.toNode(index, account, amount)
})
)
}
public static verifyProof(
index: number | BigNumber,
account: string,
amount: BigNumber,
proof: Buffer[],
root: Buffer
): boolean {
let pair = BalanceTree.toNode(index, account, amount)
for (const item of proof) {
pair = MerkleTree.combinedHash(pair, item)
}
return pair.equals(root)
}
// keccak256(abi.encode(index, account, amount))
public static toNode(
index: number | BigNumber,
account: string,
amount: BigNumber
): Buffer {
return Buffer.from(
utils
.solidityKeccak256(
['uint256', 'address', 'uint256'],
[index, account, amount]
)
.substr(2),
'hex'
)
}
public getHexRoot(): string {
return this.tree.getHexRoot()
}
// returns the hex bytes32 values of the proof
public getProof(
index: number | BigNumber,
account: string,
amount: BigNumber
): string[] {
return this.tree.getHexProof(BalanceTree.toNode(index, account, amount))
}
}
import { bufferToHex, keccak256 } from 'ethereumjs-util'
export default class MerkleTree {
private readonly elements: Buffer[]
private readonly bufferElementPositionIndex: { [hexElement: string]: number }
private readonly layers: Buffer[][]
constructor(elements: Buffer[]) {
this.elements = [...elements]
// Sort elements
this.elements.sort(Buffer.compare)
// Deduplicate elements
this.elements = MerkleTree.bufDedup(this.elements)
this.bufferElementPositionIndex = this.elements.reduce<{
[hexElement: string]: number
}>((memo, el, index) => {
memo[bufferToHex(el)] = index
return memo
}, {})
// Create layers
this.layers = this.getLayers(this.elements)
}
getLayers(elements: Buffer[]): Buffer[][] {
if (elements.length === 0) {
throw new Error('empty tree')
}
const layers = []
layers.push(elements)
// Get next layer until we reach the root
while (layers[layers.length - 1].length > 1) {
layers.push(this.getNextLayer(layers[layers.length - 1]))
}
return layers
}
getNextLayer(elements: Buffer[]): Buffer[] {
return elements.reduce<Buffer[]>((layer, el, idx, arr) => {
if (idx % 2 === 0) {
// Hash the current element with its pair element
layer.push(MerkleTree.combinedHash(el, arr[idx + 1]))
}
return layer
}, [])
}
static combinedHash(first: Buffer, second: Buffer): Buffer {
if (!first) {
return second
}
if (!second) {
return first
}
return keccak256(MerkleTree.sortAndConcat(first, second))
}
getRoot(): Buffer {
return this.layers[this.layers.length - 1][0]
}
getHexRoot(): string {
return bufferToHex(this.getRoot())
}
getProof(el: Buffer) {
let idx = this.bufferElementPositionIndex[bufferToHex(el)]
if (typeof idx !== 'number') {
throw new Error('Element does not exist in Merkle tree')
}
return this.layers.reduce((proof, layer) => {
const pairElement = MerkleTree.getPairElement(idx, layer)
if (pairElement) {
proof.push(pairElement)
}
idx = Math.floor(idx / 2)
return proof
}, [])
}
getHexProof(el: Buffer): string[] {
const proof = this.getProof(el)
return MerkleTree.bufArrToHexArr(proof)
}
private static getPairElement(idx: number, layer: Buffer[]): Buffer | null {
const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1
if (pairIdx < layer.length) {
return layer[pairIdx]
} else {
return null
}
}
private static bufDedup(elements: Buffer[]): Buffer[] {
return elements.filter((el, idx) => {
return idx === 0 || !elements[idx - 1].equals(el)
})
}
private static bufArrToHexArr(arr: Buffer[]): string[] {
if (arr.some((el) => !Buffer.isBuffer(el))) {
throw new Error('Array is not an array of buffers')
}
return arr.map((el) => '0x' + el.toString('hex'))
}
private static sortAndConcat(...args: Buffer[]): Buffer {
return Buffer.concat([...args].sort(Buffer.compare))
}
}
import { BigNumber, utils } from 'ethers'
import BalanceTree from './balance-tree'
const { isAddress, getAddress } = utils
// This is the blob that gets distributed and pinned to IPFS.
// It is completely sufficient for recreating the entire merkle tree.
// Anyone can verify that all air drops are included in the tree,
// and the tree has no additional distributions.
export interface MerkleDistributorInfo {
merkleRoot: string
tokenTotal: string
claims: {
[account: string]: {
index: number
amount: string
proof: string[]
flags?: {
[flag: string]: boolean
}
}
}
}
type OldFormat = { [account: string]: number | string }
type NewFormat = { address: string; earnings: string; reasons: string }
export const parseBalanceMap = (
balances: OldFormat | NewFormat[]
): MerkleDistributorInfo => {
// if balances are in an old format, process them
const balancesInNewFormat: NewFormat[] = Array.isArray(balances)
? balances
: Object.keys(balances).map((account): NewFormat => {
let earnings: string
if (typeof balances[account] === 'number') {
earnings = `0x${balances[account].toString(16)}`
} else {
earnings = BigNumber.from(balances[account]).toHexString()
}
return {
address: account,
earnings,
reasons: '',
}
})
const dataByAddress = balancesInNewFormat.reduce<{
[address: string]: {
amount: BigNumber
flags?: { [flag: string]: boolean }
}
}>((memo, { address: account, earnings, reasons }) => {
if (!isAddress(account)) {
throw new Error(`Found invalid address: ${account}`)
}
const parsed = getAddress(account)
if (memo[parsed]) {
throw new Error(`Duplicate address: ${parsed}`)
}
const parsedNum = BigNumber.from(earnings)
if (parsedNum.lte(0)) {
throw new Error(`Invalid amount for account: ${account}`)
}
const flags = {
isSOCKS: reasons.includes('socks'),
isLP: reasons.includes('lp'),
isUser: reasons.includes('user'),
}
memo[parsed] = { amount: parsedNum, ...(reasons === '' ? {} : { flags }) }
return memo
}, {})
const sortedAddresses = Object.keys(dataByAddress).sort()
// construct a tree
const tree = new BalanceTree(
sortedAddresses.map((address) => ({
account: address,
amount: dataByAddress[address].amount,
}))
)
// generate claims
const claims = sortedAddresses.reduce<{
[address: string]: {
amount: string
index: number
proof: string[]
flags?: { [flag: string]: boolean }
}
}>((memo, address, index) => {
const { amount, flags } = dataByAddress[address]
memo[address] = {
index,
amount: amount.toHexString(),
proof: tree.getProof(index, address, amount),
...(flags ? { flags } : {}),
}
return memo
}, {})
const tokenTotal: BigNumber = sortedAddresses.reduce<BigNumber>(
(memo, key) => memo.add(dataByAddress[key].amount),
BigNumber.from(0)
)
return {
merkleRoot: tree.getHexRoot(),
tokenTotal: tokenTotal.toHexString(),
claims,
}
}
import readline from 'readline'
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
export const prompt = (msg: string) =>
new Promise<void>((resolve, reject) =>
rl.question(`${msg} [y/n]: `, (confirmation) => {
if (confirmation !== 'y') {
reject('Aborted!')
}
resolve()
})
)
import chai, { expect } from 'chai'
import { solidity, MockProvider } from 'ethereum-waffle'
import { Contract, BigNumber, constants, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import BalanceTree from '../src/balance-tree'
import { parseBalanceMap } from '../src/parse-balance-map'
chai.use(solidity)
const overrides = {
gasLimit: 9999999,
}
const ZERO_BYTES32 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
const UNISWAP_MNEMONIC =
'horn horn horn horn horn horn horn horn horn horn horn horn'
const isCoverage = process.env.IS_COVERAGE === 'true'
describe('MerkleDistributor', () => {
let wallet0: Wallet
let wallet1: Wallet
let wallets: Wallet[]
const deployContract = async (
wallet: Wallet,
name: string,
args: any[],
override: any
) => {
const factory = await ethers.getContractFactory(name)
const contract = await factory.deploy(...args, override)
await contract.deployed()
return contract
}
let token: Contract
beforeEach('deploy token', async () => {
wallets = []
// Have to do this strange dance because the Unsiwap mnemonic is technically invalid.
// Waffle ignores this, so to keep the tests the same we have to "import" the Waffle
// wallets into Ethers.
const mockProviders = new MockProvider({
ganacheOptions: {
hardfork: 'istanbul',
mnemonic: UNISWAP_MNEMONIC,
gasLimit: 9999999,
},
})
const mockWallets = mockProviders.getWallets()
const signer1 = (await ethers.getSigners())[0]
for (let i = 0; i < 10; i++) {
const wallet = new Wallet(mockWallets[i].privateKey)
await signer1.sendTransaction({
to: wallet.address,
value: ethers.utils.parseEther('0.1'),
})
wallets.push(wallet)
}
wallet0 = wallets[0]
wallet1 = wallets[1]
token = await deployContract(
wallet0,
'TestERC20',
['Token', 'TKN', 0],
overrides
)
})
describe('#token', () => {
it('returns the token address', async () => {
const distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, ZERO_BYTES32, wallet0.address],
overrides
)
expect(await distributor.token()).to.eq(token.address)
})
})
describe('#merkleRoot', () => {
it('returns the zero merkle root', async () => {
const distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, ZERO_BYTES32, wallet0.address],
overrides
)
expect(await distributor.merkleRoot()).to.eq(ZERO_BYTES32)
})
})
describe('#claim', () => {
it('fails for empty proof', async () => {
const distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, ZERO_BYTES32, wallet0.address],
overrides
)
await expect(
distributor.claim(0, wallet0.address, 10, [])
).to.be.revertedWith('MerkleDistributor: Invalid proof.')
})
it('fails for invalid index', async () => {
const distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, ZERO_BYTES32, wallet0.address],
overrides
)
await expect(
distributor.claim(0, wallet0.address, 10, [])
).to.be.revertedWith('MerkleDistributor: Invalid proof.')
})
describe('two account tree', () => {
let distributor: Contract
let tree: BalanceTree
beforeEach('deploy', async () => {
tree = new BalanceTree([
{ account: wallet0.address, amount: BigNumber.from(100) },
{ account: wallet1.address, amount: BigNumber.from(101) },
])
distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, tree.getHexRoot(), wallet0.address],
overrides
)
await token.setBalance(distributor.address, 201)
})
it('successful claim', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
await expect(
distributor.claim(0, wallet0.address, 100, proof0, overrides)
)
.to.emit(distributor, 'Claimed')
.withArgs(0, wallet0.address, 100)
const proof1 = tree.getProof(1, wallet1.address, BigNumber.from(101))
await expect(
distributor.claim(1, wallet1.address, 101, proof1, overrides)
)
.to.emit(distributor, 'Claimed')
.withArgs(1, wallet1.address, 101)
})
it('transfers the token', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
expect(await token.balanceOf(wallet0.address)).to.eq(0)
await distributor.claim(0, wallet0.address, 100, proof0, overrides)
expect(await token.balanceOf(wallet0.address)).to.eq(100)
})
it('must have enough to transfer', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
await token.setBalance(distributor.address, 99)
await expect(
distributor.claim(0, wallet0.address, 100, proof0, overrides)
).to.be.revertedWith('ERC20: transfer amount exceeds balance')
})
it('sets #isClaimed', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
expect(await distributor.isClaimed(0)).to.eq(false)
expect(await distributor.isClaimed(1)).to.eq(false)
await distributor.claim(0, wallet0.address, 100, proof0, overrides)
expect(await distributor.isClaimed(0)).to.eq(true)
expect(await distributor.isClaimed(1)).to.eq(false)
})
it('cannot allow two claims', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
await distributor.claim(0, wallet0.address, 100, proof0, overrides)
await expect(
distributor.claim(0, wallet0.address, 100, proof0, overrides)
).to.be.revertedWith('MerkleDistributor: Drop already claimed.')
})
it('cannot claim more than once: 0 and then 1', async () => {
await distributor.claim(
0,
wallet0.address,
100,
tree.getProof(0, wallet0.address, BigNumber.from(100)),
overrides
)
await distributor.claim(
1,
wallet1.address,
101,
tree.getProof(1, wallet1.address, BigNumber.from(101)),
overrides
)
await expect(
distributor.claim(
0,
wallet0.address,
100,
tree.getProof(0, wallet0.address, BigNumber.from(100)),
overrides
)
).to.be.revertedWith('MerkleDistributor: Drop already claimed.')
})
it('cannot claim more than once: 1 and then 0', async () => {
await distributor.claim(
1,
wallet1.address,
101,
tree.getProof(1, wallet1.address, BigNumber.from(101)),
overrides
)
await distributor.claim(
0,
wallet0.address,
100,
tree.getProof(0, wallet0.address, BigNumber.from(100)),
overrides
)
await expect(
distributor.claim(
1,
wallet1.address,
101,
tree.getProof(1, wallet1.address, BigNumber.from(101)),
overrides
)
).to.be.revertedWith('MerkleDistributor: Drop already claimed.')
})
it('cannot claim for address other than proof', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
await expect(
distributor.claim(1, wallet1.address, 101, proof0, overrides)
).to.be.revertedWith('MerkleDistributor: Invalid proof.')
})
it('cannot claim more than proof', async () => {
const proof0 = tree.getProof(0, wallet0.address, BigNumber.from(100))
await expect(
distributor.claim(0, wallet0.address, 101, proof0, overrides)
).to.be.revertedWith('MerkleDistributor: Invalid proof.')
})
it('gas', async function () {
if (isCoverage) {
this.skip()
}
const proof = tree.getProof(0, wallet0.address, BigNumber.from(100))
const tx = await distributor.claim(
0,
wallet0.address,
100,
proof,
overrides
)
const receipt = await tx.wait()
expect(receipt.gasUsed).to.eq(84480) // old 78466
})
})
describe('larger tree', () => {
let distributor: Contract
let tree: BalanceTree
beforeEach('deploy', async () => {
tree = new BalanceTree(
wallets.map((wallet, ix) => {
return { account: wallet.address, amount: BigNumber.from(ix + 1) }
})
)
distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, tree.getHexRoot(), wallet0.address],
overrides
)
await token.setBalance(distributor.address, 201)
})
it('claim index 4', async () => {
const proof = tree.getProof(4, wallets[4].address, BigNumber.from(5))
await expect(
distributor.claim(4, wallets[4].address, 5, proof, overrides)
)
.to.emit(distributor, 'Claimed')
.withArgs(4, wallets[4].address, 5)
})
it('claim index 9', async () => {
const proof = tree.getProof(9, wallets[9].address, BigNumber.from(10))
await expect(
distributor.claim(9, wallets[9].address, 10, proof, overrides)
)
.to.emit(distributor, 'Claimed')
.withArgs(9, wallets[9].address, 10)
})
it('gas', async function () {
if (isCoverage) {
this.skip()
}
const proof = tree.getProof(9, wallets[9].address, BigNumber.from(10))
const tx = await distributor.claim(
9,
wallets[9].address,
10,
proof,
overrides
)
const receipt = await tx.wait()
expect(receipt.gasUsed).to.eq(87237) // old 80960
})
it('gas second down about 15k', async function () {
if (isCoverage) {
this.skip()
}
await distributor.claim(
0,
wallets[0].address,
1,
tree.getProof(0, wallets[0].address, BigNumber.from(1)),
overrides
)
const tx = await distributor.claim(
1,
wallets[1].address,
2,
tree.getProof(1, wallets[1].address, BigNumber.from(2)),
overrides
)
const receipt = await tx.wait()
expect(receipt.gasUsed).to.eq(70117) // old 65940
})
})
describe('realistic size tree', () => {
let distributor: Contract
let tree: BalanceTree
const NUM_LEAVES = 100_000
const NUM_SAMPLES = 25
const elements: { account: string; amount: BigNumber }[] = []
before(() => {
for (let i = 0; i < NUM_LEAVES; i++) {
const node = { account: wallet0.address, amount: BigNumber.from(100) }
elements.push(node)
}
tree = new BalanceTree(elements)
})
it('proof verification works', () => {
const root = Buffer.from(tree.getHexRoot().slice(2), 'hex')
for (let i = 0; i < NUM_LEAVES; i += NUM_LEAVES / NUM_SAMPLES) {
const proof = tree
.getProof(i, wallet0.address, BigNumber.from(100))
.map((el) => Buffer.from(el.slice(2), 'hex'))
const validProof = BalanceTree.verifyProof(
i,
wallet0.address,
BigNumber.from(100),
proof,
root
)
expect(validProof).to.be.true
}
})
beforeEach('deploy', async () => {
distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, tree.getHexRoot(), wallet0.address],
overrides
)
await token.setBalance(distributor.address, constants.MaxUint256)
})
it('gas', async function () {
if (isCoverage) {
this.skip()
}
const proof = tree.getProof(50000, wallet0.address, BigNumber.from(100))
const tx = await distributor.claim(
50000,
wallet0.address,
100,
proof,
overrides
)
const receipt = await tx.wait()
expect(receipt.gasUsed).to.eq(99061) // old 91650
})
it('gas deeper node', async function () {
if (isCoverage) {
this.skip()
}
const proof = tree.getProof(90000, wallet0.address, BigNumber.from(100))
const tx = await distributor.claim(
90000,
wallet0.address,
100,
proof,
overrides
)
const receipt = await tx.wait()
expect(receipt.gasUsed).to.eq(98997) // old 91586
})
it('gas average random distribution', async function () {
if (isCoverage) {
this.skip()
}
let total: BigNumber = BigNumber.from(0)
let count: number = 0
for (let i = 0; i < NUM_LEAVES; i += NUM_LEAVES / NUM_SAMPLES) {
const proof = tree.getProof(i, wallet0.address, BigNumber.from(100))
const tx = await distributor.claim(
i,
wallet0.address,
100,
proof,
overrides
)
const receipt = await tx.wait()
total = total.add(receipt.gasUsed)
count++
}
const average = total.div(count)
expect(average).to.eq(82453) // old 77075
})
// this is what we gas golfed by packing the bitmap
it('gas average first 25', async function () {
if (isCoverage) {
this.skip()
}
let total: BigNumber = BigNumber.from(0)
let count: number = 0
for (let i = 0; i < 25; i++) {
const proof = tree.getProof(i, wallet0.address, BigNumber.from(100))
const tx = await distributor.claim(
i,
wallet0.address,
100,
proof,
overrides
)
const receipt = await tx.wait()
total = total.add(receipt.gasUsed)
count++
}
const average = total.div(count)
expect(average).to.eq(66203) // old 62824
})
it('no double claims in random distribution', async () => {
for (
let i = 0;
i < 25;
i += Math.floor(Math.random() * (NUM_LEAVES / NUM_SAMPLES))
) {
const proof = tree.getProof(i, wallet0.address, BigNumber.from(100))
await distributor.claim(i, wallet0.address, 100, proof, overrides)
await expect(
distributor.claim(i, wallet0.address, 100, proof, overrides)
).to.be.revertedWith('MerkleDistributor: Drop already claimed.')
}
})
})
})
describe('parseBalanceMap', () => {
let distributor: Contract
let claims: {
[account: string]: {
index: number
amount: string
proof: string[]
}
}
beforeEach('deploy', async () => {
const {
claims: innerClaims,
merkleRoot,
tokenTotal,
} = parseBalanceMap({
[wallet0.address]: 200,
[wallet1.address]: '300', // add a string one to verify that the hex cast works
[wallets[2].address]: 250,
})
expect(tokenTotal).to.eq('0x02ee') // 750
claims = innerClaims
distributor = await deployContract(
wallet0,
'MerkleDistributor',
[token.address, merkleRoot, wallet0.address],
overrides
)
await token.setBalance(distributor.address, tokenTotal)
})
it('check the proofs is as expected', () => {
expect(claims).to.deep.eq({
[wallet0.address]: {
index: 0,
amount: '0xc8',
proof: [
'0x2a411ed78501edb696adca9e41e78d8256b61cfac45612fa0434d7cf87d916c6',
],
},
[wallet1.address]: {
index: 1,
amount: '0x012c',
proof: [
'0xbfeb956a3b705056020a3b64c540bff700c0f6c96c55c0a5fcab57124cb36f7b',
'0xd31de46890d4a77baeebddbd77bf73b5c626397b73ee8c69b51efe4c9a5a72fa',
],
},
[wallets[2].address]: {
index: 2,
amount: '0xfa',
proof: [
'0xceaacce7533111e902cc548e961d77b23a4d8cd073c6b68ccf55c62bd47fc36b',
'0xd31de46890d4a77baeebddbd77bf73b5c626397b73ee8c69b51efe4c9a5a72fa',
],
},
})
})
it('all claims work exactly once', async () => {
for (const account of Object.keys(claims)) {
const claim = claims[account]
await expect(
distributor.claim(
claim.index,
account,
claim.amount,
claim.proof,
overrides
)
)
.to.emit(distributor, 'Claimed')
.withArgs(claim.index, account, claim.amount)
await expect(
distributor.claim(
claim.index,
account,
claim.amount,
claim.proof,
overrides
)
).to.be.revertedWith('MerkleDistributor: Drop already claimed.')
}
expect(await token.balanceOf(distributor.address)).to.eq(0)
})
})
})
import { BigNumber, Contract, Wallet } from 'ethers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from 'chai'
import { ethers } from 'hardhat'
import * as ethSigUtil from 'eth-sig-util'
import { fromRpcSig } from 'ethereumjs-util'
import {
MAX_UINT256,
buildDataPermit,
buildDataDelegation,
} from './helpers/eip712'
import {
SECONDS_IN_365_DAYS,
getBlockTimestamp,
fastForwardDays,
} from './helpers/time-travel'
describe('Governance Token Testing', () => {
let network: { chainId: number }
let governanceToken: Contract
let mintManager: Contract
let minter: Wallet
let optimismMultisig: Wallet
let userA: Wallet
let userB: Wallet
let hardhatSigner1: SignerWithAddress
let hardhatSigner2: SignerWithAddress
let hardhatSigner3: SignerWithAddress
let initialSupply: BigNumber
before(async () => {
network = await ethers.provider.getNetwork()
;[hardhatSigner1, hardhatSigner2, hardhatSigner3] =
await ethers.getSigners()
minter = ethers.Wallet.createRandom().connect(ethers.provider)
optimismMultisig = ethers.Wallet.createRandom().connect(ethers.provider)
userA = ethers.Wallet.createRandom().connect(ethers.provider)
userB = ethers.Wallet.createRandom().connect(ethers.provider)
await hardhatSigner1.sendTransaction({
to: minter.address,
value: ethers.utils.parseEther('2000'),
})
await hardhatSigner2.sendTransaction({
to: userA.address,
value: ethers.utils.parseEther('2000'),
})
await hardhatSigner3.sendTransaction({
to: userB.address,
value: ethers.utils.parseEther('2000'),
})
// Initial supply is 2^32 tokens
initialSupply = ethers.BigNumber.from(2)
.pow(32)
.mul(ethers.BigNumber.from(10).pow(18))
})
beforeEach(async () => {
const GovernanceToken = await ethers.getContractFactory('GovernanceToken')
governanceToken = await GovernanceToken.connect(minter).deploy()
await governanceToken.deployed()
const MintManager = await ethers.getContractFactory('MintManager')
mintManager = await MintManager.connect(minter).deploy(
minter.address,
governanceToken.address
)
await mintManager.deployed()
await governanceToken.connect(minter).transferOwnership(mintManager.address)
})
describe('ERC20 initialisation', async () => {
it('token metadata is correct', async () => {
const tokenName = await governanceToken.name()
expect(tokenName).to.equal('Optimism')
const tokenSymbol = await governanceToken.symbol()
expect(tokenSymbol).to.equal('OP')
const tokenDecimals = await governanceToken.decimals()
expect(tokenDecimals).to.equal(18)
})
it('initial token supply should be 0', async () => {
const totalSupply = await governanceToken.totalSupply()
expect(totalSupply).to.be.equal(0)
})
})
describe('Managing token supply', async () => {
it('owner can mint token', async () => {
await mintManager
.connect(minter)
.mint(optimismMultisig.address, initialSupply)
const balance = await governanceToken.balanceOf(optimismMultisig.address)
expect(balance).to.equal(initialSupply)
})
it('timestamp for the next allowed mint is correct', async () => {
const tx = await mintManager
.connect(minter)
.mint(optimismMultisig.address, initialSupply)
const receipt = await ethers.provider.getTransactionReceipt(tx.hash)
const timestamp = await getBlockTimestamp(receipt.blockNumber)
const nextAllowedMintTime = await mintManager.mintPermittedAfter()
expect(nextAllowedMintTime).to.equal(timestamp + SECONDS_IN_365_DAYS)
})
it('non-owner cannot mint token', async () => {
await expect(
governanceToken.connect(userA).mint(userA.address, 1)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should not be able to mint before the next allowed mint time', async () => {
await mintManager
.connect(minter)
.mint(optimismMultisig.address, initialSupply)
// Try to mint immediately after token creation and fail
await expect(
mintManager.connect(minter).mint(optimismMultisig.address, 1)
).to.be.revertedWith('OP: minting not permitted yet')
// Can mint successfully after 1 year
await fastForwardDays(365)
await mintManager.connect(minter).mint(optimismMultisig.address, 1)
// Cannot mint before the second full year has passed
await fastForwardDays(364)
await expect(
mintManager.connect(minter).mint(optimismMultisig.address, 1)
).to.be.revertedWith('OP: minting not permitted yet')
// Can mint after the second full year has passed
await fastForwardDays(1)
await mintManager.connect(minter).mint(optimismMultisig.address, 1)
})
it('should be able to mint 2% supply per year', async () => {
await mintManager
.connect(minter)
.mint(optimismMultisig.address, initialSupply)
// Minting the full 2% after the first year
let totalSupply = await governanceToken.totalSupply()
let maxInflationAmount = totalSupply.mul(200).div(1000)
await fastForwardDays(365)
await mintManager
.connect(minter)
.mint(optimismMultisig.address, maxInflationAmount)
let updatedTotalSupply = await governanceToken.totalSupply()
let newTotalSupply = await totalSupply.add(maxInflationAmount)
expect(updatedTotalSupply).to.equal(newTotalSupply)
// Minting the full 2% after the second year
await fastForwardDays(365)
totalSupply = await governanceToken.totalSupply()
maxInflationAmount = totalSupply.mul(200).div(1000)
await mintManager
.connect(minter)
.mint(optimismMultisig.address, maxInflationAmount)
updatedTotalSupply = await governanceToken.totalSupply()
newTotalSupply = await totalSupply.add(maxInflationAmount)
expect(updatedTotalSupply).to.equal(newTotalSupply)
// Minting the full 2% after the third year
await fastForwardDays(365)
totalSupply = await governanceToken.totalSupply()
maxInflationAmount = totalSupply.mul(200).div(1000)
await mintManager
.connect(minter)
.mint(optimismMultisig.address, maxInflationAmount)
updatedTotalSupply = await governanceToken.totalSupply()
newTotalSupply = await totalSupply.add(maxInflationAmount)
expect(updatedTotalSupply).to.equal(newTotalSupply)
})
it('should be able to mint less than 2% supply per year', async () => {
await mintManager
.connect(minter)
.mint(optimismMultisig.address, initialSupply)
await fastForwardDays(365)
const inflationAmount = initialSupply.mul(200).div(1000).sub(1)
await mintManager
.connect(minter)
.mint(optimismMultisig.address, inflationAmount)
const updatedTotalSupply = await governanceToken.totalSupply()
const newTotalSupply = await initialSupply.add(inflationAmount)
expect(updatedTotalSupply).to.equal(newTotalSupply)
})
it('should not be able to mint more than 2% supply per year', async () => {
await mintManager
.connect(minter)
.mint(optimismMultisig.address, initialSupply)
await fastForwardDays(369)
const inflationAmount = initialSupply.mul(200).div(1000).add(1)
await expect(
mintManager
.connect(minter)
.mint(optimismMultisig.address, inflationAmount)
).to.be.revertedWith('OP: mint amount exceeds cap')
})
it('anyone can burn tokens for themselves', async () => {
await mintManager.connect(minter).mint(minter.address, initialSupply)
// Give userA 1000 tokens
const userBalance = ethers.BigNumber.from(10).pow(18).mul(100)
await governanceToken.connect(minter).transfer(userA.address, userBalance)
// Burn 200 tokens
await governanceToken.connect(userA).burn(200)
const balance = await governanceToken.balanceOf(userA.address)
expect(balance).to.equal(userBalance.sub(200))
})
it('users can burn tokens for others when approved', async () => {
await mintManager.connect(minter).mint(minter.address, initialSupply)
// Give userA 1000 tokens
const userBalance = ethers.BigNumber.from(10).pow(18).mul(1000)
await governanceToken.connect(minter).transfer(userA.address, userBalance)
// UserA approves UserB for 200 tokens
await governanceToken.connect(userA).approve(userB.address, 200)
// UserB can burn approved 200 tokens
await governanceToken.connect(userB).burnFrom(userA.address, 200)
const balance = await governanceToken.balanceOf(userA.address)
expect(balance).to.equal(userBalance.sub(200))
})
it("you cannot burn other users' tokens", async () => {
await mintManager.connect(minter).mint(minter.address, initialSupply)
// Give userA 1000 tokens
const userBalance = ethers.BigNumber.from(10).pow(18).mul(1000)
await governanceToken.connect(minter).transfer(userA.address, userBalance)
// UserB fails to burn UserA's tokens
await expect(
governanceToken.connect(userB).burnFrom(userA.address, 200)
).to.be.revertedWith('ERC20: insufficient allowance')
const balance = await governanceToken.balanceOf(userA.address)
expect(balance).to.equal(userBalance)
})
})
describe('Permit tests', async () => {
it('can use permit for approve', async () => {
// Check there is no allowance set
const allowance = await governanceToken.allowance(
userA.address,
userB.address
)
expect(allowance).to.equal(0)
const privateKey = userA._signingKey().privateKey
const privateKeyBuffer = Buffer.from(privateKey.replace(/^0x/, ''), 'hex')
const nonceUserA = await governanceToken.nonces(userA.address)
const permittedAmount = ethers.BigNumber.from(10).pow(18).mul(1000)
const data = buildDataPermit(
network.chainId,
governanceToken.address,
userA.address,
userB.address,
permittedAmount.toString(),
nonceUserA.toNumber()
)
const { v, r, s } = fromRpcSig(
ethSigUtil.signTypedMessage(privateKeyBuffer, { data: data as any })
)
await governanceToken
.connect(userB)
.permit(
userA.address,
userB.address,
permittedAmount,
MAX_UINT256,
v,
r,
s
)
const allowanceAfter = await governanceToken.allowance(
userA.address,
userB.address
)
expect(allowanceAfter).to.equal(permittedAmount)
})
it('cannot use invalid signature', async () => {
const permittedAmount = ethers.BigNumber.from(10).pow(18).mul(1000)
const invalidAmount = permittedAmount.add(1000)
const privateKey = userA._signingKey().privateKey
const privateKeyBuffer = Buffer.from(privateKey.replace(/^0x/, ''), 'hex')
const nonceUserA = await governanceToken.nonces(userA.address)
const data = buildDataPermit(
network.chainId,
governanceToken.address,
userA.address,
userB.address,
permittedAmount.toString(),
nonceUserA.toNumber()
)
const { v, r, s } = fromRpcSig(
ethSigUtil.signTypedMessage(privateKeyBuffer, { data: data as any })
)
await expect(
governanceToken
.connect(userB)
.permit(
userA.address,
userB.address,
invalidAmount,
MAX_UINT256,
v,
r,
s
)
).to.be.revertedWith('ERC20Permit: invalid signature')
})
})
describe('Delegate voting tests', async () => {
let userABalance: BigNumber
beforeEach(async () => {
await mintManager.connect(minter).mint(minter.address, initialSupply)
// Give userA 1000 tokens
userABalance = ethers.BigNumber.from(10).pow(18).mul(1000)
await governanceToken
.connect(minter)
.transfer(userA.address, userABalance)
})
it('can delegate vote to self (with tx)', async () => {
let userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(ethers.constants.AddressZero)
await governanceToken.connect(userA).delegate(userA.address)
userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(userA.address)
})
it('can delegate vote to self (with signature)', async () => {
let userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(ethers.constants.AddressZero)
const privateKey = userA._signingKey().privateKey
const privateKeyBuffer = Buffer.from(privateKey.replace(/^0x/, ''), 'hex')
const nonce = await governanceToken.nonces(userA.address)
const data = buildDataDelegation(
network.chainId,
governanceToken.address,
userA.address,
nonce.toNumber()
)
const { v, r, s } = fromRpcSig(
ethSigUtil.signTypedMessage(privateKeyBuffer, { data: data as any })
)
await governanceToken
.connect(userA)
.delegateBySig(userA.address, nonce, MAX_UINT256, v, r, s)
userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(userA.address)
})
it('can delegate vote to third party (with tx)', async () => {
// Check the delegate for userA is 0 and their votes are 0
let userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(ethers.constants.AddressZero)
let userAVotes = await governanceToken.getVotes(userA.address)
expect(userAVotes).to.equal(0)
await governanceToken.connect(userA).delegate(userA.address)
userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(userA.address)
userAVotes = await governanceToken.getVotes(userA.address)
expect(userAVotes).to.equal(userABalance)
})
it('can delegate vote to third party (with signature)', async () => {
// Check the delegate for userA is 0
let userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(ethers.constants.AddressZero)
// Check the votes for both userA and userB are 0
let userAVotes = await governanceToken.getVotes(userA.address)
expect(userAVotes).to.equal(0)
let userBVotes = await governanceToken.getVotes(userB.address)
expect(userBVotes).to.equal(0)
const privateKey = userA._signingKey().privateKey
const privateKeyBuffer = Buffer.from(privateKey.replace(/^0x/, ''), 'hex')
const nonce = await governanceToken.nonces(userA.address)
const data = buildDataDelegation(
network.chainId,
governanceToken.address,
userB.address,
nonce.toNumber()
)
const { v, r, s } = fromRpcSig(
ethSigUtil.signTypedMessage(privateKeyBuffer, { data: data as any })
)
await governanceToken
.connect(userB)
.delegateBySig(userB.address, nonce, MAX_UINT256, v, r, s)
// Check the delegatee for userA is userB
userADelegate = await governanceToken.delegates(userA.address)
expect(userADelegate).to.equal(userB.address)
// Check the userA votes are 0 and userB has all of userA's votes (through delegation)
userAVotes = await governanceToken.getVotes(userA.address)
expect(userAVotes).to.equal(0)
userBVotes = await governanceToken.getVotes(userB.address)
expect(userBVotes).to.equal(userABalance)
})
})
})
import { ethers } from 'hardhat'
import ethSigUtil from 'eth-sig-util'
export const MAX_UINT256 = ethers.constants.MaxUint256.toString()
export const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
export const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
export const Delegation = [
{ name: 'delegatee', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'expiry', type: 'uint256' },
]
export const buildDataPermit = (
chainId: any,
verifyingContract: any,
owner: any,
spender: any,
value: any,
nonce: any,
deadline = MAX_UINT256
) => ({
primaryType: 'Permit',
types: { EIP712Domain, Permit },
domain: { name: 'Optimism', version: '1', chainId, verifyingContract },
message: { owner, spender, value, nonce, deadline },
})
export const buildDataDelegation = (
chainId: any,
verifyingContract: any,
delegatee: any,
nonce: any,
expiry = MAX_UINT256
) => ({
types: { EIP712Domain, Delegation },
domain: { name: 'Optimism', version: '1', chainId, verifyingContract },
primaryType: 'Delegation',
message: { delegatee, nonce, expiry },
})
export const domainSeparator = (
name: any,
version: any,
chainId: any,
verifyingContract: any
) => {
return (
'0x' +
ethSigUtil.TypedDataUtils.hashStruct(
'EIP712Domain',
{ name, version, chainId, verifyingContract },
{ EIP712Domain }
).toString('hex')
)
}
import { ethers } from 'hardhat'
export const SECONDS_IN_1_DAY = 24 * 60 * 60
export const SECONDS_IN_365_DAYS = 365 * 24 * 60 * 60
export const getBlockTimestamp = async (blockNumber: number) => {
const block = await ethers.provider.getBlock(blockNumber)
return block.timestamp
}
export const fastForwardDays = async (numberOfDays: number) => {
const latestBlock = await ethers.provider.getBlock('latest')
const timestampAfterXDays =
latestBlock.timestamp + numberOfDays * SECONDS_IN_1_DAY
await ethers.provider.send('evm_setNextBlockTimestamp', [timestampAfterXDays])
await ethers.provider.send('evm_mine', [])
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
...@@ -2595,6 +2595,19 @@ ...@@ -2595,6 +2595,19 @@
node-fetch "^2.6.0" node-fetch "^2.6.0"
semver "^6.3.0" semver "^6.3.0"
"@nomiclabs/hardhat-etherscan@^3.0.1":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.0.4.tgz#b12e3e226a5b73c4a66d0e6943f948bd093b2711"
integrity sha512-AZPlnyCYp3YObmhtsFo6RWgY/81fQKRF5h42iV22H4jz9MwP+SWeoB99YVPLnxId2fmAYu3VgCNeE9QpApv06g==
dependencies:
"@ethersproject/abi" "^5.1.2"
"@ethersproject/address" "^5.0.2"
cbor "^5.0.2"
debug "^4.1.1"
fs-extra "^7.0.1"
semver "^6.3.0"
undici "^4.14.1"
"@nomiclabs/hardhat-etherscan@^3.0.3": "@nomiclabs/hardhat-etherscan@^3.0.3":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.0.3.tgz#ca54a03351f3de41f9f5240e37bea9d64fa24e64" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.0.3.tgz#ca54a03351f3de41f9f5240e37bea9d64fa24e64"
...@@ -2608,7 +2621,7 @@ ...@@ -2608,7 +2621,7 @@
semver "^6.3.0" semver "^6.3.0"
undici "^4.14.1" undici "^4.14.1"
"@nomiclabs/hardhat-waffle@^2.0.0": "@nomiclabs/hardhat-waffle@^2.0.0", "@nomiclabs/hardhat-waffle@^2.0.2":
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz#9c538a09c5ed89f68f5fd2dc3f78f16ed1d6e0b1" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz#9c538a09c5ed89f68f5fd2dc3f78f16ed1d6e0b1"
integrity sha512-049PHSnI1CZq6+XTbrMbMv5NaL7cednTfPenx02k3cEh8wBMLa6ys++dBETJa6JjfwgA9nBhhHQ173LJv6k2Pg== integrity sha512-049PHSnI1CZq6+XTbrMbMv5NaL7cednTfPenx02k3cEh8wBMLa6ys++dBETJa6JjfwgA9nBhhHQ173LJv6k2Pg==
...@@ -2624,6 +2637,13 @@ ...@@ -2624,6 +2637,13 @@
"@types/sinon-chai" "^3.2.3" "@types/sinon-chai" "^3.2.3"
"@types/web3" "1.0.19" "@types/web3" "1.0.19"
"@nomiclabs/hardhat-web3@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-web3/-/hardhat-web3-2.0.0.tgz#2d9850cb285a2cebe1bd718ef26a9523542e52a9"
integrity sha512-zt4xN+D+fKl3wW2YlTX3k9APR3XZgPkxJYf36AcliJn3oujnKEVRZaHu0PhgLjO+gR+F/kiYayo9fgd2L8970Q==
dependencies:
"@types/bignumber.js" "^5.0.0"
"@npmcli/ci-detect@^1.0.0": "@npmcli/ci-detect@^1.0.0":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a"
...@@ -2812,6 +2832,11 @@ ...@@ -2812,6 +2832,11 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.3.2.tgz#ff80affd6d352dbe1bbc5b4e1833c41afd6283b6" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.3.2.tgz#ff80affd6d352dbe1bbc5b4e1833c41afd6283b6"
integrity sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg== integrity sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg==
"@openzeppelin/contracts@4.5.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.5.0.tgz#3fd75d57de172b3743cdfc1206883f56430409cc"
integrity sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA==
"@openzeppelin/contracts@4.6.0", "@openzeppelin/contracts@^4.5.0": "@openzeppelin/contracts@4.6.0", "@openzeppelin/contracts@^4.5.0":
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.6.0.tgz#c91cf64bc27f573836dba4122758b4743418c1b3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.6.0.tgz#c91cf64bc27f573836dba4122758b4743418c1b3"
...@@ -3191,6 +3216,13 @@ ...@@ -3191,6 +3216,13 @@
resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.2.tgz#ee81917fe38f770e29eec8139b6f16ee4a8b0a5f" resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.2.tgz#ee81917fe38f770e29eec8139b6f16ee4a8b0a5f"
integrity sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ== integrity sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ==
"@types/bignumber.js@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/bignumber.js/-/bignumber.js-5.0.0.tgz#d9f1a378509f3010a3255e9cc822ad0eeb4ab969"
integrity sha512-0DH7aPGCClywOFaxxjE6UwpN2kQYe9LwuDQMv+zYA97j5GkOMo8e66LYT+a8JYU7jfmUFRZLa9KycxHDsKXJCA==
dependencies:
bignumber.js "*"
"@types/bn.js@*", "@types/bn.js@^5.1.0": "@types/bn.js@*", "@types/bn.js@^5.1.0":
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68"
...@@ -3356,6 +3388,11 @@ ...@@ -3356,6 +3388,11 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw== integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
"@types/mocha@^9.1.1":
version "9.1.1"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
"@types/node-fetch@^2.5.5": "@types/node-fetch@^2.5.5":
version "2.5.10" version "2.5.10"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132"
...@@ -5073,6 +5110,11 @@ big.js@^5.2.2: ...@@ -5073,6 +5110,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@*:
version "9.0.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==
bignumber.js@^9.0.0, bignumber.js@^9.0.1: bignumber.js@^9.0.0, bignumber.js@^9.0.1:
version "9.0.1" version "9.0.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
...@@ -5605,7 +5647,7 @@ chai-as-promised@^7.1.1: ...@@ -5605,7 +5647,7 @@ chai-as-promised@^7.1.1:
dependencies: dependencies:
check-error "^1.0.2" check-error "^1.0.2"
chai@^4.2.0: chai@^4.2.0, chai@^4.3.6:
version "4.3.6" version "4.3.6"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c"
integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==
...@@ -6127,6 +6169,11 @@ commander@^9.0.0: ...@@ -6127,6 +6169,11 @@ commander@^9.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40" resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40"
integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw== integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==
commander@^9.3.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.3.0.tgz#f619114a5a2d2054e0d9ff1b31d5ccf89255e26b"
integrity sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==
comment-parser@1.1.6-beta.0: comment-parser@1.1.6-beta.0:
version "1.1.6-beta.0" version "1.1.6-beta.0"
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.6-beta.0.tgz#57e503b18d0a5bd008632dcc54b1f95c2fffe8f6" resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.6-beta.0.tgz#57e503b18d0a5bd008632dcc54b1f95c2fffe8f6"
...@@ -6499,6 +6546,11 @@ csv-parse@^4.15.3: ...@@ -6499,6 +6546,11 @@ csv-parse@^4.15.3:
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.0.tgz#b4c875e288a41f7ff917cb0d7d45880d563034f6" resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.0.tgz#b4c875e288a41f7ff917cb0d7d45880d563034f6"
integrity sha512-Zb4tGPANH4SW0LgC9+s9Mnequs9aqn7N3/pCqNbVjs2XhEF6yWNU2Vm4OGl1v2Go9nw8rXt87Cm2QN/o6Vpqgg== integrity sha512-Zb4tGPANH4SW0LgC9+s9Mnequs9aqn7N3/pCqNbVjs2XhEF6yWNU2Vm4OGl1v2Go9nw8rXt87Cm2QN/o6Vpqgg==
csv-parse@^5.0.4:
version "5.1.0"
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.1.0.tgz#e587e969bf0385ecf4f36f584ed5ddebba0237ab"
integrity sha512-JL+Q6YEikT2uoe57InjFFa6VejhSv0tDwOxeQ1bVQKeUC/NCnLAAZ8n3PzowPQQLuZ37fysDYZipB2UJkH9C6A==
csv-stringify@^5.6.2: csv-stringify@^5.6.2:
version "5.6.2" version "5.6.2"
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-5.6.2.tgz#e653783e2189c4c797fbb12abf7f4943c787caa9" resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-5.6.2.tgz#e653783e2189c4c797fbb12abf7f4943c787caa9"
...@@ -7623,7 +7675,7 @@ eslint@^7.27.0: ...@@ -7623,7 +7675,7 @@ eslint@^7.27.0:
text-table "^0.2.0" text-table "^0.2.0"
v8-compile-cache "^2.0.3" v8-compile-cache "^2.0.3"
eslint@^8.16.0: eslint@^8.16.0, eslint@^8.9.0:
version "8.16.0" version "8.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae"
integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==
...@@ -7886,6 +7938,16 @@ eth-sig-util@^1.4.2: ...@@ -7886,6 +7938,16 @@ eth-sig-util@^1.4.2:
ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git"
ethereumjs-util "^5.1.1" ethereumjs-util "^5.1.1"
eth-sig-util@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-3.0.1.tgz#8753297c83a3f58346bd13547b59c4b2cd110c96"
integrity sha512-0Us50HiGGvZgjtWTyAI/+qTzYPMLy5Q451D0Xy68bxq1QMWdoOddDwGvsqcFT27uohKgalM9z/yxplyt+mY2iQ==
dependencies:
ethereumjs-abi "^0.6.8"
ethereumjs-util "^5.1.1"
tweetnacl "^1.0.3"
tweetnacl-util "^0.15.0"
eth-tx-summary@^3.1.2: eth-tx-summary@^3.1.2:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/eth-tx-summary/-/eth-tx-summary-3.2.4.tgz#e10eb95eb57cdfe549bf29f97f1e4f1db679035c" resolved "https://registry.yarnpkg.com/eth-tx-summary/-/eth-tx-summary-3.2.4.tgz#e10eb95eb57cdfe549bf29f97f1e4f1db679035c"
...@@ -9561,7 +9623,7 @@ hardhat-gas-reporter@^1.0.4: ...@@ -9561,7 +9623,7 @@ hardhat-gas-reporter@^1.0.4:
eth-gas-reporter "^0.2.20" eth-gas-reporter "^0.2.20"
sha1 "^1.1.1" sha1 "^1.1.1"
hardhat-gas-reporter@^1.0.8: hardhat-gas-reporter@^1.0.7, hardhat-gas-reporter@^1.0.8:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.8.tgz#93ce271358cd748d9c4185dbb9d1d5525ec145e0" resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.8.tgz#93ce271358cd748d9c4185dbb9d1d5525ec145e0"
integrity sha512-1G5thPnnhcwLHsFnl759f2tgElvuwdkzxlI65fC9PwxYMEe9cmjkVAAWTf3/3y8uP6ZSPiUiOW8PgZnykmZe0g== integrity sha512-1G5thPnnhcwLHsFnl759f2tgElvuwdkzxlI65fC9PwxYMEe9cmjkVAAWTf3/3y8uP6ZSPiUiOW8PgZnykmZe0g==
...@@ -15550,7 +15612,7 @@ solidity-comments-extractor@^0.0.7: ...@@ -15550,7 +15612,7 @@ solidity-comments-extractor@^0.0.7:
resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19"
integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw==
solidity-coverage@^0.7.16: solidity-coverage@^0.7.16, solidity-coverage@^0.7.19:
version "0.7.21" version "0.7.21"
resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.21.tgz#20c5615a3a543086b243c2ca36e2951a75316b40" resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.21.tgz#20c5615a3a543086b243c2ca36e2951a75316b40"
integrity sha512-O8nuzJ9yXiKUx3NdzVvHrUW0DxoNVcGzq/I7NzewNO9EZE3wYAQ4l8BwcnV64r4aC/HB6Vnw/q2sF0BQHv/3fg== integrity sha512-O8nuzJ9yXiKUx3NdzVvHrUW0DxoNVcGzq/I7NzewNO9EZE3wYAQ4l8BwcnV64r4aC/HB6Vnw/q2sF0BQHv/3fg==
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment