TC-128 native currency pools

This commit is contained in:
Drygin 2022-07-28 13:26:39 +03:00
parent 9d168730f2
commit 466e546734
16 changed files with 747 additions and 197 deletions

View File

@ -4,28 +4,28 @@
This repository contains: This repository contains:
1. `InstanceFactory` - instance factory for the creation new Tornado ERC20 pools 1. `InstanceFactory` - instance factory for the creation new Tornado ERC20/native pools
2. `InstanceFactoryWithRegistry` - governance proposal factory for the addition of new Tornado ERC20 instances to the Tornado router 2. `InstanceProposalCreator` - governance proposal factory for the addition of new Tornado instances to the Tornado router
### InstanceFactory ### InstanceFactory
Anyone can create a new ERC20 instance by calling `createInstanceClone` method of the factory with parameters: Anyone can create a new instance by calling `createInstanceClone` method of the factory with parameters:
1. `address token` - address of ERC20 token for a new instance 1. `address token` - address of ERC20 token for a new instance, zero address for the native instance
2. `uint256 denomination` - denomination for new instance (tokens can only be deposited in certain denominations into instances) 2. `uint256 denomination` - denomination for new instance (tokens can only be deposited in certain denominations into instances)
### InstanceFactoryWithRegistry ### InstanceProposalCreator
Anyone can create governance proposal for the addition of a new ERC20 instance by calling `createProposalApprove/createProposalPermit` method of the factory with parameters (proposal creation fee in TORN is charged from sender): Anyone can create governance proposal for the addition of a new instance by calling `createProposalApprove/createProposalPermit` method of the factory with parameters (proposal creation fee in TORN is charged from sender):
1. `address token` - address of ERC20 token for a new instance 1. `address token` - address of ERC20 token for a new instance, zero address for the native instance
2. `uint24 uniswapPoolSwappingFee` - fee value of Uniswap instance which will be used for `TORN/token` price determination. `3000` means 0.3% fee Uniswap pool. 2. `uint24 uniswapPoolSwappingFee` - fee value of Uniswap instance which will be used for `TORN/token` price determination. `3000` means 0.3% fee Uniswap pool. Zero value for the native instance.
3. `uint256[] denominations` - list of denominations for each new instance (tokens can only be deposited in certain denominations into instances). 3. `uint256[] denominations` - list of denominations for each new instance (tokens can only be deposited in certain denominations into instances).
4. `uint32[] protocolFees` - list of protocol fees for each new instance (this fee is only charged from registrated relayer during withdrawal process). `100` means 1% of instance denomination fee for withdrawal throw registrated relayer. 4. `uint32[] protocolFees` - list of protocol fees for each new instance (this fee is only charged from registrated relayer during withdrawal process). `100` means 1% of instance denomination fee for withdrawal throw registrated relayer.
## Factory parameters ## Factory parameters
### InstanceFactoryWithRegistry ### InstanceProposalCreator
1. `max number of new instances in one proposal` - the current version supports the addition of a maximum of 4 instances at once. 1. `max number of new instances in one proposal` - the current version supports the addition of a maximum of 4 instances at once.
2. `proposal creation fee` - this fee is charged from creator of proposal during `createProposalApprove/createProposalPermit` factory method execution. It can be changed by governance. Default value is stored in `config.js`. 2. `proposal creation fee` - this fee is charged from creator of proposal during `createProposalApprove/createProposalPermit` factory method execution. It can be changed by governance. Default value is stored in `config.js`.
@ -34,7 +34,7 @@ Anyone can create governance proposal for the addition of a new ERC20 instance b
## Warnings ## Warnings
1. This version of the factory creates a proposal for **immutable** Tornado instance initialization. 1. This version of the factory creates a proposal for **immutable** Tornado instance initialization.
2. For `InstanceFactoryWithRegistry` users should manually propose a proposal after its creation using the factory (in governance UI for example). As `propose()` method caller must have 1000 TORN locked in the governance. Moreover, the proposer can't propose more than one proposal simultaneously. 2. For `InstanceProposalCreator` users should manually propose a proposal after its creation using the factory (in governance UI for example). As `propose()` method caller must have 1000 TORN locked in the governance. Moreover, the proposer can't propose more than one proposal simultaneously.
## Tests ## Tests
@ -61,10 +61,12 @@ Check config.js for actual values.
With `salt` = `0x0000000000000000000000000000000000000000000000000000000047941987` address must be: With `salt` = `0x0000000000000000000000000000000000000000000000000000000047941987` address must be:
1. `MultipleInstanceFactory` - `0x8a1BCFc608DdF6c4715277a24310B9E8ecc4c110` 1. `SidechainInstanceFactory` - `0x0a0601c2E952aC53bb95b1A81764Fc730C9bBDd5`
2. `MultipleInstanceFactory proxy` - `0x0F9AE1d1ABbDd441B813ada0B038df130b2a6A86` 2. `SidechainInstanceFactory proxy` - `0xb4838f15185E4A7E2ED8534c89b7AC92aC927C9b`
3. `InstanceFactoryWithRegistry` - `0x557fB18B66088728Ea975136de46714983aa4f2E` 3. `InstanceFactory` - `0xa7610A8292850f516603bA0066A731109Dfe854E`
4. `InstanceFactoryWithRegistry proxy` - `0x4D1b1c294dA4D14aC0e0Eed7BcD4Db3fe2bDe4C3` 4. `InstanceFactory proxy` - `0x0f9B6646815f118d671084cd73Ea8713b18be354`
5. `InstanceProposalCreator` - `0x39F00b914f8DBD28082D1f1ae9b3210d7F7B404f`
6. `InstanceProposalCreator proxy` - `0xA2980DAfcdAf61C6d39FE63a6360907b69e840c8`
Check addresses with current config: Check addresses with current config:
@ -73,19 +75,19 @@ Check addresses with current config:
node -e 'require("./src/generateAddresses").generateWithLog()' node -e 'require("./src/generateAddresses").generateWithLog()'
``` ```
Deploy MultipleInstanceFactory: Deploy SidechainInstanceFactory:
```shell ```shell
yarn hardhat run scripts/deployMultipleInstanceFactory.js --network mainnet yarn hardhat run scripts/deploySidechainInstanceFactory.js --network mainnet
``` ```
Deploy InstanceFactoryWithRegistry: Deploy InstanceProposalCreator:
```shell ```shell
yarn hardhat run scripts/deployInstanceFactoryWithRegistry.js --network mainnet yarn hardhat run scripts/deployInstanceProposalCreator.js --network mainnet
``` ```
Verify InstanceFactory on Etherscan: Verify on Etherscan:
``` ```
yarn hardhat verify --network <network-name> <contract-address> <constructor-arguments> yarn hardhat verify --network <network-name> <contract-address> <constructor-arguments>

View File

@ -4,10 +4,10 @@ pragma solidity 0.7.6;
pragma abicoder v2; pragma abicoder v2;
import "./interfaces/IInstanceRegistry.sol"; import "./interfaces/IInstanceRegistry.sol";
import "./InstanceFactory.sol"; import "./interfaces/IInstanceFactory.sol";
contract AddInstanceProposal { contract AddInstanceProposal {
InstanceFactory public immutable instanceFactory; IInstanceFactory public immutable instanceFactory;
IInstanceRegistry public immutable instanceRegistry; IInstanceRegistry public immutable instanceRegistry;
address public immutable token; address public immutable token;
uint24 public immutable uniswapPoolSwappingFee; uint24 public immutable uniswapPoolSwappingFee;
@ -32,7 +32,7 @@ contract AddInstanceProposal {
uint256[] memory _denominations, uint256[] memory _denominations,
uint32[] memory _protocolFees uint32[] memory _protocolFees
) { ) {
instanceFactory = InstanceFactory(_instanceFactory); instanceFactory = IInstanceFactory(_instanceFactory);
instanceRegistry = IInstanceRegistry(_instanceRegistry); instanceRegistry = IInstanceRegistry(_instanceRegistry);
token = _token; token = _token;
uniswapPoolSwappingFee = _uniswapPoolSwappingFee; uniswapPoolSwappingFee = _uniswapPoolSwappingFee;
@ -57,7 +57,7 @@ contract AddInstanceProposal {
address instance = instanceFactory.createInstanceClone(denominationByIndex(i), token); address instance = instanceFactory.createInstanceClone(denominationByIndex(i), token);
IInstanceRegistry.Instance memory newInstanceData = IInstanceRegistry.Instance( IInstanceRegistry.Instance memory newInstanceData = IInstanceRegistry.Instance(
true, token != address(0),
IERC20(token), IERC20(token),
IInstanceRegistry.InstanceState.ENABLED, IInstanceRegistry.InstanceState.ENABLED,
uniswapPoolSwappingFee, uniswapPoolSwappingFee,

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
pragma abicoder v2;
import "tornado-core/contracts/ETHTornado.sol";
contract ETHTornadoCloneable is ETHTornado {
constructor(address verifier, address hasher) ETHTornado(IVerifier(verifier), IHasher(hasher), 1, 1) {}
function init(uint256 _denomination, uint32 _merkleTreeHeight) external {
require(denomination == 0 && levels == 0, "already initialized");
require(_denomination > 0, "denomination should be greater than 0");
denomination = _denomination;
require(_merkleTreeHeight > 0, "_levels should be greater than zero");
require(_merkleTreeHeight < 32, "_levels should be less than 32");
levels = _merkleTreeHeight;
for (uint32 i = 0; i < _merkleTreeHeight; i++) {
filledSubtrees[i] = zeros(i);
}
roots[0] = zeros(_merkleTreeHeight - 1);
}
}

View File

@ -7,19 +7,21 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol";
import "@openzeppelin/contracts/proxy/Clones.sol"; import "@openzeppelin/contracts/proxy/Clones.sol";
import "./ERC20TornadoCloneable.sol"; import "./ERC20TornadoCloneable.sol";
import "./ETHTornadoCloneable.sol";
contract InstanceFactory is Initializable { contract InstanceFactory is Initializable {
using Clones for address; using Clones for address;
using Address for address; using Address for address;
address public admin; address public admin;
address public implementation; address public ERC20Impl;
address public nativeCurImpl;
address public verifier; address public verifier;
address public hasher; address public hasher;
uint32 public merkleTreeHeight; uint32 public merkleTreeHeight;
event NewTreeHeightSet(uint32 indexed newTreeHeight); event NewTreeHeightSet(uint32 indexed newTreeHeight);
event NewImplementationSet(address indexed newImplemenentation, address verifier, address hasher); event NewImplementationSet(address indexed ERC20Impl, address indexed nativeCurImpl, address verifier, address hasher);
event NewInstanceCloneCreated(address indexed clone); event NewInstanceCloneCreated(address indexed clone);
modifier onlyAdmin() { modifier onlyAdmin() {
@ -43,30 +45,49 @@ contract InstanceFactory is Initializable {
merkleTreeHeight = _merkleTreeHeight; merkleTreeHeight = _merkleTreeHeight;
admin = _admin; admin = _admin;
ERC20TornadoCloneable implContract = new ERC20TornadoCloneable(_verifier, _hasher); ERC20TornadoCloneable ERC20ImplContract = new ERC20TornadoCloneable(_verifier, _hasher);
implementation = address(implContract); ERC20Impl = address(ERC20ImplContract);
ETHTornadoCloneable nativeCurImplContract = new ETHTornadoCloneable(_verifier, _hasher);
nativeCurImpl = address(nativeCurImplContract);
} }
/** /**
* @dev Creates new Tornado instance. * @dev Creates new Tornado instance.
* @param _denomination denomination of new Tornado instance * @param _denomination denomination of new Tornado instance
* @param _token address of ERC20 token for a new instance * @param _token address of ERC20 token for a new instance, if zero address, then it will be ETH
*/ */
function createInstanceClone(uint256 _denomination, address _token) public virtual returns (address) { function createInstanceClone(uint256 _denomination, address _token) public virtual onlyAdmin returns (address clone) {
return _createInstanceClone(_denomination, _token);
}
function _createInstanceClone(uint256 _denomination, address _token) internal returns (address clone) {
bytes32 salt = keccak256(abi.encodePacked(_denomination, _token)); bytes32 salt = keccak256(abi.encodePacked(_denomination, _token));
address newClone = implementation.predictDeterministicAddress(salt); if (_token == address(0)) {
if (!newClone.isContract()) { clone = nativeCurImpl.predictDeterministicAddress(salt);
implementation.cloneDeterministic(salt); if (!clone.isContract()) {
emit NewInstanceCloneCreated(newClone); nativeCurImpl.cloneDeterministic(salt);
ERC20TornadoCloneable(newClone).init(_denomination, merkleTreeHeight, _token); emit NewInstanceCloneCreated(clone);
ETHTornadoCloneable(clone).init(_denomination, merkleTreeHeight);
} }
return newClone; } else {
clone = ERC20Impl.predictDeterministicAddress(salt);
if (!clone.isContract()) {
ERC20Impl.cloneDeterministic(salt);
emit NewInstanceCloneCreated(clone);
ERC20TornadoCloneable(clone).init(_denomination, merkleTreeHeight, _token);
}
}
return clone;
} }
function getInstanceAddress(uint256 _denomination, address _token) public view returns (address) { function getInstanceAddress(uint256 _denomination, address _token) public view returns (address) {
bytes32 salt = keccak256(abi.encodePacked(_denomination, _token)); bytes32 salt = keccak256(abi.encodePacked(_denomination, _token));
return implementation.predictDeterministicAddress(salt); if (_token == address(0)) {
return nativeCurImpl.predictDeterministicAddress(salt);
} else {
return ERC20Impl.predictDeterministicAddress(salt);
}
} }
function setMerkleTreeHeight(uint32 _merkleTreeHeight) external onlyAdmin { function setMerkleTreeHeight(uint32 _merkleTreeHeight) external onlyAdmin {
@ -77,7 +98,8 @@ contract InstanceFactory is Initializable {
function generateNewImplementation(address _verifier, address _hasher) external onlyAdmin { function generateNewImplementation(address _verifier, address _hasher) external onlyAdmin {
verifier = _verifier; verifier = _verifier;
hasher = _hasher; hasher = _hasher;
implementation = address(new ERC20TornadoCloneable(_verifier, _hasher)); ERC20Impl = address(new ERC20TornadoCloneable(_verifier, _hasher));
emit NewImplementationSet(implementation, _verifier, _hasher); nativeCurImpl = address(new ETHTornadoCloneable(_verifier, _hasher));
emit NewImplementationSet(ERC20Impl, nativeCurImpl, _verifier, _hasher);
} }
} }

View File

@ -5,17 +5,19 @@ pragma abicoder v2;
import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import "./AddInstanceProposal.sol"; import "./AddInstanceProposal.sol";
import "./InstanceFactory.sol"; import "./interfaces/IInstanceFactory.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol"; import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol";
import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import { IUniswapV3PoolState } from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol"; import { IUniswapV3PoolState } from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol";
import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol";
contract InstanceFactoryWithRegistry is InstanceFactory { contract InstanceProposalCreator is Initializable {
using Address for address; using Address for address;
address public immutable governance; address public immutable governance;
address public immutable torn; address public immutable torn;
IInstanceFactory public immutable instanceFactory;
address public immutable instanceRegistry; address public immutable instanceRegistry;
IUniswapV3Factory public immutable UniswapV3Factory; IUniswapV3Factory public immutable UniswapV3Factory;
address public immutable WETH; address public immutable WETH;
@ -36,12 +38,14 @@ contract InstanceFactoryWithRegistry is InstanceFactory {
constructor( constructor(
address _governance, address _governance,
address _instanceFactory,
address _instanceRegistry, address _instanceRegistry,
address _torn, address _torn,
address _UniswapV3Factory, address _UniswapV3Factory,
address _WETH address _WETH
) { ) {
governance = _governance; governance = _governance;
instanceFactory = IInstanceFactory(_instanceFactory);
instanceRegistry = _instanceRegistry; instanceRegistry = _instanceRegistry;
torn = _torn; torn = _torn;
UniswapV3Factory = IUniswapV3Factory(_UniswapV3Factory); UniswapV3Factory = IUniswapV3Factory(_UniswapV3Factory);
@ -53,28 +57,11 @@ contract InstanceFactoryWithRegistry is InstanceFactory {
* @dev this contract will be deployed behind a proxy and should not assign values at logic address, * @dev this contract will be deployed behind a proxy and should not assign values at logic address,
* params left out because self explainable * params left out because self explainable
* */ * */
function initialize( function initialize(uint16 _TWAPSlotsMin, uint256 _creationFee) external initializer {
address _verifier,
address _hasher,
uint32 _merkleTreeHeight,
address _governance,
uint16 _TWAPSlotsMin,
uint256 _creationFee
) external initializer {
initialize(_verifier, _hasher, _merkleTreeHeight, _governance);
TWAPSlotsMin = _TWAPSlotsMin; TWAPSlotsMin = _TWAPSlotsMin;
creationFee = _creationFee; creationFee = _creationFee;
} }
/**
* @dev Creates new Tornado instances. Throws if called by any account other than the Governance.
* @param _denomination denomination of new Tornado instance
* @param _token address of ERC20 token for a new instance
*/
function createInstanceClone(uint256 _denomination, address _token) public override onlyGovernance returns (address) {
return super.createInstanceClone(_denomination, _token);
}
/** /**
* @dev Creates AddInstanceProposal with approve. * @dev Creates AddInstanceProposal with approve.
* @param _token address of ERC20 token for a new instance * @param _token address of ERC20 token for a new instance
@ -125,14 +112,14 @@ contract InstanceFactoryWithRegistry is InstanceFactory {
uint256[] memory _denominations, uint256[] memory _denominations,
uint32[] memory _protocolFees uint32[] memory _protocolFees
) internal returns (address) { ) internal returns (address) {
require(_token.isContract(), "Token is not contract"); require(_token == address(0) || _token.isContract(), "Token is not contract");
require(_denominations.length > 0, "Empty denominations"); require(_denominations.length > 0, "Empty denominations");
require(_denominations.length == _protocolFees.length, "Incorrect denominations/fees length"); require(_denominations.length == _protocolFees.length, "Incorrect denominations/fees length");
// check Uniswap Pool // check Uniswap Pool
for (uint8 i = 0; i < _protocolFees.length; i++) { for (uint8 i = 0; i < _protocolFees.length; i++) {
if (_protocolFees[i] > 0) {
require(_protocolFees[i] <= 10000, "Protocol fee is more than 100%"); require(_protocolFees[i] <= 10000, "Protocol fee is more than 100%");
if (_protocolFees[i] > 0 && _token != address(0)) {
// pool exists // pool exists
address poolAddr = UniswapV3Factory.getPool(_token, WETH, _uniswapPoolSwappingFee); address poolAddr = UniswapV3Factory.getPool(_token, WETH, _uniswapPoolSwappingFee);
require(poolAddr != address(0), "Uniswap pool is not exist"); require(poolAddr != address(0), "Uniswap pool is not exist");
@ -144,19 +131,26 @@ contract InstanceFactoryWithRegistry is InstanceFactory {
} }
address proposal = address( address proposal = address(
new AddInstanceProposal(address(this), instanceRegistry, _token, _uniswapPoolSwappingFee, _denominations, _protocolFees) new AddInstanceProposal(
address(instanceFactory),
instanceRegistry,
_token,
_uniswapPoolSwappingFee,
_denominations,
_protocolFees
)
); );
emit NewGovernanceProposalCreated(proposal); emit NewGovernanceProposalCreated(proposal);
return proposal; return proposal;
} }
function setCreationFee(uint256 _creationFee) external onlyAdmin { function setCreationFee(uint256 _creationFee) external onlyGovernance {
creationFee = _creationFee; creationFee = _creationFee;
emit NewCreationFeeSet(_creationFee); emit NewCreationFeeSet(_creationFee);
} }
function setTWAPSlotsMin(uint16 _TWAPSlotsMin) external onlyAdmin { function setTWAPSlotsMin(uint16 _TWAPSlotsMin) external onlyGovernance {
TWAPSlotsMin = _TWAPSlotsMin; TWAPSlotsMin = _TWAPSlotsMin;
emit NewTWAPSlotsMinSet(_TWAPSlotsMin); emit NewTWAPSlotsMinSet(_TWAPSlotsMin);
} }

View File

@ -1,20 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
import "./InstanceFactory.sol";
contract MultipleInstanceFactory is InstanceFactory {
/**
* @dev Creates new Tornado instances.
* @param _token address of ERC20 token for a new instance
* @param _denominations list of denominations for each new instance
*/
function createInstanceClones(address _token, uint256[] memory _denominations) external returns (address[] memory) {
address[] memory newClones = new address[](_denominations.length);
for (uint256 i = 0; i < _denominations.length; i++) {
newClones[i] = createInstanceClone(_denominations[i], _token);
}
return newClones;
}
}

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
import "./InstanceFactory.sol";
contract SidechainInstanceFactory is InstanceFactory {
/**
* @dev Creates new Tornado instance. Overriding to move onlyAdmin check for sidechains.
* @param _denomination denomination of new Tornado instance
* @param _token address of ERC20 token for a new instance, if zero address, then it will be ETH
*/
function createInstanceClone(uint256 _denomination, address _token) public override returns (address clone) {
return _createInstanceClone(_denomination, _token);
}
/**
* @dev Creates new Tornado instances.
* @param _token address of ERC20 token for a new instance
* @param _denominations list of denominations for each new instance
*/
function createInstanceClones(address _token, uint256[] memory _denominations) external returns (address[] memory) {
address[] memory newClones = new address[](_denominations.length);
for (uint256 i = 0; i < _denominations.length; i++) {
newClones[i] = _createInstanceClone(_denominations[i], _token);
}
return newClones;
}
}

View File

@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
pragma abicoder v2;
interface IInstanceFactory {
function createInstanceClone(uint256 denomination, address token) external returns (address);
}

View File

@ -3,6 +3,7 @@ require('@nomiclabs/hardhat-waffle')
require('@nomiclabs/hardhat-etherscan') require('@nomiclabs/hardhat-etherscan')
require('hardhat-log-remover') require('hardhat-log-remover')
require('solidity-coverage') require('solidity-coverage')
require('hardhat-contract-sizer')
/** /**
* @type import('hardhat/config').HardhatUserConfig * @type import('hardhat/config').HardhatUserConfig

View File

@ -34,6 +34,7 @@
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"ethereum-waffle": "^3.4.0", "ethereum-waffle": "^3.4.0",
"hardhat": "^2.4.3", "hardhat": "^2.4.3",
"hardhat-contract-sizer": "^2.6.1",
"hardhat-log-remover": "^2.0.2", "hardhat-log-remover": "^2.0.2",
"mocha-lcov-reporter": "^1.3.0", "mocha-lcov-reporter": "^1.3.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",

View File

@ -14,14 +14,14 @@ async function deploy({ address, bytecode, singletonFactory }) {
async function main() { async function main() {
const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory)
const contracts = await generate() const contracts = await generate()
await deploy({ ...contracts.factoryWithRegistryContract.implementation, singletonFactory }) await deploy({ ...contracts.factory.implementation, singletonFactory })
await deploy({ ...contracts.factoryWithRegistryContract.proxy, singletonFactory }) console.log(`Instance factory contract have been deployed on ${contracts.factory.implementation.address}`)
console.log( await deploy({ ...contracts.factory.proxy, singletonFactory })
`Instance factory with registry contract have been deployed on ${contracts.factoryWithRegistryContract.implementation.address} address`, console.log(`Instance factory proxy contract have been deployed on ${contracts.factory.proxy.address}`)
) await deploy({ ...contracts.proposalCreator.implementation, singletonFactory })
console.log( console.log(`Proposal creator have been deployed on ${contracts.proposalCreator.implementation.address}`)
`Instance factory with registry proxy contract have been deployed on ${contracts.factoryWithRegistryContract.proxy.address} address`, await deploy({ ...contracts.proposalCreator.proxy, singletonFactory })
) console.log(`Proposal creator proxy have been deployed on ${contracts.proposalCreator.proxy.address}`)
} }
main() main()

View File

@ -14,13 +14,13 @@ async function deploy({ address, bytecode, singletonFactory }) {
async function main() { async function main() {
const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory)
const contracts = await generate() const contracts = await generate()
await deploy({ ...contracts.factoryContract.implementation, singletonFactory }) await deploy({ ...contracts.sidechainFactory.implementation, singletonFactory })
await deploy({ ...contracts.factoryContract.proxy, singletonFactory }) await deploy({ ...contracts.sidechainFactory.proxy, singletonFactory })
console.log( console.log(
`MultipleInstanceFactory contract have been deployed on ${contracts.factoryContract.implementation.address} address`, `SidechainInstanceFactory contract have been deployed on ${contracts.sidechainFactory.implementation.address} address`,
) )
console.log( console.log(
`MultipleInstanceFactory proxy contract have been deployed on ${contracts.factoryContract.proxy.address} address`, `SidechainInstanceFactory proxy contract have been deployed on ${contracts.sidechainFactory.proxy.address} address`,
) )
} }

View File

@ -36,52 +36,62 @@ async function upgradableContract({ contractName, implConstructorArgs, proxyCons
} }
async function generate(config = defaultConfig) { async function generate(config = defaultConfig) {
// factory contract ----------------------------------------------- // sidechain factory contract -------------------------------------
const FactoryFactory = await ethers.getContractFactory('MultipleInstanceFactory') const SidechainFactory = await ethers.getContractFactory('SidechainInstanceFactory')
const FactoryInitData = FactoryFactory.interface.encodeFunctionData('initialize', [ const SidechainFactoryInitData = SidechainFactory.interface.encodeFunctionData('initialize', [
config.verifier, config.verifier,
config.hasher, config.hasher,
config.merkleTreeHeight, config.merkleTreeHeight,
config.admin, config.admin,
]) ])
const factoryContract = await upgradableContract({ const sidechainFactory = await upgradableContract({
contractName: 'MultipleInstanceFactory', contractName: 'SidechainInstanceFactory',
implConstructorArgs: [], implConstructorArgs: [],
proxyConstructorArgs: [config.admin, FactoryInitData], proxyConstructorArgs: [config.admin, SidechainFactoryInitData],
salt: config.salt, salt: config.salt,
}) })
// factory with registry contract --------------------------------- // factory with registry contract ---------------------------------
const FactoryWithRegistryFactory = await ethers.getContractFactory('InstanceFactoryWithRegistry') const Factory = await ethers.getContractFactory('InstanceFactory')
const FactoryWithRegistryInitData = FactoryWithRegistryFactory.interface.encodeFunctionData( const FactoryInitData = Factory.interface.encodeFunctionData('initialize', [
'initialize(address,address,uint32,address,uint16,uint256)',
[
config.verifier, config.verifier,
config.hasher, config.hasher,
config.merkleTreeHeight, config.merkleTreeHeight,
config.governance, config.governance,
])
const factory = await upgradableContract({
contractName: 'InstanceFactory',
implConstructorArgs: [],
proxyConstructorArgs: [config.governance, FactoryInitData],
salt: config.salt,
})
const ProposalCreator = await ethers.getContractFactory('InstanceProposalCreator')
const ProposalCreatorInitData = ProposalCreator.interface.encodeFunctionData('initialize', [
config.TWAPSlotsMin, config.TWAPSlotsMin,
config.creationFee, config.creationFee,
], ])
)
const factoryWithRegistryContract = await upgradableContract({ const proposalCreator = await upgradableContract({
contractName: 'InstanceFactoryWithRegistry', contractName: 'InstanceProposalCreator',
implConstructorArgs: [ implConstructorArgs: [
config.governance, config.governance,
factory.proxy.address,
config.instanceRegistry, config.instanceRegistry,
config.TORN, config.TORN,
config.UniswapV3Factory, config.UniswapV3Factory,
config.WETH, config.WETH,
], ],
proxyConstructorArgs: [config.governance, FactoryWithRegistryInitData], proxyConstructorArgs: [config.governance, ProposalCreatorInitData],
salt: config.salt, salt: config.salt,
}) })
const result = { const result = {
factoryContract, sidechainFactory,
factoryWithRegistryContract, factory,
proposalCreator,
} }
return result return result
@ -89,16 +99,12 @@ async function generate(config = defaultConfig) {
async function generateWithLog() { async function generateWithLog() {
const contracts = await generate() const contracts = await generate()
console.log('MultipleInstanceFactory contract: ', contracts.factoryContract.implementation.address) console.log('SidechainInstanceFactory contract: ', contracts.sidechainFactory.implementation.address)
console.log('MultipleInstanceFactory proxy contract: ', contracts.factoryContract.proxy.address) console.log('SidechainInstanceFactory proxy contract: ', contracts.sidechainFactory.proxy.address)
console.log( console.log('Instance factory contract: ', contracts.factory.implementation.address)
'Instance factory with registry contract: ', console.log('Instance factory proxy contract: ', contracts.factory.proxy.address)
contracts.factoryWithRegistryContract.implementation.address, console.log('Proposal creator contract: ', contracts.proposalCreator.implementation.address)
) console.log('Proposal creator proxy contract: ', contracts.proposalCreator.proxy.address)
console.log(
'Instance factory with registry proxy contract: ',
contracts.factoryWithRegistryContract.proxy.address,
)
return contracts return contracts
} }

View File

@ -51,31 +51,37 @@ describe('Instance Factory With Registry Tests', () => {
config.instanceRegistry, config.instanceRegistry,
) )
// deploy InstanceFactoryWithRegistry with CREATE2 // deploy InstanceProposalCreator with CREATE2
const singletonFactory = await ethers.getContractAt( const singletonFactory = await ethers.getContractAt(
'SingletonFactory', 'SingletonFactory',
config.singletonFactoryVerboseWrapper, config.singletonFactoryVerboseWrapper,
) )
const contracts = await generate() const contracts = await generate()
if ( if ((await ethers.provider.getCode(contracts.factory.implementation.address)) == '0x') {
(await ethers.provider.getCode(contracts.factoryWithRegistryContract.implementation.address)) == '0x' await singletonFactory.deploy(contracts.factory.implementation.bytecode, config.salt, {
) {
await singletonFactory.deploy(
contracts.factoryWithRegistryContract.implementation.bytecode,
config.salt,
{
gasLimit: config.deployGasLimit,
},
)
}
if ((await ethers.provider.getCode(contracts.factoryWithRegistryContract.proxy.address)) == '0x') {
await singletonFactory.deploy(contracts.factoryWithRegistryContract.proxy.bytecode, config.salt, {
gasLimit: config.deployGasLimit, gasLimit: config.deployGasLimit,
}) })
} }
const instanceFactory = await ethers.getContractAt( if ((await ethers.provider.getCode(contracts.factory.proxy.address)) == '0x') {
'InstanceFactoryWithRegistry', await singletonFactory.deploy(contracts.factory.proxy.bytecode, config.salt, {
contracts.factoryWithRegistryContract.proxy.address, gasLimit: config.deployGasLimit,
})
}
const instanceFactory = await ethers.getContractAt('InstanceFactory', contracts.factory.proxy.address)
if ((await ethers.provider.getCode(contracts.proposalCreator.implementation.address)) == '0x') {
await singletonFactory.deploy(contracts.proposalCreator.implementation.bytecode, config.salt, {
gasLimit: config.deployGasLimit,
})
}
if ((await ethers.provider.getCode(contracts.proposalCreator.proxy.address)) == '0x') {
await singletonFactory.deploy(contracts.proposalCreator.proxy.bytecode, config.salt, {
gasLimit: config.deployGasLimit,
})
}
const proposalCreator = await ethers.getContractAt(
'InstanceProposalCreator',
contracts.proposalCreator.proxy.address,
) )
return { return {
@ -90,73 +96,80 @@ describe('Instance Factory With Registry Tests', () => {
compToken, compToken,
instanceRegistry, instanceRegistry,
instanceFactory, instanceFactory,
proposalCreator,
} }
} }
it('Should have initialized all successfully', async function () { it('Should have initialized all successfully', async function () {
const { sender, gov, tornToken, instanceRegistry, instanceFactory } = await loadFixture(fixture) const { sender, gov, tornToken, instanceRegistry, instanceFactory, proposalCreator } = await loadFixture(
fixture,
)
expect(sender.address).to.exist expect(sender.address).to.exist
expect(gov.address).to.exist expect(gov.address).to.exist
expect(tornToken.address).to.exist expect(tornToken.address).to.exist
expect(instanceRegistry.address).to.exist expect(instanceRegistry.address).to.exist
expect(instanceFactory.address).to.exist expect(instanceFactory.address).to.exist
expect(proposalCreator.address).to.exist
}) })
it('Should set correct params for factory', async function () { it('Should set correct params for factory', async function () {
const { instanceFactory } = await loadFixture(fixture) const { instanceFactory, proposalCreator } = await loadFixture(fixture)
expect(await instanceFactory.governance()).to.be.equal(config.governance) expect(await proposalCreator.governance()).to.be.equal(config.governance)
expect(await instanceFactory.verifier()).to.be.equal(config.verifier) expect(await instanceFactory.verifier()).to.be.equal(config.verifier)
expect(await instanceFactory.hasher()).to.be.equal(config.hasher) expect(await instanceFactory.hasher()).to.be.equal(config.hasher)
expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight)
expect(await instanceFactory.implementation()).to.exist expect(await instanceFactory.ERC20Impl()).to.exist
expect(await instanceFactory.creationFee()).to.be.equal(config.creationFee) expect(await instanceFactory.nativeCurImpl()).to.exist
expect(await instanceFactory.torn()).to.be.equal(config.TORN) expect(await proposalCreator.creationFee()).to.be.equal(config.creationFee)
expect(await instanceFactory.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin) expect(await proposalCreator.torn()).to.be.equal(config.TORN)
expect(await instanceFactory.WETH()).to.be.equal(config.WETH) expect(await proposalCreator.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin)
expect(await instanceFactory.UniswapV3Factory()).to.be.equal(config.UniswapV3Factory) expect(await proposalCreator.WETH()).to.be.equal(config.WETH)
expect(await proposalCreator.UniswapV3Factory()).to.be.equal(config.UniswapV3Factory)
}) })
it('Governance should be able to set factory params', async function () { it('Governance should be able to set factory/proposalCreator params', async function () {
let { instanceFactory, gov } = await loadFixture(fixture) let { instanceFactory, proposalCreator, gov } = await loadFixture(fixture)
await expect(instanceFactory.setMerkleTreeHeight(1)).to.be.reverted await expect(instanceFactory.setMerkleTreeHeight(1)).to.be.reverted
const govSigner = await getSignerFromAddress(gov.address) const govSigner = await getSignerFromAddress(gov.address)
instanceFactory = await instanceFactory.connect(govSigner) instanceFactory = await instanceFactory.connect(govSigner)
proposalCreator = await proposalCreator.connect(govSigner)
await instanceFactory.generateNewImplementation(addressZero, addressZero) await instanceFactory.generateNewImplementation(addressZero, addressZero)
await instanceFactory.setMerkleTreeHeight(1) await instanceFactory.setMerkleTreeHeight(1)
await instanceFactory.setCreationFee(0) await proposalCreator.setCreationFee(0)
await instanceFactory.setTWAPSlotsMin(0) await proposalCreator.setTWAPSlotsMin(0)
expect(await instanceFactory.verifier()).to.be.equal(addressZero) expect(await instanceFactory.verifier()).to.be.equal(addressZero)
expect(await instanceFactory.hasher()).to.be.equal(addressZero) expect(await instanceFactory.hasher()).to.be.equal(addressZero)
expect(await instanceFactory.merkleTreeHeight()).to.be.equal(1) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(1)
expect(await instanceFactory.creationFee()).to.be.equal(0) expect(await proposalCreator.creationFee()).to.be.equal(0)
expect(await instanceFactory.TWAPSlotsMin()).to.be.equal(0) expect(await proposalCreator.TWAPSlotsMin()).to.be.equal(0)
await instanceFactory.generateNewImplementation(config.verifier, config.hasher) await instanceFactory.generateNewImplementation(config.verifier, config.hasher)
await instanceFactory.setMerkleTreeHeight(config.merkleTreeHeight) await instanceFactory.setMerkleTreeHeight(config.merkleTreeHeight)
await instanceFactory.setCreationFee(config.creationFee) await proposalCreator.setCreationFee(config.creationFee)
await instanceFactory.setTWAPSlotsMin(config.TWAPSlotsMin) await proposalCreator.setTWAPSlotsMin(config.TWAPSlotsMin)
expect(await instanceFactory.verifier()).to.be.equal(config.verifier) expect(await instanceFactory.verifier()).to.be.equal(config.verifier)
expect(await instanceFactory.hasher()).to.be.equal(config.hasher) expect(await instanceFactory.hasher()).to.be.equal(config.hasher)
expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight)
expect(await instanceFactory.creationFee()).to.be.equal(config.creationFee) expect(await proposalCreator.creationFee()).to.be.equal(config.creationFee)
expect(await instanceFactory.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin) expect(await proposalCreator.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin)
}) })
it('Should successfully deploy/propose/execute proposal - add instance', async function () { it('Should successfully deploy/propose/execute proposal - add instance', async function () {
let { sender, instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) let { sender, instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } =
await loadFixture(fixture)
// deploy proposal ---------------------------------------------- // deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(instanceFactory.address, config.creationFee) await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(() => await expect(() =>
instanceFactory proposalCreator
.connect(sender) .connect(sender)
.createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]),
).to.changeTokenBalances( ).to.changeTokenBalances(
@ -165,7 +178,7 @@ describe('Instance Factory With Registry Tests', () => {
[BigNumber.from(0).sub(config.creationFee), config.creationFee], [BigNumber.from(0).sub(config.creationFee), config.creationFee],
) )
let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt( const proposal = await ethers.getContractAt(
'AddInstanceProposal', 'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)), ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
@ -235,7 +248,8 @@ describe('Instance Factory With Registry Tests', () => {
}) })
it('Should successfully deploy/propose/execute proposal - add instances', async function () { it('Should successfully deploy/propose/execute proposal - add instances', async function () {
let { sender, instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) let { sender, instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } =
await loadFixture(fixture)
const denominations = [ const denominations = [
ethers.utils.parseEther('1'), ethers.utils.parseEther('1'),
@ -249,17 +263,17 @@ describe('Instance Factory With Registry Tests', () => {
// deploy proposal ---------------------------------------------- // deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(instanceFactory.address, config.creationFee) await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(() => await expect(() =>
instanceFactory.connect(sender).createProposalApprove(config.COMP, 3000, denominations, protocolFees), proposalCreator.connect(sender).createProposalApprove(config.COMP, 3000, denominations, protocolFees),
).to.changeTokenBalances( ).to.changeTokenBalances(
tornToken, tornToken,
[sender, gov], [sender, gov],
[BigNumber.from(0).sub(config.creationFee), config.creationFee], [BigNumber.from(0).sub(config.creationFee), config.creationFee],
) )
let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt( const proposal = await ethers.getContractAt(
'AddInstanceProposal', 'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)), ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
@ -333,7 +347,9 @@ describe('Instance Factory With Registry Tests', () => {
}) })
it('Should successfully deploy proposal with permit', async function () { it('Should successfully deploy proposal with permit', async function () {
let { instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) let { instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(
fixture,
)
const privateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3' const privateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
const publicKey = '0x' + ethers.utils.computeAddress(Buffer.from(privateKey.slice(2), 'hex')) const publicKey = '0x' + ethers.utils.computeAddress(Buffer.from(privateKey.slice(2), 'hex'))
@ -358,7 +374,7 @@ describe('Instance Factory With Registry Tests', () => {
const curTimestamp = Math.trunc(new Date().getTime() / 1000) const curTimestamp = Math.trunc(new Date().getTime() / 1000)
const args = { const args = {
owner: sender, owner: sender,
spender: instanceFactory.address, spender: proposalCreator.address,
value: config.creationFee, value: config.creationFee,
nonce: 0, nonce: 0,
deadline: curTimestamp + 1000, deadline: curTimestamp + 1000,
@ -370,7 +386,7 @@ describe('Instance Factory With Registry Tests', () => {
expect(signer).to.equal(sender.address) expect(signer).to.equal(sender.address)
await expect(() => await expect(() =>
instanceFactory.createProposalPermit( proposalCreator.createProposalPermit(
config.COMP, config.COMP,
3000, 3000,
[ethers.utils.parseEther('100')], [ethers.utils.parseEther('100')],
@ -387,7 +403,7 @@ describe('Instance Factory With Registry Tests', () => {
[BigNumber.from(0).sub(config.creationFee), config.creationFee], [BigNumber.from(0).sub(config.creationFee), config.creationFee],
) )
let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt( const proposal = await ethers.getContractAt(
'AddInstanceProposal', 'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)), ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
@ -403,15 +419,15 @@ describe('Instance Factory With Registry Tests', () => {
}) })
it('Should deposit and withdraw into the new instance', async function () { it('Should deposit and withdraw into the new instance', async function () {
let { sender, instanceFactory, gov, tornWhale, tornToken, router, compToken, compWhale } = let { sender, proposalCreator, gov, tornWhale, tornToken, router, compToken, compWhale } =
await loadFixture(fixture) await loadFixture(fixture)
// deploy proposal ---------------------------------------------- // deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(instanceFactory.address, config.creationFee) await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(() => await expect(() =>
instanceFactory proposalCreator
.connect(sender) .connect(sender)
.createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]),
).to.changeTokenBalances( ).to.changeTokenBalances(
@ -420,7 +436,7 @@ describe('Instance Factory With Registry Tests', () => {
[BigNumber.from(0).sub(config.creationFee), config.creationFee], [BigNumber.from(0).sub(config.creationFee), config.creationFee],
) )
let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt( const proposal = await ethers.getContractAt(
'AddInstanceProposal', 'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)), ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
@ -486,36 +502,382 @@ describe('Instance Factory With Registry Tests', () => {
}) })
it('Should not deploy proposal with incorrect Uniswap pool', async function () { it('Should not deploy proposal with incorrect Uniswap pool', async function () {
let { sender, instanceFactory, tornWhale, tornToken } = await loadFixture(fixture) let { sender, proposalCreator, tornWhale, tornToken } = await loadFixture(fixture)
// deploy proposal ---------------------------------------------- // deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(instanceFactory.address, config.creationFee) await tornToken.approve(proposalCreator.address, config.creationFee)
await expect( await expect(
instanceFactory proposalCreator
.connect(sender) .connect(sender)
.createProposalApprove(config.COMP, 4000, [ethers.utils.parseEther('100')], [30]), .createProposalApprove(config.COMP, 4000, [ethers.utils.parseEther('100')], [30]),
).to.be.revertedWith('Uniswap pool is not exist') ).to.be.revertedWith('Uniswap pool is not exist')
await expect( await expect(
instanceFactory proposalCreator
.connect(sender) .connect(sender)
.createProposalApprove(config.COMP, 10000, [ethers.utils.parseEther('100')], [30]), .createProposalApprove(config.COMP, 10000, [ethers.utils.parseEther('100')], [30]),
).to.be.revertedWith('Uniswap pool TWAP slots number is low') ).to.be.revertedWith('Uniswap pool TWAP slots number is low')
}) })
it('Should not deploy proposal with incorrect protocol fee', async function () { it('Should not deploy proposal with incorrect protocol fee', async function () {
let { sender, instanceFactory, tornWhale, tornToken } = await loadFixture(fixture) let { sender, proposalCreator, tornWhale, tornToken } = await loadFixture(fixture)
// deploy proposal ---------------------------------------------- // deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(instanceFactory.address, config.creationFee) await tornToken.approve(proposalCreator.address, config.creationFee)
await expect( await expect(
instanceFactory proposalCreator
.connect(sender) .connect(sender)
.createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [10300]), .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [10300]),
).to.be.revertedWith('Protocol fee is more than 100%') ).to.be.revertedWith('Protocol fee is more than 100%')
}) })
it('Should successfully deploy/propose/execute proposal - add native instance', async function () {
let { sender, instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } =
await loadFixture(fixture)
const denomination = ethers.utils.parseEther('1.5')
// deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(() =>
proposalCreator.connect(sender).createProposalApprove(addressZero, 0, [denomination], [30]),
).to.changeTokenBalances(
tornToken,
[sender, gov],
[BigNumber.from(0).sub(config.creationFee), config.creationFee],
)
let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt(
'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
)
expect(await proposal.instanceFactory()).to.be.equal(instanceFactory.address)
expect(await proposal.instanceRegistry()).to.be.equal(instanceRegistry.address)
expect(await proposal.token()).to.be.equal(addressZero)
expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(0)
expect(await proposal.numInstances()).to.be.equal(1)
expect(await proposal.protocolFeeByIndex(0)).to.be.equal(30)
expect(await proposal.denominationByIndex(0)).to.be.equal(denomination)
// propose proposal ---------------------------------------------
let response, id, state
gov = await gov.connect(tornWhale)
await tornToken.connect(tornWhale).approve(gov.address, ethers.utils.parseEther('26000'))
await gov.lockWithApproval(ethers.utils.parseEther('26000'))
response = await gov.propose(proposal.address, 'ETH 1.5 instance proposal')
id = await gov.latestProposalIds(tornWhale.address)
state = await gov.state(id)
const { events } = await response.wait()
const args = events.find(({ event }) => event == 'ProposalCreated').args
expect(args.id).to.be.equal(id)
expect(args.proposer).to.be.equal(tornWhale.address)
expect(args.target).to.be.equal(proposal.address)
expect(args.description).to.be.equal('ETH 1.5 instance proposal')
expect(state).to.be.equal(ProposalState.Pending)
// execute proposal ---------------------------------------------
await minewait((await gov.VOTING_DELAY()).add(1).toNumber())
await expect(gov.castVote(id, true)).to.not.be.reverted
expect(await gov.state(id)).to.be.equal(ProposalState.Active)
await minewait(
(
await gov.VOTING_PERIOD()
)
.add(await gov.EXECUTION_DELAY())
.add(96400)
.toNumber(),
)
expect(await gov.state(id)).to.be.equal(ProposalState.AwaitingExecution)
let tx = await gov.execute(id)
expect(await gov.state(id)).to.be.equal(ProposalState.Executed)
// check instance initialization --------------------------------
let receipt = await tx.wait()
const instanceAddr = '0x' + receipt.events[0].topics[1].toString().slice(-40)
const instance = await ethers.getContractAt('ETHTornadoCloneable', instanceAddr)
expect(await instance.verifier()).to.be.equal(config.verifier)
expect(await instance.hasher()).to.be.equal(config.hasher)
expect(await instance.levels()).to.be.equal(config.merkleTreeHeight)
expect(await instance.denomination()).to.equal(denomination)
const instanceData = await instanceRegistry.instances(instance.address)
expect(instanceData.isERC20).to.be.equal(false)
expect(instanceData.token).to.be.equal(addressZero)
expect(instanceData.state).to.be.equal(1)
expect(instanceData.uniswapPoolSwappingFee).to.be.equal(0)
expect(instanceData.protocolFeePercentage).to.be.equal(30)
})
it('Should successfully deploy/propose/execute proposal - add native instances', async function () {
let { sender, instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } =
await loadFixture(fixture)
const denominations = [
ethers.utils.parseEther('1.5'),
ethers.utils.parseEther('10.5'),
ethers.utils.parseEther('100.5'),
ethers.utils.parseEther('1000.5'),
]
const numInstances = denominations.length
const protocolFees = [30, 30, 30, 30]
// deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(() =>
proposalCreator.connect(sender).createProposalApprove(addressZero, 0, denominations, protocolFees),
).to.changeTokenBalances(
tornToken,
[sender, gov],
[BigNumber.from(0).sub(config.creationFee), config.creationFee],
)
let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt(
'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
)
expect(await proposal.instanceFactory()).to.be.equal(instanceFactory.address)
expect(await proposal.instanceRegistry()).to.be.equal(instanceRegistry.address)
expect(await proposal.token()).to.be.equal(addressZero)
expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(0)
expect(await proposal.numInstances()).to.be.equal(numInstances)
for (let i = 0; i < numInstances; i++) {
expect(await proposal.protocolFeeByIndex(i)).to.be.equal(protocolFees[i])
expect(await proposal.denominationByIndex(i)).to.be.equal(denominations[i])
}
// propose proposal ---------------------------------------------
let response, id, state
gov = await gov.connect(tornWhale)
await tornToken.connect(tornWhale).approve(gov.address, ethers.utils.parseEther('26000'))
await gov.lockWithApproval(ethers.utils.parseEther('26000'))
response = await gov.propose(proposal.address, 'ETH instances proposal')
id = await gov.latestProposalIds(tornWhale.address)
state = await gov.state(id)
const { events } = await response.wait()
const args = events.find(({ event }) => event == 'ProposalCreated').args
expect(args.id).to.be.equal(id)
expect(args.proposer).to.be.equal(tornWhale.address)
expect(args.target).to.be.equal(proposal.address)
expect(args.description).to.be.equal('ETH instances proposal')
expect(state).to.be.equal(ProposalState.Pending)
// execute proposal ---------------------------------------------
await minewait((await gov.VOTING_DELAY()).add(1).toNumber())
await expect(gov.castVote(id, true)).to.not.be.reverted
expect(await gov.state(id)).to.be.equal(ProposalState.Active)
await minewait(
(
await gov.VOTING_PERIOD()
)
.add(await gov.EXECUTION_DELAY())
.add(96400)
.toNumber(),
)
expect(await gov.state(id)).to.be.equal(ProposalState.AwaitingExecution)
await gov.execute(id)
expect(await gov.state(id)).to.be.equal(ProposalState.Executed)
// check instances initialization -------------------------------
logs = await instanceFactory.queryFilter('NewInstanceCloneCreated')
for (let i = 0; i < numInstances; i++) {
let instanceAddr = '0x' + logs[logs.length - numInstances + i].topics[1].slice(-40)
let instance = await ethers.getContractAt('ETHTornadoCloneable', instanceAddr)
expect(await instance.verifier()).to.be.equal(config.verifier)
expect(await instance.hasher()).to.be.equal(config.hasher)
expect(await instance.levels()).to.be.equal(config.merkleTreeHeight)
expect(await instance.denomination()).to.equal(denominations[i])
let instanceData = await instanceRegistry.instances(instance.address)
expect(instanceData.isERC20).to.be.equal(false)
expect(instanceData.token).to.be.equal(addressZero)
expect(instanceData.state).to.be.equal(1)
expect(instanceData.uniswapPoolSwappingFee).to.be.equal(0)
expect(instanceData.protocolFeePercentage).to.be.equal(protocolFees[i])
}
})
it('Should successfully deploy proposal with permit for native', async function () {
let { instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(
fixture,
)
const denomination = ethers.utils.parseEther('1.5')
const privateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
const publicKey = '0x' + ethers.utils.computeAddress(Buffer.from(privateKey.slice(2), 'hex'))
const sender = await ethers.getSigner(publicKey.slice(2))
await expect(() =>
tornToken.connect(tornWhale).transfer(sender.address, config.creationFee),
).to.changeTokenBalances(
tornToken,
[tornWhale, sender],
[BigNumber.from(0).sub(config.creationFee), config.creationFee],
)
// prepare permit data
const domain = {
name: await tornToken.name(),
version: '1',
chainId: 1,
verifyingContract: tornToken.address,
}
const curTimestamp = Math.trunc(new Date().getTime() / 1000)
const args = {
owner: sender,
spender: proposalCreator.address,
value: config.creationFee,
nonce: 0,
deadline: curTimestamp + 1000,
}
const permitSigner = new PermitSigner(domain, args)
const signature = await permitSigner.getSignature(privateKey)
const signer = await permitSigner.getSignerAddress(args, signature.hex)
expect(signer).to.equal(sender.address)
await expect(() =>
proposalCreator.createProposalPermit(
addressZero,
0,
[denomination],
[30],
sender.address,
args.deadline.toString(),
signature.v,
signature.r,
signature.s,
),
).to.changeTokenBalances(
tornToken,
[sender, gov],
[BigNumber.from(0).sub(config.creationFee), config.creationFee],
)
let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt(
'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
)
expect(await proposal.instanceFactory()).to.be.equal(instanceFactory.address)
expect(await proposal.instanceRegistry()).to.be.equal(instanceRegistry.address)
expect(await proposal.token()).to.be.equal(addressZero)
expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(0)
expect(await proposal.numInstances()).to.be.equal(1)
expect(await proposal.protocolFeeByIndex(0)).to.be.equal(30)
expect(await proposal.denominationByIndex(0)).to.be.equal(denomination)
})
it('Should deposit and withdraw into the new native instance', async function () {
let { sender, proposalCreator, gov, tornWhale, tornToken, router } = await loadFixture(fixture)
const denomination = ethers.utils.parseEther('1.5')
// deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(() =>
proposalCreator.connect(sender).createProposalApprove(addressZero, 0, [denomination], [30]),
).to.changeTokenBalances(
tornToken,
[sender, gov],
[BigNumber.from(0).sub(config.creationFee), config.creationFee],
)
let logs = await proposalCreator.queryFilter('NewGovernanceProposalCreated')
const proposal = await ethers.getContractAt(
'AddInstanceProposal',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
)
// propose proposal ---------------------------------------------
let id
gov = await gov.connect(tornWhale)
await tornToken.connect(tornWhale).approve(gov.address, ethers.utils.parseEther('26000'))
await gov.lockWithApproval(ethers.utils.parseEther('26000'))
await gov.propose(proposal.address, 'ETH instance proposal')
id = await gov.latestProposalIds(tornWhale.address)
// execute proposal ---------------------------------------------
await minewait((await gov.VOTING_DELAY()).add(1).toNumber())
await expect(gov.castVote(id, true)).to.not.be.reverted
await minewait(
(
await gov.VOTING_PERIOD()
)
.add(await gov.EXECUTION_DELAY())
.add(96400)
.toNumber(),
)
let tx = await gov.execute(id)
let receipt = await tx.wait()
const instanceAddr = '0x' + receipt.events[0].topics[1].toString().slice(-40)
const instance = await ethers.getContractAt('ETHTornadoCloneable', instanceAddr)
// check instance work ------------------------------------------
const depo = createDeposit({
nullifier: rbigint(31),
secret: rbigint(31),
})
await expect(() =>
router.deposit(instance.address, toHex(depo.commitment), [], { value: denomination }),
).to.changeEtherBalances([sender, instance], [BigNumber.from(0).sub(denomination), denomination])
let pevents = await instance.queryFilter('Deposit')
await initialize({ merkleTreeHeight: 20 })
const { proof, args } = await generateProof({
deposit: depo,
recipient: sender.address,
events: pevents,
})
await expect(() => router.withdraw(instance.address, proof, ...args)).to.changeEtherBalances(
[instance, sender],
[BigNumber.from(0).sub(denomination), denomination],
)
})
it('Should not deploy native currency proposal with incorrect protocol fee', async function () {
let { sender, proposalCreator, tornWhale, tornToken } = await loadFixture(fixture)
// deploy proposal ----------------------------------------------
await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee)
await tornToken.approve(proposalCreator.address, config.creationFee)
await expect(
proposalCreator
.connect(sender)
.createProposalApprove(addressZero, 0, [ethers.utils.parseEther('1.5')], [10300]),
).to.be.revertedWith('Protocol fee is more than 100%')
})
}) })

View File

@ -8,7 +8,7 @@ const { getSignerFromAddress } = require('./utils')
const { generate } = require('../src/generateAddresses') const { generate } = require('../src/generateAddresses')
const { rbigint, createDeposit, toHex, generateProof, initialize } = require('tornado-cli') const { rbigint, createDeposit, toHex, generateProof, initialize } = require('tornado-cli')
describe('Multiple Instance Factory Tests', () => { describe('Sidechain Instance Factory Tests', () => {
const addressZero = ethers.constants.AddressZero const addressZero = ethers.constants.AddressZero
async function fixture() { async function fixture() {
@ -34,19 +34,19 @@ describe('Multiple Instance Factory Tests', () => {
config.singletonFactoryVerboseWrapper, config.singletonFactoryVerboseWrapper,
) )
const contracts = await generate() const contracts = await generate()
if ((await ethers.provider.getCode(contracts.factoryContract.implementation.address)) == '0x') { if ((await ethers.provider.getCode(contracts.sidechainFactory.implementation.address)) == '0x') {
await singletonFactory.deploy(contracts.factoryContract.implementation.bytecode, config.salt, { await singletonFactory.deploy(contracts.sidechainFactory.implementation.bytecode, config.salt, {
gasLimit: config.deployGasLimit, gasLimit: config.deployGasLimit,
}) })
} }
if ((await ethers.provider.getCode(contracts.factoryContract.proxy.address)) == '0x') { if ((await ethers.provider.getCode(contracts.sidechainFactory.proxy.address)) == '0x') {
await singletonFactory.deploy(contracts.factoryContract.proxy.bytecode, config.salt, { await singletonFactory.deploy(contracts.sidechainFactory.proxy.bytecode, config.salt, {
gasLimit: config.deployGasLimit, gasLimit: config.deployGasLimit,
}) })
} }
const instanceFactory = await ethers.getContractAt( const instanceFactory = await ethers.getContractAt(
'MultipleInstanceFactory', 'SidechainInstanceFactory',
contracts.factoryContract.proxy.address, contracts.sidechainFactory.proxy.address,
) )
return { return {
@ -72,10 +72,11 @@ describe('Multiple Instance Factory Tests', () => {
expect(await instanceFactory.verifier()).to.be.equal(config.verifier) expect(await instanceFactory.verifier()).to.be.equal(config.verifier)
expect(await instanceFactory.hasher()).to.be.equal(config.hasher) expect(await instanceFactory.hasher()).to.be.equal(config.hasher)
expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight)
expect(await instanceFactory.implementation()).to.exist expect(await instanceFactory.ERC20Impl()).to.exist
expect(await instanceFactory.nativeCurImpl()).to.exist
}) })
it('Governance should be able to set factory params', async function () { it('Admin should be able to set factory params', async function () {
let { instanceFactory, owner } = await loadFixture(fixture) let { instanceFactory, owner } = await loadFixture(fixture)
await expect(instanceFactory.setMerkleTreeHeight(1)).to.be.reverted await expect(instanceFactory.setMerkleTreeHeight(1)).to.be.reverted
@ -196,4 +197,100 @@ describe('Multiple Instance Factory Tests', () => {
[BigNumber.from(0).sub(value), value], [BigNumber.from(0).sub(value), value],
) )
}) })
it('Should successfully add native currency instance', async function () {
let { sender, instanceFactory } = await loadFixture(fixture)
const denomination = ethers.utils.parseEther('1')
// deploy instance
await instanceFactory.connect(sender).createInstanceClone(denomination, addressZero)
// check instance initialization
let logs = await instanceFactory.queryFilter('NewInstanceCloneCreated')
const instance = await ethers.getContractAt(
'ETHTornadoCloneable',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
)
expect(await instance.verifier()).to.be.equal(config.verifier)
expect(await instance.hasher()).to.be.equal(config.hasher)
expect(await instance.levels()).to.be.equal(config.merkleTreeHeight)
expect(await instance.denomination()).to.equal(denomination)
// try to deploy the same instance again
await instanceFactory.connect(sender).createInstanceClone(denomination, addressZero)
// check that instance has not been created - no new NewInstanceCloneCreated event
let curLogs = await instanceFactory.queryFilter('NewInstanceCloneCreated')
expect(curLogs.length).to.be.equal(logs.length)
})
it('Should successfully add native currency instances', async function () {
let { sender, instanceFactory } = await loadFixture(fixture)
const denominations = [
ethers.utils.parseEther('1'),
ethers.utils.parseEther('10'),
ethers.utils.parseEther('100'),
ethers.utils.parseEther('1000'),
]
const numInstances = denominations.length
// deploy instances
await instanceFactory.connect(sender).createInstanceClones(addressZero, denominations)
// check instance initialization
let logs = await instanceFactory.queryFilter('NewInstanceCloneCreated')
for (let i = 0; i < numInstances; i++) {
let instanceAddr = '0x' + logs[logs.length - numInstances + i].topics[1].slice(-40)
let instance = await ethers.getContractAt('ETHTornadoCloneable', instanceAddr)
expect(await instance.verifier()).to.be.equal(config.verifier)
expect(await instance.hasher()).to.be.equal(config.hasher)
expect(await instance.levels()).to.be.equal(config.merkleTreeHeight)
expect(await instance.denomination()).to.equal(denominations[i])
}
})
it('Should deposit and withdraw into the new native currency instance', async function () {
let { sender, instanceFactory } = await loadFixture(fixture)
const denomination = ethers.utils.parseEther('1.5')
// deploy instance
await instanceFactory.connect(sender).createInstanceClone(denomination, addressZero)
let logs = await instanceFactory.queryFilter('NewInstanceCloneCreated')
const instance = await ethers.getContractAt(
'ETHTornadoCloneable',
ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)),
)
// check instance work ------------------------------------------
const depo = createDeposit({
nullifier: rbigint(31),
secret: rbigint(31),
})
await expect(() =>
instance.connect(sender).deposit(toHex(depo.commitment), {
value: denomination,
}),
).to.changeEtherBalances([sender, instance], [BigNumber.from(0).sub(denomination), denomination])
let pevents = await instance.queryFilter('Deposit')
await initialize({ merkleTreeHeight: 20 })
const { proof, args } = await generateProof({
deposit: depo,
recipient: sender.address,
events: pevents,
})
await expect(() => instance.withdraw(proof, ...args)).to.changeEtherBalances(
[instance, sender],
[BigNumber.from(0).sub(denomination), denomination],
)
})
}) })

View File

@ -617,6 +617,11 @@
"@babel/helper-validator-identifier" "^7.14.9" "@babel/helper-validator-identifier" "^7.14.9"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@consento/sync-randombytes@^1.0.4", "@consento/sync-randombytes@^1.0.5": "@consento/sync-randombytes@^1.0.4", "@consento/sync-randombytes@^1.0.5":
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/@consento/sync-randombytes/-/sync-randombytes-1.0.5.tgz#5be6bc58c6a6fa6e09f04cc684d037e29e6c28d5" resolved "https://registry.yarnpkg.com/@consento/sync-randombytes/-/sync-randombytes-1.0.5.tgz#5be6bc58c6a6fa6e09f04cc684d037e29e6c28d5"
@ -6311,6 +6316,15 @@ cli-spinners@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
cli-table3@^0.6.0:
version "0.6.2"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==
dependencies:
string-width "^4.2.0"
optionalDependencies:
"@colors/colors" "1.5.0"
cli-width@^2.0.0: cli-width@^2.0.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
@ -10216,6 +10230,14 @@ har-validator@~5.1.3:
ajv "^6.12.3" ajv "^6.12.3"
har-schema "^2.0.0" har-schema "^2.0.0"
hardhat-contract-sizer@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.6.1.tgz#2b0046a55fa1ec96f19fdab7fde372377401c874"
integrity sha512-b8wS7DBvyo22kmVwpzstAQTdDCThpl/ySBqZh5ga9Yxjf61/uTL12TEg5nl7lDeWy73ntEUzxMwY6XxbQEc2wA==
dependencies:
chalk "^4.0.0"
cli-table3 "^0.6.0"
hardhat-log-remover@^2.0.2: hardhat-log-remover@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/hardhat-log-remover/-/hardhat-log-remover-2.0.2.tgz#6014fe2c515ced1e0eaa7a4d854e37695aaac61a" resolved "https://registry.yarnpkg.com/hardhat-log-remover/-/hardhat-log-remover-2.0.2.tgz#6014fe2c515ced1e0eaa7a4d854e37695aaac61a"