diff --git a/config.js b/config.js index 1a06dbc..07a8148 100644 --- a/config.js +++ b/config.js @@ -9,4 +9,5 @@ module.exports = { COMP: '0xc00e94Cb662C3520282E6f5717214004A7f26888', TORN: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C', tornWhale: '0xF977814e90dA44bFA03b6295A0616a897441aceC', + creationFee: '200000000000000000000', // 200 TORN } diff --git a/contracts/AddInstanceProposal.sol b/contracts/AddInstanceProposal.sol index fdae4e7..acfb386 100644 --- a/contracts/AddInstanceProposal.sol +++ b/contracts/AddInstanceProposal.sol @@ -70,19 +70,25 @@ contract AddInstanceProposal { } function denominationByIndex(uint256 _index) public view returns (uint256) { - if (_index == 0) { return denomination0; } - else if (_index == 1) { return denomination1; } - else if (_index == 2) { return denomination2; } - else { + if (_index == 0) { + return denomination0; + } else if (_index == 1) { + return denomination1; + } else if (_index == 2) { + return denomination2; + } else { revert("Invalid instance index"); } } function protocolFeeByIndex(uint256 _index) public view returns (uint32) { - if (_index == 0) { return protocolFee0; } - else if (_index == 1) { return protocolFee1; } - else if (_index == 2) { return protocolFee2; } - else { + if (_index == 0) { + return protocolFee0; + } else if (_index == 1) { + return protocolFee1; + } else if (_index == 2) { + return protocolFee2; + } else { revert("Invalid instance index"); } } diff --git a/contracts/ERC20TornadoCloneable.sol b/contracts/ERC20TornadoCloneable.sol index d2f8f1e..45fde4d 100644 --- a/contracts/ERC20TornadoCloneable.sol +++ b/contracts/ERC20TornadoCloneable.sol @@ -14,7 +14,7 @@ contract ERC20TornadoCloneable is ERC20Tornado { address _token ) external { require(denomination == 0 && levels == 0, "already initialized"); - + token = IERC20(_token); require(_denomination > 0, "denomination should be greater than 0"); denomination = _denomination; diff --git a/contracts/InstanceFactory.sol b/contracts/InstanceFactory.sol index 322c801..c7d9d66 100644 --- a/contracts/InstanceFactory.sol +++ b/contracts/InstanceFactory.sol @@ -9,22 +9,26 @@ import "@openzeppelin/contracts/proxy/Clones.sol"; import "./ERC20TornadoCloneable.sol"; import "./AddInstanceProposal.sol"; import "./interfaces/IGovernance.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); @@ -34,13 +38,17 @@ contract InstanceFactory is Ownable { address _hasher, uint32 _merkleTreeHeight, address _governance, - address _instanceRegistry + address _instanceRegistry, + address _torn, + uint256 _creationFee ) { verifier = _verifier; hasher = _hasher; merkleTreeHeight = _merkleTreeHeight; governance = _governance; instanceRegistry = _instanceRegistry; + torn = _torn; + creationFee = _creationFee; ERC20TornadoCloneable implContract = new ERC20TornadoCloneable(_verifier, _hasher); implementation = address(implContract); @@ -65,25 +73,46 @@ contract InstanceFactory is Ownable { return implementation.predictDeterministicAddress(salt); } - function createNewProposal( + 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); + } + + 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"); // TODO should we check that such instance already exist? require(_uniswapPoolSwappingFee > 0, "uniswapPoolSwappingFee is zero"); // TODO should we check > 0 ? 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 - )); + address proposal = address( + new AddInstanceProposal(address(this), instanceRegistry, _token, _uniswapPoolSwappingFee, _denominations, _protocolFees) + ); emit NewGovernanceProposalCreated(proposal); return proposal; @@ -104,6 +133,11 @@ contract InstanceFactory is Ownable { emit NewTreeHeightSet(merkleTreeHeight); } + function setCreationFee(uint256 _creationFee) external onlyOwner { + creationFee = _creationFee; + emit NewCreationFeeSet(_creationFee); + } + function setImplementation(address _newImplementation) external onlyOwner { implementation = _newImplementation; emit NewImplementationSet(implementation); diff --git a/contracts/interfaces/IInstanceRegistry.sol b/contracts/interfaces/IInstanceRegistry.sol index 657e340..e5d37df 100644 --- a/contracts/interfaces/IInstanceRegistry.sol +++ b/contracts/interfaces/IInstanceRegistry.sol @@ -6,8 +6,10 @@ pragma abicoder v2; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IInstanceRegistry { - - enum InstanceState { DISABLED, ENABLED} + enum InstanceState { + DISABLED, + ENABLED + } struct Instance { bool isERC20; diff --git a/contracts/mock/CompileDummy.sol b/contracts/mock/CompileDummy.sol index 0bad471..b69bca4 100644 --- a/contracts/mock/CompileDummy.sol +++ b/contracts/mock/CompileDummy.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.6.12 || 0.7.6; +pragma solidity 0.6.12||0.7.6; import { Governance } from "tornado-governance/contracts/v1/Governance.sol"; import { InstanceRegistry } from "tornado-relayer-registry/contracts/tornado-proxy/InstanceRegistry.sol"; diff --git a/scripts/permit.js b/scripts/permit.js new file mode 100644 index 0000000..36ce381 --- /dev/null +++ b/scripts/permit.js @@ -0,0 +1,44 @@ +const { EIP712Signer } = require('@ticket721/e712') + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +] + +class PermitSigner extends EIP712Signer { + constructor(_domain, _permitArgs) { + super(_domain, ['Permit', Permit]) + this.permitArgs = _permitArgs + } + + // Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline) + setPermitInfo(_permitArgs) { + this.permitArgs = _permitArgs + } + + getPayload() { + return this.generatePayload(this.permitArgs, 'Permit') + } + + async getSignature(privateKey) { + let payload = this.getPayload() + payload.message.owner = payload.message.owner.address + const { hex, v, r, s } = await this.sign(privateKey, payload) + return { + hex, + v, + r: '0x' + r, + s: '0x' + s, + } + } + + getSignerAddress(permitArgs, signature) { + const original_payload = this.generatePayload(permitArgs, 'Permit') + return this.verify(original_payload, signature) + } +} + +module.exports = { PermitSigner } diff --git a/test/instance.factory.test.js b/test/instance.factory.test.js index ccef3bb..327c7e2 100644 --- a/test/instance.factory.test.js +++ b/test/instance.factory.test.js @@ -7,7 +7,7 @@ const { rbigint, createDeposit, toHex, generateProof, initialize } = require('to const MixerContractABI = require('tornado-cli/build/contracts/Mixer.abi.json') const config = require('../config') const { getSignerFromAddress, minewait } = require('./utils') - +const { PermitSigner } = require('../scripts/permit.js') describe('Instance Factory Tests', () => { const ProposalState = { @@ -26,14 +26,11 @@ describe('Instance Factory Tests', () => { const [sender, deployer, multisig] = await ethers.getSigners() const tornWhale = await getSignerFromAddress(config.tornWhale) - - const gov = await ethers.getContractAt( - 'Governance', - config.governance, - ) + + const gov = await ethers.getContractAt('Governance', config.governance) const tornToken = await ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', config.TORN, ) @@ -43,7 +40,7 @@ describe('Instance Factory Tests', () => { ) instanceRegistry = await ethers.getContractAt( - 'tornado-relayer-registry/contracts/tornado-proxy/InstanceRegistry.sol:InstanceRegistry', + 'tornado-relayer-registry/contracts/tornado-proxy/InstanceRegistry.sol:InstanceRegistry', config.instanceRegistry, ) @@ -54,11 +51,23 @@ describe('Instance Factory Tests', () => { config.hasher, config.merkleTreeHeight, config.governance, - config.instanceRegistry + config.instanceRegistry, + config.TORN, + config.creationFee, ) await instanceFactory.deployed() - return { sender, deployer, multisig, tornWhale, gov, tornToken, compToken, instanceRegistry, instanceFactory } + return { + sender, + deployer, + multisig, + tornWhale, + gov, + tornToken, + compToken, + instanceRegistry, + instanceFactory, + } } it('Should have initialized all successfully', async function () { @@ -73,11 +82,13 @@ describe('Instance Factory Tests', () => { 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.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 () { @@ -87,48 +98,58 @@ describe('Instance Factory Tests', () => { 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.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.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 { instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) + let { sender, instanceFactory, gov, instanceRegistry, tornWhale, tornToken } = await loadFixture(fixture) // deploy proposal ---------------------------------------------- - let tx = await instanceFactory.createNewProposal( - config.COMP, - 3000, - [ethers.utils.parseEther('100')], - [30] - ) - let receipt = await tx.wait() + await tornToken.connect(tornWhale).transfer(sender.address, config.creationFee) + await tornToken.approve(instanceFactory.address, config.creationFee) - const proposal = await ethers.getContractAt( - 'AddInstanceProposal', - receipt.events[0].args[0], + 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], ) - - 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')) + + let logs = await ethers.provider.getLogs(instanceFactory.filters.NewGovernanceProposalCreated()) + const proposal = await ethers.getContractAt( + 'AddInstanceProposal', + ethers.utils.getAddress('0x' + logs[0].topics[1].slice(26)), + ) + + 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 @@ -147,7 +168,7 @@ describe('Instance Factory Tests', () => { 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 @@ -161,7 +182,7 @@ describe('Instance Factory Tests', () => { .toNumber(), ) expect(await gov.state(id)).to.be.equal(ProposalState.AwaitingExecution) - + tx = await gov.execute(id) expect(await gov.state(id)).to.be.equal(ProposalState.Executed) @@ -169,25 +190,92 @@ describe('Instance Factory Tests', () => { // check instance initialization -------------------------------- receipt = await tx.wait() const instanceAddr = '0x' + receipt.events[0].topics[1].toString().slice(-40) - const instance = await ethers.getContractAt( - 'ERC20TornadoCloneable', - instanceAddr, - ) + 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')) + 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) + 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 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 ethers.provider.getLogs(instanceFactory.filters.NewGovernanceProposalCreated()) + const proposal = await ethers.getContractAt( + 'AddInstanceProposal', + ethers.utils.getAddress('0x' + logs[0].topics[1].slice(26)), + ) + + 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 prepare data for instance deposit/withdraw tests', async () => { // const RAITokenAddress = '0x03ab458634910AaD20eF5f1C8ee96F1D6ac54919' // await sendr('hardhat_impersonateAccount', ['0x46a0B4Fa58141ABa23185e79f7047A7dFd0FF100'])