diff --git a/README.md b/README.md index 80f5d1f..28c8013 100644 --- a/README.md +++ b/README.md @@ -4,28 +4,28 @@ This repository contains: -1. `InstanceFactory` - instance factory for the creation new Tornado ERC20 pools -2. `InstanceFactoryWithRegistry` - governance proposal factory for the addition of new Tornado ERC20 instances to the Tornado router +1. `InstanceFactory` - instance factory for the creation new Tornado ERC20/native pools +2. `InstanceProposalCreator` - governance proposal factory for the addition of new Tornado instances to the Tornado router ### 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) -### 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 -2. `uint24 uniswapPoolSwappingFee` - fee value of Uniswap instance which will be used for `TORN/token` price determination. `3000` means 0.3% fee Uniswap pool. +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. 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). 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 -### 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. 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 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 @@ -61,10 +61,12 @@ Check config.js for actual values. With `salt` = `0x0000000000000000000000000000000000000000000000000000000047941987` address must be: -1. `MultipleInstanceFactory` - `0x8a1BCFc608DdF6c4715277a24310B9E8ecc4c110` -2. `MultipleInstanceFactory proxy` - `0x0F9AE1d1ABbDd441B813ada0B038df130b2a6A86` -3. `InstanceFactoryWithRegistry` - `0x557fB18B66088728Ea975136de46714983aa4f2E` -4. `InstanceFactoryWithRegistry proxy` - `0x4D1b1c294dA4D14aC0e0Eed7BcD4Db3fe2bDe4C3` +1. `SidechainInstanceFactory` - `0x0a0601c2E952aC53bb95b1A81764Fc730C9bBDd5` +2. `SidechainInstanceFactory proxy` - `0xb4838f15185E4A7E2ED8534c89b7AC92aC927C9b` +3. `InstanceFactory` - `0xa7610A8292850f516603bA0066A731109Dfe854E` +4. `InstanceFactory proxy` - `0x0f9B6646815f118d671084cd73Ea8713b18be354` +5. `InstanceProposalCreator` - `0x39F00b914f8DBD28082D1f1ae9b3210d7F7B404f` +6. `InstanceProposalCreator proxy` - `0xA2980DAfcdAf61C6d39FE63a6360907b69e840c8` Check addresses with current config: @@ -73,19 +75,19 @@ Check addresses with current config: node -e 'require("./src/generateAddresses").generateWithLog()' ``` -Deploy MultipleInstanceFactory: +Deploy SidechainInstanceFactory: ```shell - yarn hardhat run scripts/deployMultipleInstanceFactory.js --network mainnet + yarn hardhat run scripts/deploySidechainInstanceFactory.js --network mainnet ``` -Deploy InstanceFactoryWithRegistry: +Deploy InstanceProposalCreator: ```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 diff --git a/contracts/AddInstanceProposal.sol b/contracts/AddInstanceProposal.sol index 96cf6de..04f5d24 100644 --- a/contracts/AddInstanceProposal.sol +++ b/contracts/AddInstanceProposal.sol @@ -4,10 +4,10 @@ pragma solidity 0.7.6; pragma abicoder v2; import "./interfaces/IInstanceRegistry.sol"; -import "./InstanceFactory.sol"; +import "./interfaces/IInstanceFactory.sol"; contract AddInstanceProposal { - InstanceFactory public immutable instanceFactory; + IInstanceFactory public immutable instanceFactory; IInstanceRegistry public immutable instanceRegistry; address public immutable token; uint24 public immutable uniswapPoolSwappingFee; @@ -32,7 +32,7 @@ contract AddInstanceProposal { uint256[] memory _denominations, uint32[] memory _protocolFees ) { - instanceFactory = InstanceFactory(_instanceFactory); + instanceFactory = IInstanceFactory(_instanceFactory); instanceRegistry = IInstanceRegistry(_instanceRegistry); token = _token; uniswapPoolSwappingFee = _uniswapPoolSwappingFee; @@ -57,7 +57,7 @@ contract AddInstanceProposal { address instance = instanceFactory.createInstanceClone(denominationByIndex(i), token); IInstanceRegistry.Instance memory newInstanceData = IInstanceRegistry.Instance( - true, + token != address(0), IERC20(token), IInstanceRegistry.InstanceState.ENABLED, uniswapPoolSwappingFee, diff --git a/contracts/ETHTornadoCloneable.sol b/contracts/ETHTornadoCloneable.sol new file mode 100644 index 0000000..2269e07 --- /dev/null +++ b/contracts/ETHTornadoCloneable.sol @@ -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); + } +} diff --git a/contracts/InstanceFactory.sol b/contracts/InstanceFactory.sol index e2c1c60..253baa1 100644 --- a/contracts/InstanceFactory.sol +++ b/contracts/InstanceFactory.sol @@ -7,19 +7,21 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; import "./ERC20TornadoCloneable.sol"; +import "./ETHTornadoCloneable.sol"; contract InstanceFactory is Initializable { using Clones for address; using Address for address; address public admin; - address public implementation; + address public ERC20Impl; + address public nativeCurImpl; address public verifier; address public hasher; uint32 public merkleTreeHeight; 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); modifier onlyAdmin() { @@ -43,30 +45,49 @@ contract InstanceFactory is Initializable { merkleTreeHeight = _merkleTreeHeight; admin = _admin; - ERC20TornadoCloneable implContract = new ERC20TornadoCloneable(_verifier, _hasher); - implementation = address(implContract); + ERC20TornadoCloneable ERC20ImplContract = new ERC20TornadoCloneable(_verifier, _hasher); + ERC20Impl = address(ERC20ImplContract); + ETHTornadoCloneable nativeCurImplContract = new ETHTornadoCloneable(_verifier, _hasher); + nativeCurImpl = address(nativeCurImplContract); } /** * @dev Creates 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)); - address newClone = implementation.predictDeterministicAddress(salt); - if (!newClone.isContract()) { - implementation.cloneDeterministic(salt); - emit NewInstanceCloneCreated(newClone); - ERC20TornadoCloneable(newClone).init(_denomination, merkleTreeHeight, _token); + if (_token == address(0)) { + clone = nativeCurImpl.predictDeterministicAddress(salt); + if (!clone.isContract()) { + nativeCurImpl.cloneDeterministic(salt); + emit NewInstanceCloneCreated(clone); + ETHTornadoCloneable(clone).init(_denomination, merkleTreeHeight); + } + } else { + clone = ERC20Impl.predictDeterministicAddress(salt); + if (!clone.isContract()) { + ERC20Impl.cloneDeterministic(salt); + emit NewInstanceCloneCreated(clone); + ERC20TornadoCloneable(clone).init(_denomination, merkleTreeHeight, _token); + } } - return newClone; + return clone; } function getInstanceAddress(uint256 _denomination, address _token) public view returns (address) { 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 { @@ -77,7 +98,8 @@ contract InstanceFactory is Initializable { function generateNewImplementation(address _verifier, address _hasher) external onlyAdmin { verifier = _verifier; hasher = _hasher; - implementation = address(new ERC20TornadoCloneable(_verifier, _hasher)); - emit NewImplementationSet(implementation, _verifier, _hasher); + ERC20Impl = address(new ERC20TornadoCloneable(_verifier, _hasher)); + nativeCurImpl = address(new ETHTornadoCloneable(_verifier, _hasher)); + emit NewImplementationSet(ERC20Impl, nativeCurImpl, _verifier, _hasher); } } diff --git a/contracts/InstanceFactoryWithRegistry.sol b/contracts/InstanceProposalCreator.sol similarity index 79% rename from contracts/InstanceFactoryWithRegistry.sol rename to contracts/InstanceProposalCreator.sol index 42a4271..9ff39d3 100644 --- a/contracts/InstanceFactoryWithRegistry.sol +++ b/contracts/InstanceProposalCreator.sol @@ -5,17 +5,19 @@ pragma abicoder v2; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import "./AddInstanceProposal.sol"; -import "./InstanceFactory.sol"; +import "./interfaces/IInstanceFactory.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol"; import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.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; address public immutable governance; address public immutable torn; + IInstanceFactory public immutable instanceFactory; address public immutable instanceRegistry; IUniswapV3Factory public immutable UniswapV3Factory; address public immutable WETH; @@ -36,12 +38,14 @@ contract InstanceFactoryWithRegistry is InstanceFactory { constructor( address _governance, + address _instanceFactory, address _instanceRegistry, address _torn, address _UniswapV3Factory, address _WETH ) { governance = _governance; + instanceFactory = IInstanceFactory(_instanceFactory); instanceRegistry = _instanceRegistry; torn = _torn; 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, * params left out because self explainable * */ - function initialize( - address _verifier, - address _hasher, - uint32 _merkleTreeHeight, - address _governance, - uint16 _TWAPSlotsMin, - uint256 _creationFee - ) external initializer { - initialize(_verifier, _hasher, _merkleTreeHeight, _governance); + function initialize(uint16 _TWAPSlotsMin, uint256 _creationFee) external initializer { TWAPSlotsMin = _TWAPSlotsMin; 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. * @param _token address of ERC20 token for a new instance @@ -125,14 +112,14 @@ contract InstanceFactoryWithRegistry is InstanceFactory { uint256[] memory _denominations, uint32[] memory _protocolFees ) 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 == _protocolFees.length, "Incorrect denominations/fees length"); // check Uniswap Pool 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 address poolAddr = UniswapV3Factory.getPool(_token, WETH, _uniswapPoolSwappingFee); require(poolAddr != address(0), "Uniswap pool is not exist"); @@ -144,19 +131,26 @@ contract InstanceFactoryWithRegistry is InstanceFactory { } address proposal = address( - new AddInstanceProposal(address(this), instanceRegistry, _token, _uniswapPoolSwappingFee, _denominations, _protocolFees) + new AddInstanceProposal( + address(instanceFactory), + instanceRegistry, + _token, + _uniswapPoolSwappingFee, + _denominations, + _protocolFees + ) ); emit NewGovernanceProposalCreated(proposal); return proposal; } - function setCreationFee(uint256 _creationFee) external onlyAdmin { + function setCreationFee(uint256 _creationFee) external onlyGovernance { creationFee = _creationFee; emit NewCreationFeeSet(_creationFee); } - function setTWAPSlotsMin(uint16 _TWAPSlotsMin) external onlyAdmin { + function setTWAPSlotsMin(uint16 _TWAPSlotsMin) external onlyGovernance { TWAPSlotsMin = _TWAPSlotsMin; emit NewTWAPSlotsMinSet(_TWAPSlotsMin); } diff --git a/contracts/MultipleInstanceFactory.sol b/contracts/MultipleInstanceFactory.sol deleted file mode 100644 index 965ffb5..0000000 --- a/contracts/MultipleInstanceFactory.sol +++ /dev/null @@ -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; - } -} diff --git a/contracts/SidechainInstanceFactory.sol b/contracts/SidechainInstanceFactory.sol new file mode 100644 index 0000000..922e96d --- /dev/null +++ b/contracts/SidechainInstanceFactory.sol @@ -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; + } +} diff --git a/contracts/interfaces/IInstanceFactory.sol b/contracts/interfaces/IInstanceFactory.sol new file mode 100644 index 0000000..ab4e2dc --- /dev/null +++ b/contracts/interfaces/IInstanceFactory.sol @@ -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); +} diff --git a/hardhat.config.js b/hardhat.config.js index 233dad7..cda536f 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -3,6 +3,7 @@ require('@nomiclabs/hardhat-waffle') require('@nomiclabs/hardhat-etherscan') require('hardhat-log-remover') require('solidity-coverage') +require('hardhat-contract-sizer') /** * @type import('hardhat/config').HardhatUserConfig diff --git a/package.json b/package.json index 3b3705f..7706371 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint-plugin-prettier": "^3.4.0", "ethereum-waffle": "^3.4.0", "hardhat": "^2.4.3", + "hardhat-contract-sizer": "^2.6.1", "hardhat-log-remover": "^2.0.2", "mocha-lcov-reporter": "^1.3.0", "prettier": "^2.3.2", diff --git a/scripts/deployInstanceFactoryWithRegistry.js b/scripts/deployInstanceProposalCreator.js similarity index 50% rename from scripts/deployInstanceFactoryWithRegistry.js rename to scripts/deployInstanceProposalCreator.js index bd30e8a..9bcf547 100644 --- a/scripts/deployInstanceFactoryWithRegistry.js +++ b/scripts/deployInstanceProposalCreator.js @@ -14,14 +14,14 @@ async function deploy({ address, bytecode, singletonFactory }) { async function main() { const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) const contracts = await generate() - await deploy({ ...contracts.factoryWithRegistryContract.implementation, singletonFactory }) - await deploy({ ...contracts.factoryWithRegistryContract.proxy, singletonFactory }) - console.log( - `Instance factory with registry contract have been deployed on ${contracts.factoryWithRegistryContract.implementation.address} address`, - ) - console.log( - `Instance factory with registry proxy contract have been deployed on ${contracts.factoryWithRegistryContract.proxy.address} address`, - ) + await deploy({ ...contracts.factory.implementation, singletonFactory }) + console.log(`Instance factory contract have been deployed on ${contracts.factory.implementation.address}`) + await deploy({ ...contracts.factory.proxy, singletonFactory }) + console.log(`Instance factory proxy contract have been deployed on ${contracts.factory.proxy.address}`) + await deploy({ ...contracts.proposalCreator.implementation, singletonFactory }) + console.log(`Proposal creator have been deployed on ${contracts.proposalCreator.implementation.address}`) + await deploy({ ...contracts.proposalCreator.proxy, singletonFactory }) + console.log(`Proposal creator proxy have been deployed on ${contracts.proposalCreator.proxy.address}`) } main() diff --git a/scripts/deployMultipleInstanceFactory.js b/scripts/deploySidechainInstanceFactory.js similarity index 65% rename from scripts/deployMultipleInstanceFactory.js rename to scripts/deploySidechainInstanceFactory.js index fb293ac..557c1c4 100644 --- a/scripts/deployMultipleInstanceFactory.js +++ b/scripts/deploySidechainInstanceFactory.js @@ -14,13 +14,13 @@ async function deploy({ address, bytecode, singletonFactory }) { async function main() { const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) const contracts = await generate() - await deploy({ ...contracts.factoryContract.implementation, singletonFactory }) - await deploy({ ...contracts.factoryContract.proxy, singletonFactory }) + await deploy({ ...contracts.sidechainFactory.implementation, singletonFactory }) + await deploy({ ...contracts.sidechainFactory.proxy, singletonFactory }) 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( - `MultipleInstanceFactory proxy contract have been deployed on ${contracts.factoryContract.proxy.address} address`, + `SidechainInstanceFactory proxy contract have been deployed on ${contracts.sidechainFactory.proxy.address} address`, ) } diff --git a/src/generateAddresses.js b/src/generateAddresses.js index 37600d4..ce5872a 100644 --- a/src/generateAddresses.js +++ b/src/generateAddresses.js @@ -36,52 +36,62 @@ async function upgradableContract({ contractName, implConstructorArgs, proxyCons } async function generate(config = defaultConfig) { - // factory contract ----------------------------------------------- - const FactoryFactory = await ethers.getContractFactory('MultipleInstanceFactory') - const FactoryInitData = FactoryFactory.interface.encodeFunctionData('initialize', [ + // sidechain factory contract ------------------------------------- + const SidechainFactory = await ethers.getContractFactory('SidechainInstanceFactory') + const SidechainFactoryInitData = SidechainFactory.interface.encodeFunctionData('initialize', [ config.verifier, config.hasher, config.merkleTreeHeight, config.admin, ]) - const factoryContract = await upgradableContract({ - contractName: 'MultipleInstanceFactory', + const sidechainFactory = await upgradableContract({ + contractName: 'SidechainInstanceFactory', implConstructorArgs: [], - proxyConstructorArgs: [config.admin, FactoryInitData], + proxyConstructorArgs: [config.admin, SidechainFactoryInitData], salt: config.salt, }) // factory with registry contract --------------------------------- - const FactoryWithRegistryFactory = await ethers.getContractFactory('InstanceFactoryWithRegistry') - const FactoryWithRegistryInitData = FactoryWithRegistryFactory.interface.encodeFunctionData( - 'initialize(address,address,uint32,address,uint16,uint256)', - [ - config.verifier, - config.hasher, - config.merkleTreeHeight, - config.governance, - config.TWAPSlotsMin, - config.creationFee, - ], - ) + const Factory = await ethers.getContractFactory('InstanceFactory') + const FactoryInitData = Factory.interface.encodeFunctionData('initialize', [ + config.verifier, + config.hasher, + config.merkleTreeHeight, + config.governance, + ]) - const factoryWithRegistryContract = await upgradableContract({ - contractName: 'InstanceFactoryWithRegistry', + 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.creationFee, + ]) + + const proposalCreator = await upgradableContract({ + contractName: 'InstanceProposalCreator', implConstructorArgs: [ config.governance, + factory.proxy.address, config.instanceRegistry, config.TORN, config.UniswapV3Factory, config.WETH, ], - proxyConstructorArgs: [config.governance, FactoryWithRegistryInitData], + proxyConstructorArgs: [config.governance, ProposalCreatorInitData], salt: config.salt, }) const result = { - factoryContract, - factoryWithRegistryContract, + sidechainFactory, + factory, + proposalCreator, } return result @@ -89,16 +99,12 @@ async function generate(config = defaultConfig) { async function generateWithLog() { const contracts = await generate() - console.log('MultipleInstanceFactory contract: ', contracts.factoryContract.implementation.address) - console.log('MultipleInstanceFactory proxy contract: ', contracts.factoryContract.proxy.address) - console.log( - 'Instance factory with registry contract: ', - contracts.factoryWithRegistryContract.implementation.address, - ) - console.log( - 'Instance factory with registry proxy contract: ', - contracts.factoryWithRegistryContract.proxy.address, - ) + console.log('SidechainInstanceFactory contract: ', contracts.sidechainFactory.implementation.address) + console.log('SidechainInstanceFactory proxy contract: ', contracts.sidechainFactory.proxy.address) + console.log('Instance factory contract: ', contracts.factory.implementation.address) + console.log('Instance factory proxy contract: ', contracts.factory.proxy.address) + console.log('Proposal creator contract: ', contracts.proposalCreator.implementation.address) + console.log('Proposal creator proxy contract: ', contracts.proposalCreator.proxy.address) return contracts } diff --git a/test/factory.with.registry.test.js b/test/factory.with.registry.test.js index fe7818f..ac8bbcc 100644 --- a/test/factory.with.registry.test.js +++ b/test/factory.with.registry.test.js @@ -51,31 +51,37 @@ describe('Instance Factory With Registry Tests', () => { config.instanceRegistry, ) - // deploy InstanceFactoryWithRegistry with CREATE2 + // deploy InstanceProposalCreator with CREATE2 const singletonFactory = await ethers.getContractAt( 'SingletonFactory', config.singletonFactoryVerboseWrapper, ) const contracts = await generate() - if ( - (await ethers.provider.getCode(contracts.factoryWithRegistryContract.implementation.address)) == '0x' - ) { - 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, { + if ((await ethers.provider.getCode(contracts.factory.implementation.address)) == '0x') { + await singletonFactory.deploy(contracts.factory.implementation.bytecode, config.salt, { gasLimit: config.deployGasLimit, }) } - const instanceFactory = await ethers.getContractAt( - 'InstanceFactoryWithRegistry', - contracts.factoryWithRegistryContract.proxy.address, + if ((await ethers.provider.getCode(contracts.factory.proxy.address)) == '0x') { + await singletonFactory.deploy(contracts.factory.proxy.bytecode, config.salt, { + 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 { @@ -90,73 +96,80 @@ describe('Instance Factory With Registry Tests', () => { compToken, instanceRegistry, instanceFactory, + proposalCreator, } } 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(gov.address).to.exist expect(tornToken.address).to.exist expect(instanceRegistry.address).to.exist expect(instanceFactory.address).to.exist + expect(proposalCreator.address).to.exist }) 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.hasher()).to.be.equal(config.hasher) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight) - expect(await instanceFactory.implementation()).to.exist - expect(await instanceFactory.creationFee()).to.be.equal(config.creationFee) - expect(await instanceFactory.torn()).to.be.equal(config.TORN) - expect(await instanceFactory.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin) - expect(await instanceFactory.WETH()).to.be.equal(config.WETH) - expect(await instanceFactory.UniswapV3Factory()).to.be.equal(config.UniswapV3Factory) + expect(await instanceFactory.ERC20Impl()).to.exist + expect(await instanceFactory.nativeCurImpl()).to.exist + expect(await proposalCreator.creationFee()).to.be.equal(config.creationFee) + expect(await proposalCreator.torn()).to.be.equal(config.TORN) + expect(await proposalCreator.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin) + 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 () { - let { instanceFactory, gov } = await loadFixture(fixture) + it('Governance should be able to set factory/proposalCreator params', async function () { + let { instanceFactory, proposalCreator, gov } = await loadFixture(fixture) await expect(instanceFactory.setMerkleTreeHeight(1)).to.be.reverted const govSigner = await getSignerFromAddress(gov.address) instanceFactory = await instanceFactory.connect(govSigner) + proposalCreator = await proposalCreator.connect(govSigner) await instanceFactory.generateNewImplementation(addressZero, addressZero) await instanceFactory.setMerkleTreeHeight(1) - await instanceFactory.setCreationFee(0) - await instanceFactory.setTWAPSlotsMin(0) + await proposalCreator.setCreationFee(0) + await proposalCreator.setTWAPSlotsMin(0) expect(await instanceFactory.verifier()).to.be.equal(addressZero) expect(await instanceFactory.hasher()).to.be.equal(addressZero) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(1) - expect(await instanceFactory.creationFee()).to.be.equal(0) - expect(await instanceFactory.TWAPSlotsMin()).to.be.equal(0) + expect(await proposalCreator.creationFee()).to.be.equal(0) + expect(await proposalCreator.TWAPSlotsMin()).to.be.equal(0) await instanceFactory.generateNewImplementation(config.verifier, config.hasher) await instanceFactory.setMerkleTreeHeight(config.merkleTreeHeight) - await instanceFactory.setCreationFee(config.creationFee) - await instanceFactory.setTWAPSlotsMin(config.TWAPSlotsMin) + await proposalCreator.setCreationFee(config.creationFee) + await proposalCreator.setTWAPSlotsMin(config.TWAPSlotsMin) expect(await instanceFactory.verifier()).to.be.equal(config.verifier) expect(await instanceFactory.hasher()).to.be.equal(config.hasher) expect(await instanceFactory.merkleTreeHeight()).to.be.equal(config.merkleTreeHeight) - expect(await instanceFactory.creationFee()).to.be.equal(config.creationFee) - expect(await instanceFactory.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin) + expect(await proposalCreator.creationFee()).to.be.equal(config.creationFee) + expect(await proposalCreator.TWAPSlotsMin()).to.be.equal(config.TWAPSlotsMin) }) 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 ---------------------------------------------- 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(() => - instanceFactory + proposalCreator .connect(sender) .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), ).to.changeTokenBalances( @@ -165,7 +178,7 @@ describe('Instance Factory With Registry Tests', () => { [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( 'AddInstanceProposal', 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 () { - let { sender, instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) + let { sender, instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } = + await loadFixture(fixture) const denominations = [ ethers.utils.parseEther('1'), @@ -249,17 +263,17 @@ describe('Instance Factory With Registry Tests', () => { // deploy proposal ---------------------------------------------- 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(() => - instanceFactory.connect(sender).createProposalApprove(config.COMP, 3000, denominations, protocolFees), + proposalCreator.connect(sender).createProposalApprove(config.COMP, 3000, denominations, protocolFees), ).to.changeTokenBalances( tornToken, [sender, gov], [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( 'AddInstanceProposal', 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 () { - let { instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) + let { instanceFactory, proposalCreator, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture( + fixture, + ) const privateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3' 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 args = { owner: sender, - spender: instanceFactory.address, + spender: proposalCreator.address, value: config.creationFee, nonce: 0, deadline: curTimestamp + 1000, @@ -370,7 +386,7 @@ describe('Instance Factory With Registry Tests', () => { expect(signer).to.equal(sender.address) await expect(() => - instanceFactory.createProposalPermit( + proposalCreator.createProposalPermit( config.COMP, 3000, [ethers.utils.parseEther('100')], @@ -387,7 +403,7 @@ describe('Instance Factory With Registry Tests', () => { [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( 'AddInstanceProposal', 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 () { - let { sender, instanceFactory, gov, tornWhale, tornToken, router, compToken, compWhale } = + let { sender, proposalCreator, gov, tornWhale, tornToken, router, compToken, compWhale } = await loadFixture(fixture) // deploy proposal ---------------------------------------------- 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(() => - instanceFactory + proposalCreator .connect(sender) .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), ).to.changeTokenBalances( @@ -420,7 +436,7 @@ describe('Instance Factory With Registry Tests', () => { [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( 'AddInstanceProposal', 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 () { - let { sender, instanceFactory, tornWhale, tornToken } = await loadFixture(fixture) + let { sender, proposalCreator, tornWhale, tornToken } = await loadFixture(fixture) // deploy proposal ---------------------------------------------- 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( - instanceFactory + proposalCreator .connect(sender) .createProposalApprove(config.COMP, 4000, [ethers.utils.parseEther('100')], [30]), ).to.be.revertedWith('Uniswap pool is not exist') await expect( - instanceFactory + proposalCreator .connect(sender) .createProposalApprove(config.COMP, 10000, [ethers.utils.parseEther('100')], [30]), ).to.be.revertedWith('Uniswap pool TWAP slots number is low') }) 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 ---------------------------------------------- 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( - instanceFactory + proposalCreator .connect(sender) .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [10300]), ).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%') + }) }) diff --git a/test/multiple.instance.factory.test.js b/test/sidechain.instance.factory.test.js similarity index 61% rename from test/multiple.instance.factory.test.js rename to test/sidechain.instance.factory.test.js index 8acd878..1a7f37a 100644 --- a/test/multiple.instance.factory.test.js +++ b/test/sidechain.instance.factory.test.js @@ -8,7 +8,7 @@ const { getSignerFromAddress } = require('./utils') const { generate } = require('../src/generateAddresses') const { rbigint, createDeposit, toHex, generateProof, initialize } = require('tornado-cli') -describe('Multiple Instance Factory Tests', () => { +describe('Sidechain Instance Factory Tests', () => { const addressZero = ethers.constants.AddressZero async function fixture() { @@ -34,19 +34,19 @@ describe('Multiple Instance Factory Tests', () => { config.singletonFactoryVerboseWrapper, ) const contracts = await generate() - if ((await ethers.provider.getCode(contracts.factoryContract.implementation.address)) == '0x') { - await singletonFactory.deploy(contracts.factoryContract.implementation.bytecode, config.salt, { + if ((await ethers.provider.getCode(contracts.sidechainFactory.implementation.address)) == '0x') { + await singletonFactory.deploy(contracts.sidechainFactory.implementation.bytecode, config.salt, { gasLimit: config.deployGasLimit, }) } - if ((await ethers.provider.getCode(contracts.factoryContract.proxy.address)) == '0x') { - await singletonFactory.deploy(contracts.factoryContract.proxy.bytecode, config.salt, { + if ((await ethers.provider.getCode(contracts.sidechainFactory.proxy.address)) == '0x') { + await singletonFactory.deploy(contracts.sidechainFactory.proxy.bytecode, config.salt, { gasLimit: config.deployGasLimit, }) } const instanceFactory = await ethers.getContractAt( - 'MultipleInstanceFactory', - contracts.factoryContract.proxy.address, + 'SidechainInstanceFactory', + contracts.sidechainFactory.proxy.address, ) return { @@ -72,10 +72,11 @@ describe('Multiple Instance Factory Tests', () => { expect(await instanceFactory.verifier()).to.be.equal(config.verifier) expect(await instanceFactory.hasher()).to.be.equal(config.hasher) 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) await expect(instanceFactory.setMerkleTreeHeight(1)).to.be.reverted @@ -196,4 +197,100 @@ describe('Multiple Instance Factory Tests', () => { [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], + ) + }) }) diff --git a/yarn.lock b/yarn.lock index 69b1d7e..8b08e9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -617,6 +617,11 @@ "@babel/helper-validator-identifier" "^7.14.9" 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": version "1.0.5" 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" 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: version "2.2.1" 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" 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: version "2.0.2" resolved "https://registry.yarnpkg.com/hardhat-log-remover/-/hardhat-log-remover-2.0.2.tgz#6014fe2c515ced1e0eaa7a4d854e37695aaac61a"