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()
})
)
This diff is collapsed.
This diff is collapsed.
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