diff --git a/.solcover.js b/.solcover.js index db331df..ba8ea30 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,14 +1,3 @@ module.exports = { - skipFiles: [ - 'tornado_proxy/TornadoProxy.sol', - 'tornado_proxy/ITornadoTrees.sol', - 'tornado_proxy/ITornadoInstance.sol', - 'ERC20TornadoVirtual.sol', - 'denomination_templates/Add1Instance.sol', - 'denomination_templates/Add2Instances.sol', - 'denomination_templates/Add3Instances.sol', - 'denomination_templates/Add4Instances.sol', - 'denomination_templates/Add5Instances.sol', - 'denomination_templates/Add6Instances.sol', - ], + skipFiles: [], } diff --git a/README.md b/README.md index e71fcbb..3dc2b65 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,19 @@ ## About -This repository contains governance proposal factory for the addition of new Tornado ERC20 instances to the Tornado router. +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 + +### InstanceFactory + +Anyone can create a new ERC20 instance by calling `createInstanceClone` method of the factory with parameters: + +1. `address token` - address of ERC20 token for a new instance +2. `uint256 denomination` - denomination for new instance (tokens can only be deposited in certain denominations into instances) + +### InstanceFactoryWithRegistry 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): @@ -13,13 +25,15 @@ Anyone can create governance proposal for the addition of a new ERC20 instance b ## Factory parameters +### InstanceFactoryWithRegistry + 1. `max number of new instances in one proposal` - the current version supports the addition of a maximum of 3 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`. ## Warnings 1. This version of the factory creates a proposal for **immutable** Tornado instance initialization. -2. 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 `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. ## Tests @@ -46,7 +60,8 @@ Check config.js for actual values. With `salt` = `0x0000000000000000000000000000000000000000000000000000000047941987` address must be: -1. `InstanceFactory` - `0xBb3bd4849F88E709Ea6e5dC8F2C4cDc5293a12d5` +1. `InstanceFactory` - `0x9A04e3F1091A69CB53D163abE7ad9bbc86C23823` +1. `InstanceFactoryWithRegistry` - `0xee994E045B9Ec5a37f3f85d34f9fD087A0c69236` Check addresses with current config: @@ -61,14 +76,14 @@ Deploy InstanceFactory: yarn hardhat run scripts/deployInstanceFactory.js --network mainnet ``` +Deploy InstanceFactoryWithRegistry: + +```shell + yarn hardhat run scripts/deployInstanceFactoryWithRegistry.js --network mainnet +``` + Verify InstanceFactory on Etherscan: ``` yarn hardhat verify --network ``` - -With current config: - -``` - yarn hardhat verify --network mainnet 0x7a6e627DC6F66617b4A74Be097A8f56c622fa24c 0xce172ce1F20EC0B3728c9965470eaf994A03557A 0x83584f83f26aF4eDDA9CBe8C730bc87C364b28fe 20 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce 0xB20c66C4DE72433F3cE747b58B86830c459CA911 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C 200000000000000000000 -``` diff --git a/config.js b/config.js index fa4b8c3..5c9c57e 100644 --- a/config.js +++ b/config.js @@ -14,4 +14,5 @@ module.exports = { compWhale: '0xF977814e90dA44bFA03b6295A0616a897441aceC', creationFee: '200000000000000000000', // 200 TORN deployGasLimit: 7000000, + owner: '0xBAE5aBfa98466Dbe68836763B087f2d189f4D28f', } diff --git a/contracts/InstanceFactory.sol b/contracts/InstanceFactory.sol index 39527cd..4c4331a 100644 --- a/contracts/InstanceFactory.sol +++ b/contracts/InstanceFactory.sol @@ -7,68 +7,44 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; import "./ERC20TornadoCloneable.sol"; -import "./AddInstanceProposal.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol"; contract InstanceFactory is Ownable { using Clones for address; using Address for address; - address public immutable governance; - address public immutable torn; - address public immutable instanceRegistry; address public implementation; address public verifier; address public hasher; uint32 public merkleTreeHeight; - uint256 public creationFee; event NewVerifierSet(address indexed newVerifier); event NewHasherSet(address indexed newHasher); event NewTreeHeightSet(uint32 indexed newTreeHeight); - event NewCreationFeeSet(uint256 indexed newCreationFee); event NewImplementationSet(address indexed newImplemenentation); event NewInstanceCloneCreated(address indexed clone); - event NewGovernanceProposalCreated(address indexed proposal); - - /** - * @dev Throws if called by any account other than the Governance. - */ - modifier onlyGovernance() { - require(owner() == _msgSender(), "Caller is not the Governance"); - _; - } constructor( address _verifier, address _hasher, uint32 _merkleTreeHeight, - address _governance, - address _instanceRegistry, - address _torn, - uint256 _creationFee + address _owner ) { verifier = _verifier; hasher = _hasher; merkleTreeHeight = _merkleTreeHeight; - governance = _governance; - instanceRegistry = _instanceRegistry; - torn = _torn; - creationFee = _creationFee; ERC20TornadoCloneable implContract = new ERC20TornadoCloneable(_verifier, _hasher); implementation = address(implContract); - transferOwnership(_governance); + transferOwnership(_owner); } /** - * @dev Throws if called by any account other than the Governance. + * @dev Creates new Tornado instance. * @param _denomination denomination of new Tornado instance * @param _token address of ERC20 token for a new instance */ - function createInstanceClone(uint256 _denomination, address _token) external onlyGovernance returns (address) { + function createInstanceClone(uint256 _denomination, address _token) public virtual returns (address) { bytes32 salt = keccak256(abi.encodePacked(_denomination, _token)); require(!implementation.predictDeterministicAddress(salt).isContract(), "Instance already exists"); @@ -85,95 +61,27 @@ contract InstanceFactory is Ownable { return implementation.predictDeterministicAddress(salt); } - /** - * @dev Creates AddInstanceProposal with approve. - * @param _token address of ERC20 token for a new instance - * @param _uniswapPoolSwappingFee fee value of Uniswap instance which will be used for `TORN/token` price determination. - * `3000` means 0.3% fee Uniswap pool. - * @param _denominations list of denominations for each new instance - * @param _protocolFees list of protocol fees for each new instance. - * `100` means that instance withdrawal fee is 1% of denomination. - */ - function createProposalApprove( - address _token, - uint24 _uniswapPoolSwappingFee, - uint256[] memory _denominations, - uint32[] memory _protocolFees - ) external returns (address) { - require(IERC20(torn).transferFrom(msg.sender, governance, creationFee)); - return _createProposal(_token, _uniswapPoolSwappingFee, _denominations, _protocolFees); - } - - /** - * @dev Creates AddInstanceProposal with approve. - * @param _token address of ERC20 token for a new instance - * @param _uniswapPoolSwappingFee fee value of Uniswap instance which will be used for `TORN/token` price determination. - * `3000` means 0.3% fee Uniswap pool. - * @param _denominations list of denominations for each new instance - * @param _protocolFees list of protocol fees for each new instance. - * `100` means that instance withdrawal fee is 1% of denomination. - */ - function createProposalPermit( - address _token, - uint24 _uniswapPoolSwappingFee, - uint256[] memory _denominations, - uint32[] memory _protocolFees, - address creater, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (address) { - IERC20Permit(torn).permit(creater, address(this), creationFee, deadline, v, r, s); - require(IERC20(torn).transferFrom(creater, governance, creationFee)); - return _createProposal(_token, _uniswapPoolSwappingFee, _denominations, _protocolFees); - } - - function _createProposal( - address _token, - uint24 _uniswapPoolSwappingFee, - uint256[] memory _denominations, - uint32[] memory _protocolFees - ) internal returns (address) { - require(_token.isContract(), "Token is not contract"); - require(_uniswapPoolSwappingFee > 0, "uniswapPoolSwappingFee is zero"); - require(_denominations.length > 0, "Empty denominations"); - require(_denominations.length == _protocolFees.length, "Incorrect denominations/fees length"); - - address proposal = address( - new AddInstanceProposal(address(this), instanceRegistry, _token, _uniswapPoolSwappingFee, _denominations, _protocolFees) - ); - emit NewGovernanceProposalCreated(proposal); - - return proposal; - } - - function setVerifier(address _verifier) external onlyGovernance { + function setVerifier(address _verifier) external onlyOwner { verifier = _verifier; emit NewVerifierSet(verifier); } - function setHasher(address _hasher) external onlyGovernance { + function setHasher(address _hasher) external onlyOwner { hasher = _hasher; emit NewHasherSet(hasher); } - function setMerkleTreeHeight(uint32 _merkleTreeHeight) external onlyGovernance { + function setMerkleTreeHeight(uint32 _merkleTreeHeight) external onlyOwner { merkleTreeHeight = _merkleTreeHeight; emit NewTreeHeightSet(merkleTreeHeight); } - function setCreationFee(uint256 _creationFee) external onlyGovernance { - creationFee = _creationFee; - emit NewCreationFeeSet(_creationFee); - } - - function setImplementation(address _newImplementation) external onlyGovernance { + function setImplementation(address _newImplementation) external onlyOwner { implementation = _newImplementation; emit NewImplementationSet(implementation); } - function generateNewImplementation() external onlyGovernance { + function generateNewImplementation() external onlyOwner { implementation = address(new ERC20TornadoCloneable(verifier, hasher)); } } diff --git a/contracts/InstanceFactoryWithRegistry.sol b/contracts/InstanceFactoryWithRegistry.sol new file mode 100644 index 0000000..9046df7 --- /dev/null +++ b/contracts/InstanceFactoryWithRegistry.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.7.6; +pragma abicoder v2; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import "./AddInstanceProposal.sol"; +import "./InstanceFactory.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol"; + +contract InstanceFactoryWithRegistry is InstanceFactory { + using Address for address; + + address public immutable governance; + address public immutable torn; + address public immutable instanceRegistry; + uint256 public creationFee; + + event NewCreationFeeSet(uint256 indexed newCreationFee); + event NewGovernanceProposalCreated(address indexed proposal); + + /** + * @dev Throws if called by any account other than the Governance. + */ + modifier onlyGovernance() { + require(owner() == _msgSender(), "Caller is not the Governance"); + _; + } + + constructor( + address _verifier, + address _hasher, + uint32 _merkleTreeHeight, + address _governance, + address _instanceRegistry, + address _torn, + uint256 _creationFee + ) InstanceFactory(_verifier, _hasher, _merkleTreeHeight, _governance) { + governance = _governance; + instanceRegistry = _instanceRegistry; + torn = _torn; + creationFee = _creationFee; + } + + /** + * @dev 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 + * @param _uniswapPoolSwappingFee fee value of Uniswap instance which will be used for `TORN/token` price determination. + * `3000` means 0.3% fee Uniswap pool. + * @param _denominations list of denominations for each new instance + * @param _protocolFees list of protocol fees for each new instance. + * `100` means that instance withdrawal fee is 1% of denomination. + */ + function createProposalApprove( + address _token, + uint24 _uniswapPoolSwappingFee, + uint256[] memory _denominations, + uint32[] memory _protocolFees + ) external returns (address) { + require(IERC20(torn).transferFrom(msg.sender, governance, creationFee)); + return _createProposal(_token, _uniswapPoolSwappingFee, _denominations, _protocolFees); + } + + /** + * @dev Creates AddInstanceProposal with permit. + * @param _token address of ERC20 token for a new instance + * @param _uniswapPoolSwappingFee fee value of Uniswap instance which will be used for `TORN/token` price determination. + * `3000` means 0.3% fee Uniswap pool. + * @param _denominations list of denominations for each new instance + * @param _protocolFees list of protocol fees for each new instance. + * `100` means that instance withdrawal fee is 1% of denomination. + */ + function createProposalPermit( + address _token, + uint24 _uniswapPoolSwappingFee, + uint256[] memory _denominations, + uint32[] memory _protocolFees, + address creater, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (address) { + IERC20Permit(torn).permit(creater, address(this), creationFee, deadline, v, r, s); + require(IERC20(torn).transferFrom(creater, governance, creationFee)); + return _createProposal(_token, _uniswapPoolSwappingFee, _denominations, _protocolFees); + } + + function _createProposal( + address _token, + uint24 _uniswapPoolSwappingFee, + uint256[] memory _denominations, + uint32[] memory _protocolFees + ) internal returns (address) { + require(_token.isContract(), "Token is not contract"); + require(_uniswapPoolSwappingFee > 0, "uniswapPoolSwappingFee is zero"); + require(_denominations.length > 0, "Empty denominations"); + require(_denominations.length == _protocolFees.length, "Incorrect denominations/fees length"); + + address proposal = address( + new AddInstanceProposal(address(this), instanceRegistry, _token, _uniswapPoolSwappingFee, _denominations, _protocolFees) + ); + emit NewGovernanceProposalCreated(proposal); + + return proposal; + } + + function setCreationFee(uint256 _creationFee) external onlyGovernance { + creationFee = _creationFee; + emit NewCreationFeeSet(_creationFee); + } +} diff --git a/scripts/deployInstanceFactoryWithRegistry.js b/scripts/deployInstanceFactoryWithRegistry.js new file mode 100644 index 0000000..bc24b52 --- /dev/null +++ b/scripts/deployInstanceFactoryWithRegistry.js @@ -0,0 +1,28 @@ +const { ethers } = require('hardhat') +const config = require('../config') +const { generate } = require('../src/generateAddresses') + +async function deploy({ address, bytecode, singletonFactory }) { + const contractCode = await ethers.provider.getCode(address) + if (contractCode !== '0x') { + console.log(`Contract ${address} already deployed. Skipping...`) + return + } + await singletonFactory.deploy(bytecode, config.salt, { gasLimit: config.deployGasLimit }) +} + +async function main() { + const singletonFactory = await ethers.getContractAt('SingletonFactory', config.singletonFactory) + const contracts = await generate() + await deploy({ ...contracts.factoryWithRegistryContract, singletonFactory }) + console.log( + `Instance factory with registry contract have been deployed on ${contracts.factoryWithRegistryContract.address} address`, + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/src/generateAddresses.js b/src/generateAddresses.js index 21ea249..50ece99 100644 --- a/src/generateAddresses.js +++ b/src/generateAddresses.js @@ -6,6 +6,19 @@ async function generate(config = defaultConfig) { const deploymentBytecodeFactory = FactoryFactory.bytecode + FactoryFactory.interface + .encodeDeploy([config.verifier, config.hasher, config.merkleTreeHeight, config.owner]) + .slice(2) + + const factoryAddress = ethers.utils.getCreate2Address( + config.singletonFactory, + config.salt, + ethers.utils.keccak256(deploymentBytecodeFactory), + ) + + const FactoryWithRegistryFactory = await ethers.getContractFactory('InstanceFactoryWithRegistry') + const deploymentBytecodeFactoryWithRegistry = + FactoryWithRegistryFactory.bytecode + + FactoryWithRegistryFactory.interface .encodeDeploy([ config.verifier, config.hasher, @@ -17,10 +30,10 @@ async function generate(config = defaultConfig) { ]) .slice(2) - const factoryAddress = ethers.utils.getCreate2Address( + const factoryWithRegistryAddress = ethers.utils.getCreate2Address( config.singletonFactory, config.salt, - ethers.utils.keccak256(deploymentBytecodeFactory), + ethers.utils.keccak256(deploymentBytecodeFactoryWithRegistry), ) const result = { @@ -29,6 +42,11 @@ async function generate(config = defaultConfig) { bytecode: deploymentBytecodeFactory, isProxy: false, }, + factoryWithRegistryContract: { + address: factoryWithRegistryAddress, + bytecode: deploymentBytecodeFactoryWithRegistry, + isProxy: false, + }, } return result @@ -37,6 +55,7 @@ async function generate(config = defaultConfig) { async function generateWithLog() { const contracts = await generate() console.log('Instance factory contract: ', contracts.factoryContract.address) + console.log('Instance factory with registry contract: ', contracts.factoryWithRegistryContract.address) return contracts } diff --git a/test/factory.test.js b/test/factory.test.js index fa0b9a7..c588ab8 100644 --- a/test/factory.test.js +++ b/test/factory.test.js @@ -4,45 +4,30 @@ const { loadFixture } = waffle const { expect } = require('chai') const { BigNumber } = require('@ethersproject/bignumber') const config = require('../config') -const { getSignerFromAddress, minewait } = require('./utils') -const { PermitSigner } = require('../src/permit.js') +const { getSignerFromAddress } = require('./utils') const { generate } = require('../src/generateAddresses') +const { rbigint, createDeposit, toHex, generateProof, initialize } = require('tornado-cli') describe('Instance Factory Tests', () => { - const ProposalState = { - Pending: 0, - Active: 1, - Defeated: 2, - Timelocked: 3, - AwaitingExecution: 4, - Executed: 5, - Expired: 6, - } - const addressZero = ethers.constants.AddressZero async function fixture() { - const [sender, deployer, multisig] = await ethers.getSigners() + const [sender, deployer] = await ethers.getSigners() - const tornWhale = await getSignerFromAddress(config.tornWhale) + const owner = await getSignerFromAddress(config.owner) - const gov = await ethers.getContractAt('Governance', config.governance) + await sender.sendTransaction({ + to: config.owner, + value: ethers.utils.parseEther('1'), + }) - const tornToken = await ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', - config.TORN, - ) + const compWhale = await getSignerFromAddress(config.compWhale) const compToken = await ethers.getContractAt( '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', config.COMP, ) - const instanceRegistry = await ethers.getContractAt( - 'tornado-relayer-registry/contracts/tornado-proxy/InstanceRegistry.sol:InstanceRegistry', - config.instanceRegistry, - ) - // deploy InstanceFactory with CREATE2 const singletonFactory = await ethers.getContractAt( 'SingletonFactory', @@ -59,328 +44,115 @@ describe('Instance Factory Tests', () => { return { sender, deployer, - multisig, - tornWhale, - gov, - tornToken, + owner, compToken, - instanceRegistry, + compWhale, instanceFactory, } } it('Should have initialized all successfully', async function () { - const { sender, gov, tornToken, instanceRegistry, instanceFactory } = await loadFixture(fixture) + const { sender, compToken, instanceFactory } = await loadFixture(fixture) expect(sender.address).to.exist - expect(gov.address).to.exist - expect(tornToken.address).to.exist - expect(instanceRegistry.address).to.exist + expect(compToken.address).to.exist expect(instanceFactory.address).to.exist }) it('Should set correct params for factory', async function () { const { instanceFactory } = await loadFixture(fixture) - expect(await instanceFactory.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) }) it('Governance should be able to set factory params', async function () { - let { instanceFactory, gov } = await loadFixture(fixture) + let { instanceFactory, owner } = await loadFixture(fixture) await expect(instanceFactory.setVerifier(addressZero)).to.be.reverted - const govSigner = await getSignerFromAddress(gov.address) - instanceFactory = await instanceFactory.connect(govSigner) + instanceFactory = await instanceFactory.connect(owner) await instanceFactory.setVerifier(addressZero) await instanceFactory.setHasher(addressZero) await instanceFactory.setMerkleTreeHeight(1) - await instanceFactory.setCreationFee(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) await instanceFactory.setVerifier(config.verifier) await instanceFactory.setHasher(config.hasher) await instanceFactory.setMerkleTreeHeight(config.merkleTreeHeight) - await instanceFactory.setCreationFee(config.creationFee) 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) }) - it('Should successfully deploy/propose/execute proposal - add instance', async function () { - let { sender, instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) + it('Should successfully add instance', async function () { + let { sender, instanceFactory } = await loadFixture(fixture) - // deploy proposal ---------------------------------------------- - await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) - await tornToken.approve(instanceFactory.address, config.creationFee) + // deploy instance + await instanceFactory.connect(sender).createInstanceClone(ethers.utils.parseEther('1000'), config.COMP) - await expect(() => - instanceFactory - .connect(sender) - .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), - ).to.changeTokenBalances( - tornToken, - [sender, gov], - [BigNumber.from(0).sub(config.creationFee), config.creationFee], + // check instance initialization + let logs = await instanceFactory.queryFilter('NewInstanceCloneCreated') + const instance = await ethers.getContractAt( + 'ERC20TornadoCloneable', + ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)), ) - let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') - const proposal = await ethers.getContractAt( - 'AddInstanceProposal', - ethers.utils.getAddress('0x' + logs[0].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(config.COMP) - expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(3000) - 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(ethers.utils.parseEther('100')) - - // 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, 'COMP token 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('COMP token 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('ERC20TornadoCloneable', instanceAddr) - - expect(await instance.token()).to.be.equal(config.COMP) - 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(ethers.utils.parseEther('100')) - - const instanceData = await instanceRegistry.instances(instance.address) - expect(instanceData.isERC20).to.be.equal(true) - expect(instanceData.token).to.be.equal(config.COMP) - expect(instanceData.state).to.be.equal(1) - expect(instanceData.uniswapPoolSwappingFee).to.be.equal(3000) - expect(instanceData.protocolFeePercentage).to.be.equal(30) - }) - - it('Should successfully deploy/propose/execute proposal - add instances', async function () { - let { sender, instanceFactory, 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 expect(() => - instanceFactory - .connect(sender) - .createProposalApprove( - config.COMP, - 3000, - [ethers.utils.parseEther('100'), ethers.utils.parseEther('1000')], - [30, 30], - ), - ).to.changeTokenBalances( - tornToken, - [sender, gov], - [BigNumber.from(0).sub(config.creationFee), config.creationFee], - ) - - let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') - const proposal = await ethers.getContractAt( - 'AddInstanceProposal', - ethers.utils.getAddress('0x' + logs[0].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(config.COMP) - expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(3000) - expect(await proposal.numInstances()).to.be.equal(2) - expect(await proposal.protocolFeeByIndex(0)).to.be.equal(30) - expect(await proposal.protocolFeeByIndex(1)).to.be.equal(30) - expect(await proposal.denominationByIndex(0)).to.be.equal(ethers.utils.parseEther('100')) - expect(await proposal.denominationByIndex(1)).to.be.equal(ethers.utils.parseEther('1000')) - - // 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, 'COMP token 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('COMP token 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') - let instanceAddr = '0x' + logs[0].topics[1].slice(-40) - let instance = await ethers.getContractAt('ERC20TornadoCloneable', instanceAddr) - - expect(await instance.token()).to.be.equal(config.COMP) - 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(ethers.utils.parseEther('100')) - - let instanceData = await instanceRegistry.instances(instance.address) - expect(instanceData.isERC20).to.be.equal(true) - expect(instanceData.token).to.be.equal(config.COMP) - expect(instanceData.state).to.be.equal(1) - expect(instanceData.uniswapPoolSwappingFee).to.be.equal(3000) - expect(instanceData.protocolFeePercentage).to.be.equal(30) - - instanceAddr = '0x' + logs[1].topics[1].slice(-40) - instance = await ethers.getContractAt('ERC20TornadoCloneable', instanceAddr) - expect(await instance.token()).to.be.equal(config.COMP) 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(ethers.utils.parseEther('1000')) - - instanceData = await instanceRegistry.instances(instance.address) - expect(instanceData.isERC20).to.be.equal(true) - expect(instanceData.token).to.be.equal(config.COMP) - expect(instanceData.state).to.be.equal(1) - expect(instanceData.uniswapPoolSwappingFee).to.be.equal(3000) - expect(instanceData.protocolFeePercentage).to.be.equal(30) }) - it('Should successfully deploy proposal with permit', async function () { - let { instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) + it('Should deposit and withdraw into the new instance', async function () { + let { sender, instanceFactory, compToken, compWhale } = await loadFixture(fixture) - const privateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3' - const publicKey = '0x' + ethers.utils.computeAddress(Buffer.from(privateKey.slice(2), 'hex')) - const sender = await ethers.getSigner(publicKey.slice(2)) + // deploy instance + await instanceFactory.connect(sender).createInstanceClone(ethers.utils.parseEther('100'), config.COMP) - await expect(() => - tornToken.connect(tornWhale).transfer(sender.address, config.creationFee), - ).to.changeTokenBalances( - tornToken, - [tornWhale, sender], - [BigNumber.from(0).sub(config.creationFee), config.creationFee], + let logs = await instanceFactory.queryFilter('NewInstanceCloneCreated') + const instance = await ethers.getContractAt( + 'ERC20TornadoCloneable', + ethers.utils.getAddress('0x' + logs[logs.length - 1].topics[1].slice(-40)), ) - // prepare permit data - const domain = { - name: await tornToken.name(), - version: '1', - chainId: 1, - verifyingContract: tornToken.address, - } + // check instance work ------------------------------------------ + const depo = createDeposit({ + nullifier: rbigint(31), + secret: rbigint(31), + }) - const curTimestamp = Math.trunc(new Date().getTime() / 1000) - const args = { - owner: sender, - spender: instanceFactory.address, - value: config.creationFee, - nonce: 0, - deadline: curTimestamp + 1000, - } + const value = ethers.utils.parseEther('100') - 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 compToken.connect(compWhale).transfer(sender.address, value) + await compToken.connect(sender).approve(instance.address, value) - await expect(() => - instanceFactory.createProposalPermit( - config.COMP, - 3000, - [ethers.utils.parseEther('100')], - [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], + await expect(() => instance.deposit(toHex(depo.commitment), [])).to.changeTokenBalances( + compToken, + [sender, instance], + [BigNumber.from(0).sub(value), value], ) - let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') - const proposal = await ethers.getContractAt( - 'AddInstanceProposal', - ethers.utils.getAddress('0x' + logs[0].topics[1].slice(-40)), - ) + let pevents = await instance.queryFilter('Deposit') + await initialize({ merkleTreeHeight: 20 }) - 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(config.COMP) - expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(3000) - 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(ethers.utils.parseEther('100')) + const { proof, args } = await generateProof({ + deposit: depo, + recipient: sender.address, + events: pevents, + }) + + await expect(() => instance.withdraw(proof, ...args)).to.changeTokenBalances( + compToken, + [instance, sender], + [BigNumber.from(0).sub(value), value], + ) }) }) diff --git a/test/factory.with.registry.test.js b/test/factory.with.registry.test.js new file mode 100644 index 0000000..271c0ef --- /dev/null +++ b/test/factory.with.registry.test.js @@ -0,0 +1,482 @@ +const hre = require('hardhat') +const { ethers, waffle } = hre +const { loadFixture } = waffle +const { expect } = require('chai') +const { BigNumber } = require('@ethersproject/bignumber') +const config = require('../config') +const { getSignerFromAddress, minewait } = require('./utils') +const { PermitSigner } = require('../src/permit.js') +const { generate } = require('../src/generateAddresses') +const { rbigint, createDeposit, toHex, generateProof, initialize } = require('tornado-cli') + +describe('Instance Factory With Registry Tests', () => { + const ProposalState = { + Pending: 0, + Active: 1, + Defeated: 2, + Timelocked: 3, + AwaitingExecution: 4, + Executed: 5, + Expired: 6, + } + + const addressZero = ethers.constants.AddressZero + + async function fixture() { + const [sender, deployer, multisig] = await ethers.getSigners() + + const tornWhale = await getSignerFromAddress(config.tornWhale) + + const compWhale = await getSignerFromAddress(config.compWhale) + + const gov = await ethers.getContractAt('Governance', config.governance) + + const router = await ethers.getContractAt( + 'tornado-relayer-registry/contracts/tornado-proxy/TornadoRouter.sol:TornadoRouter', + config.router, + ) + + const tornToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + config.TORN, + ) + + const compToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', + config.COMP, + ) + + const instanceRegistry = await ethers.getContractAt( + 'tornado-relayer-registry/contracts/tornado-proxy/InstanceRegistry.sol:InstanceRegistry', + config.instanceRegistry, + ) + + // deploy InstanceFactoryWithRegistry with CREATE2 + const singletonFactory = await ethers.getContractAt( + 'SingletonFactory', + config.singletonFactoryVerboseWrapper, + ) + const contracts = await generate() + if ((await ethers.provider.getCode(contracts.factoryWithRegistryContract.address)) == '0x') { + await singletonFactory.deploy(contracts.factoryWithRegistryContract.bytecode, config.salt, { + gasLimit: config.deployGasLimit, + }) + } + const instanceFactory = await ethers.getContractAt( + 'InstanceFactoryWithRegistry', + contracts.factoryWithRegistryContract.address, + ) + + return { + sender, + deployer, + multisig, + tornWhale, + compWhale, + router, + gov, + tornToken, + compToken, + instanceRegistry, + instanceFactory, + } + } + + it('Should have initialized all successfully', async function () { + const { sender, gov, tornToken, instanceRegistry, instanceFactory } = 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 + }) + + it('Should set correct params for factory', async function () { + const { instanceFactory } = await loadFixture(fixture) + + expect(await instanceFactory.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) + }) + + it('Governance should be able to set factory params', async function () { + let { instanceFactory, gov } = await loadFixture(fixture) + + await expect(instanceFactory.setVerifier(addressZero)).to.be.reverted + + const govSigner = await getSignerFromAddress(gov.address) + instanceFactory = await instanceFactory.connect(govSigner) + + await instanceFactory.setVerifier(addressZero) + await instanceFactory.setHasher(addressZero) + await instanceFactory.setMerkleTreeHeight(1) + await instanceFactory.setCreationFee(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) + + await instanceFactory.setVerifier(config.verifier) + await instanceFactory.setHasher(config.hasher) + await instanceFactory.setMerkleTreeHeight(config.merkleTreeHeight) + await instanceFactory.setCreationFee(config.creationFee) + + 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) + }) + + it('Should successfully deploy/propose/execute proposal - add instance', async function () { + let { sender, instanceFactory, 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 expect(() => + instanceFactory + .connect(sender) + .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), + ).to.changeTokenBalances( + tornToken, + [sender, gov], + [BigNumber.from(0).sub(config.creationFee), config.creationFee], + ) + + let logs = await instanceFactory.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(config.COMP) + expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(3000) + 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(ethers.utils.parseEther('100')) + + // 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, 'COMP token 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('COMP token 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('ERC20TornadoCloneable', instanceAddr) + + expect(await instance.token()).to.be.equal(config.COMP) + 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(ethers.utils.parseEther('100')) + + const instanceData = await instanceRegistry.instances(instance.address) + expect(instanceData.isERC20).to.be.equal(true) + expect(instanceData.token).to.be.equal(config.COMP) + expect(instanceData.state).to.be.equal(1) + expect(instanceData.uniswapPoolSwappingFee).to.be.equal(3000) + expect(instanceData.protocolFeePercentage).to.be.equal(30) + }) + + it('Should successfully deploy/propose/execute proposal - add instances', async function () { + let { sender, instanceFactory, 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 expect(() => + instanceFactory + .connect(sender) + .createProposalApprove( + config.COMP, + 3000, + [ethers.utils.parseEther('100'), ethers.utils.parseEther('1000')], + [30, 30], + ), + ).to.changeTokenBalances( + tornToken, + [sender, gov], + [BigNumber.from(0).sub(config.creationFee), config.creationFee], + ) + + let logs = await instanceFactory.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(config.COMP) + expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(3000) + expect(await proposal.numInstances()).to.be.equal(2) + expect(await proposal.protocolFeeByIndex(0)).to.be.equal(30) + expect(await proposal.protocolFeeByIndex(1)).to.be.equal(30) + expect(await proposal.denominationByIndex(0)).to.be.equal(ethers.utils.parseEther('100')) + expect(await proposal.denominationByIndex(1)).to.be.equal(ethers.utils.parseEther('1000')) + + // 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, 'COMP token 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('COMP token 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') + let instanceAddr = '0x' + logs[logs.length - 2].topics[1].slice(-40) + let instance = await ethers.getContractAt('ERC20TornadoCloneable', instanceAddr) + + expect(await instance.token()).to.be.equal(config.COMP) + 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(ethers.utils.parseEther('100')) + + let instanceData = await instanceRegistry.instances(instance.address) + expect(instanceData.isERC20).to.be.equal(true) + expect(instanceData.token).to.be.equal(config.COMP) + expect(instanceData.state).to.be.equal(1) + expect(instanceData.uniswapPoolSwappingFee).to.be.equal(3000) + expect(instanceData.protocolFeePercentage).to.be.equal(30) + + instanceAddr = '0x' + logs[logs.length - 1].topics[1].slice(-40) + instance = await ethers.getContractAt('ERC20TornadoCloneable', instanceAddr) + + expect(await instance.token()).to.be.equal(config.COMP) + 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(ethers.utils.parseEther('1000')) + + instanceData = await instanceRegistry.instances(instance.address) + expect(instanceData.isERC20).to.be.equal(true) + expect(instanceData.token).to.be.equal(config.COMP) + expect(instanceData.state).to.be.equal(1) + expect(instanceData.uniswapPoolSwappingFee).to.be.equal(3000) + expect(instanceData.protocolFeePercentage).to.be.equal(30) + }) + + it('Should successfully deploy proposal with permit', async function () { + let { instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) + + 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: instanceFactory.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(() => + instanceFactory.createProposalPermit( + config.COMP, + 3000, + [ethers.utils.parseEther('100')], + [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 instanceFactory.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(config.COMP) + expect(await proposal.uniswapPoolSwappingFee()).to.be.equal(3000) + 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(ethers.utils.parseEther('100')) + }) + + it('Should deposit and withdraw into the new instance', async function () { + let { sender, instanceFactory, 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 expect(() => + instanceFactory + .connect(sender) + .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]), + ).to.changeTokenBalances( + tornToken, + [sender, gov], + [BigNumber.from(0).sub(config.creationFee), config.creationFee], + ) + + let logs = await instanceFactory.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, 'COMP token 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('ERC20TornadoCloneable', instanceAddr) + + // check instance work ------------------------------------------ + const depo = createDeposit({ + nullifier: rbigint(31), + secret: rbigint(31), + }) + + const value = ethers.utils.parseEther('100') + + await compToken.connect(compWhale).transfer(sender.address, value) + await compToken.connect(sender).approve(router.address, value) + + await expect(() => router.deposit(instance.address, toHex(depo.commitment), [])).to.changeTokenBalances( + compToken, + [sender, instance], + [BigNumber.from(0).sub(value), value], + ) + + 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.changeTokenBalances( + compToken, + [instance, sender], + [BigNumber.from(0).sub(value), value], + ) + }) +}) diff --git a/test/instance.tests.js b/test/instance.tests.js deleted file mode 100644 index a85af3b..0000000 --- a/test/instance.tests.js +++ /dev/null @@ -1,166 +0,0 @@ -const hre = require('hardhat') -const { ethers, waffle } = hre -const { loadFixture } = waffle -const { expect } = require('chai') -const { BigNumber } = require('@ethersproject/bignumber') -const { rbigint, createDeposit, toHex, generateProof, initialize } = require('tornado-cli') -const config = require('../config') -const { getSignerFromAddress, minewait } = require('./utils') -const { generate } = require('../src/generateAddresses') - -describe('Instance Factory Tests', () => { - const ProposalState = { - Pending: 0, - Active: 1, - Defeated: 2, - Timelocked: 3, - AwaitingExecution: 4, - Executed: 5, - Expired: 6, - } - - async function fixture() { - const [sender, deployer, multisig] = await ethers.getSigners() - - const tornWhale = await getSignerFromAddress(config.tornWhale) - const compWhale = await getSignerFromAddress(config.compWhale) - - let gov = await ethers.getContractAt('Governance', config.governance) - - const tornToken = await ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', - config.TORN, - ) - - const compToken = await ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', - config.COMP, - ) - - const instanceRegistry = await ethers.getContractAt( - 'tornado-relayer-registry/contracts/tornado-proxy/InstanceRegistry.sol:InstanceRegistry', - config.instanceRegistry, - ) - - const router = await ethers.getContractAt( - 'tornado-relayer-registry/contracts/tornado-proxy/TornadoRouter.sol:TornadoRouter', - config.router, - ) - - // deploy InstanceFactory with CREATE2 - const singletonFactory = await ethers.getContractAt( - 'SingletonFactory', - config.singletonFactoryVerboseWrapper, - ) - const contracts = await generate() - if ((await ethers.provider.getCode(contracts.factoryContract.address)) == '0x') { - await singletonFactory.deploy(contracts.factoryContract.bytecode, config.salt, { - gasLimit: config.deployGasLimit, - }) - } - const instanceFactory = await ethers.getContractAt('InstanceFactory', contracts.factoryContract.address) - - // deploy proposal - await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) - await tornToken.approve(instanceFactory.address, config.creationFee) - - await instanceFactory - .connect(sender) - .createProposalApprove(config.COMP, 3000, [ethers.utils.parseEther('100')], [30]) - - let logs = await instanceFactory.queryFilter('NewGovernanceProposalCreated') - const proposal = await ethers.getContractAt( - 'AddInstanceProposal', - ethers.utils.getAddress('0x' + logs[0].topics[1].slice(-40)), - ) - - // propose proposal - 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, 'COMP token instance proposal') - const 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 - 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) - - logs = await instanceFactory.queryFilter('NewInstanceCloneCreated') - const instance = await ethers.getContractAt( - 'ERC20TornadoCloneable', - ethers.utils.getAddress('0x' + logs[0].topics[1].slice(-40)), - ) - - return { - sender, - deployer, - multisig, - tornWhale, - compWhale, - gov, - tornToken, - compToken, - instanceRegistry, - router, - instanceFactory, - instance, - } - } - - it('Should set correct params for factory', async function () { - const { instance } = await loadFixture(fixture) - - expect(await instance.token()).to.be.equal(config.COMP) - 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(ethers.utils.parseEther('100')) - }) - - it('Should deposit and withdraw into the new instance', async function () { - const { sender, instance, compToken, compWhale, router } = await loadFixture(fixture) - - const depo = createDeposit({ - nullifier: rbigint(31), - secret: rbigint(31), - }) - - const value = ethers.utils.parseEther('100') - - await compToken.connect(compWhale).transfer(sender.address, value) - await compToken.connect(sender).approve(router.address, value) - - await expect(() => router.deposit(instance.address, toHex(depo.commitment), [])).to.changeTokenBalances( - compToken, - [sender, instance], - [BigNumber.from(0).sub(value), value], - ) - - 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.changeTokenBalances( - compToken, - [instance, sender], - [BigNumber.from(0).sub(value), value], - ) - }) -})