diff --git a/contracts/anchors/bridged/LinkableCompTokenAnchorPoseidon2.sol b/contracts/anchors/bridged/LinkableCompTokenAnchorPoseidon2.sol index 2a696c1e9..865e49003 100644 --- a/contracts/anchors/bridged/LinkableCompTokenAnchorPoseidon2.sol +++ b/contracts/anchors/bridged/LinkableCompTokenAnchorPoseidon2.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; -import "../../interfaces/IMintableCompToken.sol"; +import "../../tokens/TokenWrapper.sol"; import "./LinkableAnchorPoseidon2.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -20,10 +20,18 @@ contract LinkableCompTokenAnchorPoseidon2 is LinkableAnchorPoseidon2 { uint256 _denomination, uint32 _merkleTreeHeight, uint32 _chainID, - IMintableCompToken _token + TokenWrapper _token ) LinkableAnchorPoseidon2(_verifier, _hasher, _denomination, _merkleTreeHeight, _chainID) { token = address(_token); } + + function wrap(address tokenAddress, uint256 amount) public { + TokenWrapper(token).wrap(msg.sender, tokenAddress, amount); + } + + function unwrap(address tokenAddress, uint256 amount) public { + TokenWrapper(token).unwrap(msg.sender, tokenAddress, amount); + } function _processDeposit() internal override { require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance"); diff --git a/contracts/tokens/TokenWrapper.sol b/contracts/tokens/TokenWrapper.sol index 79fa775ae..22fb24dc0 100644 --- a/contracts/tokens/TokenWrapper.sol +++ b/contracts/tokens/TokenWrapper.sol @@ -28,13 +28,14 @@ abstract contract TokenWrapper is CompToken { @param tokenAddress Address of ERC20 to transfer. @param amount Amount of tokens to transfer. */ - function wrap(address tokenAddress, uint256 amount) public { + function wrap(address sender, address tokenAddress, uint256 amount) public { + require(hasRole(MINTER_ROLE, msg.sender), "ERC20PresetMinterPauser: must have minter role"); require(_isValidAddress(tokenAddress), "Invalid token address"); require(_isValidAmount(amount), "Invalid token amount"); - // transfer liquidity to tthe token wrapper - IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + // transfer liquidity to the token wrapper + IERC20(tokenAddress).transferFrom(sender, address(this), amount); // mint the wrapped token for the sender - mint(msg.sender, amount); + mint(sender, amount); } /** @@ -42,13 +43,14 @@ abstract contract TokenWrapper is CompToken { @param tokenAddress Address of ERC20 to unwrap into. @param amount Amount of tokens to burn. */ - function unwrap(address tokenAddress, uint256 amount) public { + function unwrap(address sender, address tokenAddress, uint256 amount) public { + require(hasRole(MINTER_ROLE, msg.sender), "ERC20PresetMinterPauser: must have minter role"); require(_isValidAddress(tokenAddress), "Invalid token address"); require(_isValidAmount(amount), "Invalid token amount"); // burn wrapped token from sender - burnFrom(msg.sender, amount); + burnFrom(sender, amount); // transfer liquidity from the token wrapper to the sender - ERC20PresetMinterPauser(tokenAddress).transferFrom(address(this), msg.sender, amount); + IERC20(tokenAddress).transfer(sender, amount); } /** @dev this function is defined in a child contract */ diff --git a/test/integration/compAnchorIntegration.js b/test/integration/compAnchorIntegration.js new file mode 100644 index 000000000..7f1c6638c --- /dev/null +++ b/test/integration/compAnchorIntegration.js @@ -0,0 +1,559 @@ +const TruffleAssert = require('truffle-assertions'); +const Ethers = require('ethers'); +const helpers = require('../helpers'); +const { toBN } = require('web3-utils') +const assert = require('assert'); +const BridgeContract = artifacts.require('Bridge'); +const LinkableAnchorContract = artifacts.require('./LinkableCompTokenAnchorPoseidon2.sol'); +const Verifier = artifacts.require('./VerifierPoseidonBridge.sol'); +const Hasher = artifacts.require('PoseidonT3'); +const Token = artifacts.require('ERC20Mock'); +const CompToken = artifacts.require('CompToken') +const AnchorHandlerContract = artifacts.require('AnchorHandler'); + +const fs = require('fs') +const path = require('path'); +const { NATIVE_AMOUNT } = process.env +let prefix = 'poseidon-test' +const snarkjs = require('snarkjs'); +const BN = require('bn.js'); +const F = require('circomlib').babyJub.F; +const Scalar = require('ffjavascript').Scalar; +const MerkleTree = require('../../lib/MerkleTree'); + +const { + etherUnsigned +} = helpers; +const { network } = require('hardhat'); +const TimelockHarness = artifacts.require('TimelockHarness'); +const GovernorBravoImmutable = artifacts.require('GovernorBravoImmutable'); +const GovernedTokenWrapper = artifacts.require('GovernedTokenWrapper'); +const solparse = require('solparse'); + +const governorBravoPath = path.join(__dirname, '../../', 'contracts', 'governance/GovernorBravoInterfaces.sol'); +const statesInverted = solparse + .parseFile(governorBravoPath) + .body + .find(k => k.name === 'GovernorBravoDelegateStorageV1') + .body + .find(k => k.name == 'ProposalState') + .members + +const states = Object.entries(statesInverted).reduce((obj, [key, value]) => ({ ...obj, [value]: key }), {}); + + +contract('E2E LinkableCompTokenAnchors - Cross chain withdrawals with gov bravo', async accounts => { + const relayerThreshold = 1; + const originChainID = 1; + const destChainID = 2; + const bravoAdmin = accounts[0]; + const user1 = accounts[1]; + const user2 = accounts[2]; + const relayer1Address = accounts[3]; + const sender = accounts[5]; + const operator = accounts[6]; + const initialTokenMintAmount = BigInt(1e25); + const tokenDenomination = '1000000000000000000000'; + const merkleTreeHeight = 30; + const fee = BigInt((new BN(`${NATIVE_AMOUNT}`).shrn(1)).toString()) || BigInt((new BN(`${1e17}`)).toString()); + const refund = BigInt((new BN('0')).toString()); + + let MINTER_ROLE; + let originMerkleRoot; + let destMerkleRoot; + let originBlockHeight = 1; + let destBlockHeight = 1; + let originUpdateNonce; + let destUpdateNonce; + let hasher, verifier; + let originDeposit; + let originWrapperToken; + let destWrapperToken; + let destDeposit; + let tree; + let createWitness; + let OriginBridgeInstance; + let OriginChainLinkableAnchorInstance; + let OriginAnchorHandlerInstance; + let originDepositData; + let originDepositDataHash; + let resourceID; + let initialResourceIDs; + let originInitialContractAddresses; + let DestBridgeInstance; + let DestChainLinkableAnchorInstance + let DestAnchorHandlerInstance; + let destDepositData; + let destDepositDataHash; + let destInitialContractAddresses; + + const name = 'Webb-1'; + const symbol = 'WEB1'; + let originGov; + let destGov; + let originWEBB; + let destWEBB; + let originToken; + let destToken; + + beforeEach(async () => { + // create tokens + await Promise.all([ + Token.new().then(instance => originToken = instance), + Token.new().then(instance => destToken = instance), + CompToken.new('Webb', 'WEBB').then(instance => originWEBB = instance), + CompToken.new('Webb', 'WEBB').then(instance => destWEBB = instance), + ]) + // instantiate governed token wrappers and gov bravos + delay = etherUnsigned(2 * 24 * 60 * 60).multipliedBy(2) + originTimelock = await TimelockHarness.new(bravoAdmin, delay); + destTimelock = await TimelockHarness.new(bravoAdmin, delay); + originGov = await GovernorBravoImmutable.new(originTimelock.address, originWEBB.address, bravoAdmin, 10, 1, '100000000000000000000000'); + destGov = await GovernorBravoImmutable.new(destTimelock.address, destWEBB.address, bravoAdmin, 10, 1, '100000000000000000000000'); + await originGov._initiate(); + await originTimelock.harnessSetAdmin(originGov.address); + await destGov._initiate(); + await destTimelock.harnessSetAdmin(destGov.address); + //initialize TokenWrappers + originWrapperToken = await GovernedTokenWrapper.new(name, symbol, originTimelock.address, '1000000000000000000000000', {from: sender}); + destWrapperToken = await GovernedTokenWrapper.new(name, symbol, destTimelock.address, '1000000000000000000000000', {from: sender}); + // delegate bravoAdmin on both chains + await originWEBB.mint(bravoAdmin, initialTokenMintAmount) + await destWEBB.mint(bravoAdmin, initialTokenMintAmount) + await originWEBB.delegate(bravoAdmin); + await destWEBB.delegate(bravoAdmin); + // instantiate bridges on both sides + await Promise.all([ + BridgeContract.new(originChainID, [relayer1Address], relayerThreshold, 0, 100).then(instance => OriginBridgeInstance = instance), + BridgeContract.new(destChainID, [relayer1Address], relayerThreshold, 0, 100).then(instance => DestBridgeInstance = instance), + // create hasher, verifier, and tokens + Hasher.new().then(instance => hasher = instance), + Verifier.new().then(instance => verifier = instance) + ]); + // initialize anchors on both chains + OriginChainLinkableAnchorInstance = await LinkableAnchorContract.new( + verifier.address, + hasher.address, + tokenDenomination, + merkleTreeHeight, + originChainID, + originWrapperToken.address, + {from: sender}); + DestChainLinkableAnchorInstance = await LinkableAnchorContract.new( + verifier.address, + hasher.address, + tokenDenomination, + merkleTreeHeight, + destChainID, + destWrapperToken.address, + {from: sender}); + // set Minting permissions for anchors + MINTER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes('MINTER_ROLE')); + await originWrapperToken.grantRole(MINTER_ROLE, OriginChainLinkableAnchorInstance.address, {from: sender}); + await destWrapperToken.grantRole(MINTER_ROLE, DestChainLinkableAnchorInstance.address, {from: sender}); + // create resource ID using anchor address + resourceID = helpers.createResourceID(OriginChainLinkableAnchorInstance.address, 0); + initialResourceIDs = [resourceID]; + originInitialContractAddresses = [DestChainLinkableAnchorInstance.address]; + destInitialContractAddresses = [OriginChainLinkableAnchorInstance.address]; + // initialize anchorHanders + await Promise.all([ + AnchorHandlerContract.new(OriginBridgeInstance.address, initialResourceIDs, originInitialContractAddresses) + .then(instance => OriginAnchorHandlerInstance = instance), + AnchorHandlerContract.new(DestBridgeInstance.address, initialResourceIDs, destInitialContractAddresses) + .then(instance => DestAnchorHandlerInstance = instance), + ]); + // increase allowance and set resources for bridge + await Promise.all([ + OriginBridgeInstance.adminSetResource(OriginAnchorHandlerInstance.address, resourceID, OriginChainLinkableAnchorInstance.address), + DestBridgeInstance.adminSetResource(DestAnchorHandlerInstance.address, resourceID, DestChainLinkableAnchorInstance.address) + ]); + // set bridge and handler permissions for anchors + await Promise.all([ + OriginChainLinkableAnchorInstance.setHandler(OriginAnchorHandlerInstance.address, {from: sender}), + OriginChainLinkableAnchorInstance.setBridge(OriginBridgeInstance.address, {from: sender}), + DestChainLinkableAnchorInstance.setHandler(DestAnchorHandlerInstance.address, {from: sender}), + DestChainLinkableAnchorInstance.setBridge(DestBridgeInstance.address, {from: sender}) + ]); + + createWitness = async (data) => { + const wtns = {type: 'mem'}; + await snarkjs.wtns.calculate(data, path.join( + 'test', + 'fixtures', + 'poseidon_bridge_2.wasm' + ), wtns); + return wtns; + } + + tree = new MerkleTree(merkleTreeHeight, null, prefix) + zkey_final = fs.readFileSync('test/fixtures/circuit_final.zkey').buffer; + }); + + it('[sanity] bridges configured with threshold and relayers', async () => { + assert.equal(await OriginBridgeInstance._chainID(), originChainID); + assert.equal(await OriginBridgeInstance._relayerThreshold(), relayerThreshold) + assert.equal((await OriginBridgeInstance._totalRelayers()).toString(), '1') + assert.equal(await DestBridgeInstance._chainID(), destChainID) + assert.equal(await DestBridgeInstance._relayerThreshold(), relayerThreshold) + assert.equal((await DestBridgeInstance._totalRelayers()).toString(), '1') + }) + + it('Wrapping should fail if Anchor does not have MINTER_ROLE', async () => { + //originToken is voted to be approved for token wrapper on origin chain + await helpers.addTokenToWrapper(originGov, originWrapperToken, originToken, bravoAdmin, states); + // revoke anchor permissions + await originWrapperToken.revokeRole(MINTER_ROLE, OriginChainLinkableAnchorInstance.address, {from: sender}); + await originToken.mint(user1, initialTokenMintAmount); + await originToken.approve(originWrapperToken.address, initialTokenMintAmount, {from: user1}); + await TruffleAssert.reverts(OriginChainLinkableAnchorInstance.wrap(originToken.address, tokenDenomination, {from: user1}), + 'ERC20PresetMinterPauser: must have minter role'); + }) + + it('Anchor fails to mint on withdraw without MINTER_ROLE', async () => { + //originToken is voted to be approved for token wrapper on origin chain + await helpers.addTokenToWrapper(originGov, originWrapperToken, originToken, bravoAdmin, states); + /* + * User1 wraps originToken for originWrapperToken + */ + await originToken.mint(user1, initialTokenMintAmount); + await originToken.approve(originWrapperToken.address, initialTokenMintAmount, {from: user1}); + await originWrapperToken.approve(OriginChainLinkableAnchorInstance.address, initialTokenMintAmount, {from: user1}); + await OriginChainLinkableAnchorInstance.wrap(originToken.address, tokenDenomination, {from: user1}); + /* + * User1 deposits on origin chain + */ + // generate deposit commitment targeting withdrawal on destination chain + originDeposit = helpers.generateDeposit(destChainID); + // deposit on origin chain and define nonce + let { logs } = await OriginChainLinkableAnchorInstance.deposit(helpers.toFixedHex(originDeposit.commitment), {from: user1}); + originUpdateNonce = logs[0].args.leafIndex; + originMerkleRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); + // create correct update proposal data for the deposit on origin chain + originDepositData = helpers.createUpdateProposalData(originChainID, originBlockHeight, originMerkleRoot); + originDepositDataHash = Ethers.utils.keccak256(DestAnchorHandlerInstance.address + originDepositData.substr(2)); + /* + * Relayers vote on dest chain + */ + // deposit on origin chain leads to update proposal on dest chain + // relayer1 creates the deposit proposal for the deposit + await TruffleAssert.passes(DestBridgeInstance.voteProposal( + originChainID, + originUpdateNonce, + resourceID, + originDepositDataHash, + { from: relayer1Address } + )); + // relayer1 will execute the deposit proposal + await TruffleAssert.passes(DestBridgeInstance.executeProposal( + originChainID, + originUpdateNonce, + originDepositData, + resourceID, + { from: relayer1Address } + )); + + /* + * User1 generates proof + */ + const destNeighborRoots = await DestChainLinkableAnchorInstance.getLatestNeighborRoots(); + await tree.insert(originDeposit.commitment); + + let { root, path_elements, path_index } = await tree.path(0); + const destNativeRoot = await DestChainLinkableAnchorInstance.getLastRoot(); + let input = { + // public + nullifierHash: originDeposit.nullifierHash, + recipient: user1, + relayer: operator, + fee, + refund, + chainID: originDeposit.chainID, + roots: [destNativeRoot, ...destNeighborRoots], + // private + nullifier: originDeposit.nullifier, + secret: originDeposit.secret, + pathElements: path_elements, + pathIndices: path_index, + diffs: [destNativeRoot, ...destNeighborRoots].map(r => { + return F.sub( + Scalar.fromString(`${r}`), + Scalar.fromString(`${destNeighborRoots[0]}`), + ).toString(); + }), + }; + let wtns = await createWitness(input); + let res = await snarkjs.groth16.prove('test/fixtures/circuit_final.zkey', wtns); + proof = res.proof; + publicSignals = res.publicSignals; + let vKey = await snarkjs.zKey.exportVerificationKey('test/fixtures/circuit_final.zkey'); + res = await snarkjs.groth16.verify(vKey, publicSignals, proof); + assert.strictEqual(res, true); + let args = [ + helpers.createRootsBytes(input.roots), + helpers.toFixedHex(input.nullifierHash), + helpers.toFixedHex(input.recipient, 20), + helpers.toFixedHex(input.relayer, 20), + helpers.toFixedHex(input.fee), + helpers.toFixedHex(input.refund), + ]; + let proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); + // revoke anchor permissions + await destWrapperToken.revokeRole(MINTER_ROLE, DestChainLinkableAnchorInstance.address, {from: sender}); + // user1 withdraw on dest chain + await TruffleAssert.reverts(DestChainLinkableAnchorInstance.withdraw + (`0x${proofEncoded}`, ...args, { from: input.relayer, gasPrice: '0' }), 'ERC20PresetMinterPauser: must have minter role'); + }) + + it('cross chain deposits and withdrawals should work', async () => { + //originToken is voted to be approved for token wrapper on origin chain + await helpers.addTokenToWrapper(originGov, originWrapperToken, originToken, bravoAdmin, states); + /* + * User1 wraps originToken for originWrapperToken + */ + await originToken.mint(user1, initialTokenMintAmount); + // approve wrapper to transfer user1's originTokens + await originToken.approve(originWrapperToken.address, initialTokenMintAmount, {from: user1}); + // approve anchor for deposit + await originWrapperToken.approve(OriginChainLinkableAnchorInstance.address, initialTokenMintAmount, {from: user1}); + await OriginChainLinkableAnchorInstance.wrap(originToken.address, tokenDenomination, {from: user1}); + /* + * User1 deposits on origin chain + */ + // generate deposit commitment targeting withdrawal on destination chain + originDeposit = helpers.generateDeposit(destChainID); + // deposit on origin chain and define nonce + let { logs } = await OriginChainLinkableAnchorInstance.deposit(helpers.toFixedHex(originDeposit.commitment), {from: user1}); + originUpdateNonce = logs[0].args.leafIndex; + originMerkleRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); + // create correct update proposal data for the deposit on origin chain + originDepositData = helpers.createUpdateProposalData(originChainID, originBlockHeight, originMerkleRoot); + originDepositDataHash = Ethers.utils.keccak256(DestAnchorHandlerInstance.address + originDepositData.substr(2)); + /* + * Relayers vote on dest chain + */ + // deposit on origin chain leads to update proposal on dest chain + // relayer1 creates the deposit proposal for the deposit + await TruffleAssert.passes(DestBridgeInstance.voteProposal( + originChainID, + originUpdateNonce, + resourceID, + originDepositDataHash, + { from: relayer1Address } + )); + // relayer1 will execute the deposit proposal + await TruffleAssert.passes(DestBridgeInstance.executeProposal( + originChainID, + originUpdateNonce, + originDepositData, + resourceID, + { from: relayer1Address } + )); + // check initial balances + let balanceOperatorBefore = await destWrapperToken.balanceOf(operator); + let balanceReceiverBefore = await destWrapperToken.balanceOf(user1); + // get roots for proof + const destNeighborRoots = await DestChainLinkableAnchorInstance.getLatestNeighborRoots(); + /* + * User1 generates proof + */ + await tree.insert(originDeposit.commitment); + + let { root, path_elements, path_index } = await tree.path(0); + const destNativeRoot = await DestChainLinkableAnchorInstance.getLastRoot(); + let input = { + // public + nullifierHash: originDeposit.nullifierHash, + recipient: user1, + relayer: operator, + fee, + refund, + chainID: originDeposit.chainID, + roots: [destNativeRoot, ...destNeighborRoots], + // private + nullifier: originDeposit.nullifier, + secret: originDeposit.secret, + pathElements: path_elements, + pathIndices: path_index, + diffs: [destNativeRoot, ...destNeighborRoots].map(r => { + return F.sub( + Scalar.fromString(`${r}`), + Scalar.fromString(`${destNeighborRoots[0]}`), + ).toString(); + }), + }; + + let wtns = await createWitness(input); + + let res = await snarkjs.groth16.prove('test/fixtures/circuit_final.zkey', wtns); + proof = res.proof; + publicSignals = res.publicSignals; + let vKey = await snarkjs.zKey.exportVerificationKey('test/fixtures/circuit_final.zkey'); + res = await snarkjs.groth16.verify(vKey, publicSignals, proof); + assert.strictEqual(res, true); + + let args = [ + helpers.createRootsBytes(input.roots), + helpers.toFixedHex(input.nullifierHash), + helpers.toFixedHex(input.recipient, 20), + helpers.toFixedHex(input.relayer, 20), + helpers.toFixedHex(input.fee), + helpers.toFixedHex(input.refund), + ]; + + let proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); + + /* + * user1 withdraw on dest chain + */ + ({ logs } = await DestChainLinkableAnchorInstance.withdraw + (`0x${proofEncoded}`, ...args, { from: input.relayer, gasPrice: '0' })); + + let balanceDestAnchorAfter = await destWrapperToken.balanceOf(DestChainLinkableAnchorInstance.address); + let balanceOperatorAfter = await destWrapperToken.balanceOf(input.relayer); + let balanceUser1AfterUnwrap = await destWrapperToken.balanceOf(user1); + const feeBN = toBN(fee.toString()) + assert.strictEqual(balanceDestAnchorAfter.toString(), toBN(0).toString()); + assert.strictEqual(balanceOperatorAfter.toString(), balanceOperatorBefore.add(feeBN).toString()); + assert.strictEqual(balanceUser1AfterUnwrap.toString(), balanceReceiverBefore.add(toBN(tokenDenomination)).sub(feeBN).toString()); + assert.strictEqual((await destWrapperToken.totalSupply()).toString(), tokenDenomination.toString()); + isSpent = await DestChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); + assert(isSpent); + + //originToken is voted to be approved for token wrapper on origin chain + await helpers.addTokenToWrapper(destGov, destWrapperToken, destToken, bravoAdmin, states); + /* + * user2 wraps destToken for destWrapperToken + */ + await destToken.mint(user2, initialTokenMintAmount); + // approve tokenwrapper to transfer destTokens from user2 + await destToken.approve(destWrapperToken.address, initialTokenMintAmount, {from: user2}); + // increase allowance for user1 to burn + await destWrapperToken.approve(DestChainLinkableAnchorInstance.address, initialTokenMintAmount, {from: user1}); + // increase allowance for user2 to deposit on destAnchor + await destWrapperToken.approve(DestChainLinkableAnchorInstance.address, initialTokenMintAmount, {from: user2}); + await DestChainLinkableAnchorInstance.wrap(destToken.address, tokenDenomination, {from: user2}); + /* + * User1 unwraps destWrapperToken for destToken with new liquidity + */ + await DestChainLinkableAnchorInstance.unwrap(destToken.address, balanceUser1AfterUnwrap, {from: user1}); + let balanceUser1AfterUnwrapUnwrap = await destToken.balanceOf(user1); + assert.strictEqual(balanceUser1AfterUnwrapUnwrap.toString(), balanceUser1AfterUnwrap.toString()); + assert.strictEqual((await destWrapperToken.totalSupply()).toString(), toBN(tokenDenomination).add(feeBN).toString()); + /* + * user2 deposit on dest chain + */ + // generate deposit commitment + destDeposit = helpers.generateDeposit(originChainID); + // deposit on dest chain and define nonce + ({logs} = await DestChainLinkableAnchorInstance.deposit(helpers.toFixedHex(destDeposit.commitment), {from: user2})); + destUpdateNonce = logs[0].args.leafIndex; + destMerkleRoot = await DestChainLinkableAnchorInstance.getLastRoot(); + // create correct update proposal data for the deposit on dest chain + destDepositData = helpers.createUpdateProposalData(destChainID, destBlockHeight, destMerkleRoot); + destDepositDataHash = Ethers.utils.keccak256(OriginAnchorHandlerInstance.address + destDepositData.substr(2)); + /* + * relayers vote on origin chain + */ + // deposit on dest chain leads to update proposal on origin chain + // relayer1 creates the deposit proposal + await TruffleAssert.passes(OriginBridgeInstance.voteProposal( + destChainID, + destUpdateNonce, + resourceID, + destDepositDataHash, + { from: relayer1Address } + )); + // relayer1 will execute the update proposal + await TruffleAssert.passes(OriginBridgeInstance.executeProposal( + destChainID, + destUpdateNonce, + destDepositData, + resourceID, + { from: relayer1Address } + )); + // check initial balances + balanceOperatorBefore = await originWrapperToken.balanceOf(operator); + balanceReceiverBefore = await originWrapperToken.balanceOf(user2); + // get roots for proof + const originNeighborRoots = await OriginChainLinkableAnchorInstance.getLatestNeighborRoots(); + /* + * user2 generates proof + */ + tree = new MerkleTree(merkleTreeHeight, null, prefix) + await tree.insert(destDeposit.commitment); + + ({ root, path_elements, path_index } = await tree.path(0)); + const originNativeRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); + input = { + // public + nullifierHash: destDeposit.nullifierHash, + recipient: user2, + relayer: operator, + fee, + refund, + chainID: destDeposit.chainID, + roots: [originNativeRoot, ...originNeighborRoots], + // private + nullifier: destDeposit.nullifier, + secret: destDeposit.secret, + pathElements: path_elements, + pathIndices: path_index, + diffs: [originNativeRoot, originNeighborRoots[0]].map(r => { + return F.sub( + Scalar.fromString(`${r}`), + Scalar.fromString(`${originNeighborRoots[0]}`), + ).toString(); + }), + }; + + wtns = await createWitness(input); + + res = await snarkjs.groth16.prove('test/fixtures/circuit_final.zkey', wtns); + proof = res.proof; + publicSignals = res.publicSignals; + vKey = await snarkjs.zKey.exportVerificationKey('test/fixtures/circuit_final.zkey'); + res = await snarkjs.groth16.verify(vKey, publicSignals, proof); + assert.strictEqual(res, true); + + isSpent = await DestChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); + assert.strictEqual(isSpent, false); + + args = [ + helpers.createRootsBytes(input.roots), + helpers.toFixedHex(input.nullifierHash), + helpers.toFixedHex(input.recipient, 20), + helpers.toFixedHex(input.relayer, 20), + helpers.toFixedHex(input.fee), + helpers.toFixedHex(input.refund), + ]; + proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); + /* + * user1 withdraw on origin chain + */ + let balanceOriginAnchorAfterDeposit = await originWrapperToken.balanceOf(OriginChainLinkableAnchorInstance.address); + ({ logs } = await OriginChainLinkableAnchorInstance.withdraw + (`0x${proofEncoded}`, ...args, { from: input.relayer, gasPrice: '0' })); + + let balanceOriginAnchorAfter = await originWrapperToken.balanceOf(OriginChainLinkableAnchorInstance.address); + balanceOperatorAfter = await originWrapperToken.balanceOf(input.relayer); + let balanceUser2AfterWithdraw = await originWrapperToken.balanceOf(user2); + + assert.strictEqual(balanceOriginAnchorAfter.toString(), toBN(0).toString()); + assert.strictEqual(balanceOperatorAfter.toString(), balanceOperatorBefore.add(feeBN).toString()); + assert.strictEqual(balanceUser2AfterWithdraw.toString(), balanceReceiverBefore.add(toBN(tokenDenomination)).sub(feeBN).toString()); + + isSpent = await OriginChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); + assert(isSpent); + /* + * User2 unwraps originWrapperToken for originToken + */ + //increase allowance for burn + await originWrapperToken.approve(OriginChainLinkableAnchorInstance.address, initialTokenMintAmount, {from: user2}); + await OriginChainLinkableAnchorInstance.unwrap(originToken.address, balanceUser2AfterWithdraw, {from: user2}); + let balanceUser2AfterUnwrap = await originToken.balanceOf(user2); + assert.strictEqual(balanceUser2AfterUnwrap.toString(), balanceUser2AfterUnwrap.toString()); + assert.strictEqual((await originWrapperToken.totalSupply()).toString(), feeBN.toString()); + }) +}) + diff --git a/test/integration/historicalRootWithdraw.js b/test/integration/historicalRootWithdraw.js index f93cab5ad..7ea79898a 100644 --- a/test/integration/historicalRootWithdraw.js +++ b/test/integration/historicalRootWithdraw.js @@ -1,14 +1,14 @@ const TruffleAssert = require('truffle-assertions'); const Ethers = require('ethers'); -const Helpers = require('../helpers'); +const helpers = require('../helpers'); const { toBN } = require('web3-utils') const assert = require('assert'); -const BridgeContract = artifacts.require("Bridge"); -const LinkableAnchorContract = artifacts.require("./LinkableERC20AnchorPoseidon2.sol"); +const BridgeContract = artifacts.require('Bridge'); +const LinkableAnchorContract = artifacts.require('./LinkableERC20AnchorPoseidon2.sol'); const Verifier = artifacts.require('./VerifierPoseidonBridge.sol'); -const Hasher = artifacts.require("PoseidonT3"); -const Token = artifacts.require("ERC20Mock"); -const AnchorHandlerContract = artifacts.require("AnchorHandler"); +const Hasher = artifacts.require('PoseidonT3'); +const Token = artifacts.require('ERC20Mock'); +const AnchorHandlerContract = artifacts.require('AnchorHandler'); const fs = require('fs') const path = require('path'); @@ -17,26 +17,23 @@ let prefix = 'poseidon-test' const snarkjs = require('snarkjs'); const BN = require('bn.js'); const F = require('circomlib').babyJub.F; -const Scalar = require("ffjavascript").Scalar; +const Scalar = require('ffjavascript').Scalar; const MerkleTree = require('../../lib/MerkleTree'); contract('E2E LinkableAnchors - Cross chain withdraw using historical root should work', async accounts => { - const relayerThreshold = 2; + const relayerThreshold = 1; const originChainID = 1; const destChainID = 2; const relayer1Address = accounts[3]; - const relayer2Address = accounts[4]; const operator = accounts[6]; - const initialTokenMintAmount = BigInt(1e25); - const maxRoots = 1; + const tokenDenomination = '1000000000000000000000'; const merkleTreeHeight = 30; const sender = accounts[5]; - const fee = BigInt((new BN(`${NATIVE_AMOUNT}`).shrn(1)).toString()) || BigInt((new BN(`${1e17}`)).toString()); const refund = BigInt((new BN('0')).toString()); - const recipient = Helpers.getRandomRecipient(); + const recipient = helpers.getRandomRecipient(); let originMerkleRoot; let originBlockHeight = 1; @@ -44,8 +41,7 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul let hasher, verifier; let originChainToken; let destChainToken; - let originDeposit; - let tokenDenomination = '1000000000000000000000'; + let originDeposit; let tree; let createWitness; let OriginChainLinkableAnchorInstance; @@ -61,7 +57,7 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul beforeEach(async () => { await Promise.all([ // instantiate bridges on dest chain side - BridgeContract.new(destChainID, [relayer1Address, relayer2Address], relayerThreshold, 0, 100).then(instance => DestBridgeInstance = instance), + BridgeContract.new(destChainID, [relayer1Address], relayerThreshold, 0, 100).then(instance => DestBridgeInstance = instance), // create hasher, verifier, and tokens Hasher.new().then(instance => hasher = instance), Verifier.new().then(instance => verifier = instance), @@ -86,7 +82,7 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul destChainToken.address, {from: sender}); // create resource ID using anchor address - resourceID = Helpers.createResourceID(OriginChainLinkableAnchorInstance.address, 0); + resourceID = helpers.createResourceID(OriginChainLinkableAnchorInstance.address, 0); initialResourceIDs = [resourceID]; destInitialContractAddresses = [OriginChainLinkableAnchorInstance.address]; // initialize anchorHanders @@ -103,11 +99,11 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul ]); createWitness = async (data) => { - const wtns = {type: "mem"}; + const wtns = {type: 'mem'}; await snarkjs.wtns.calculate(data, path.join( - "test", - "fixtures", - "poseidon_bridge_2.wasm" + 'test', + 'fixtures', + 'poseidon_bridge_2.wasm' ), wtns); return wtns; } @@ -119,25 +115,25 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul it('[sanity] dest chain bridge configured with threshold and relayers', async () => { assert.equal(await DestBridgeInstance._chainID(), destChainID) assert.equal(await DestBridgeInstance._relayerThreshold(), relayerThreshold) - assert.equal((await DestBridgeInstance._totalRelayers()).toString(), '2') + assert.equal((await DestBridgeInstance._totalRelayers()).toString(), '1') }) it('withdrawing across bridge after two deposits should work', async () => { /* - * first deposit on origin chain + * sender deposits on origin chain anchor */ // minting Tokens await originChainToken.mint(sender, initialTokenMintAmount); //increase allowance originChainToken.approve(OriginChainLinkableAnchorInstance.address, initialTokenMintAmount, { from: sender }); // deposit on both chains and define nonces based on events emmited - let firstOriginDeposit = Helpers.generateDeposit(destChainID); + let firstOriginDeposit = helpers.generateDeposit(destChainID); let { logs } = await OriginChainLinkableAnchorInstance.deposit( - Helpers.toFixedHex(firstOriginDeposit.commitment), {from: sender}); + helpers.toFixedHex(firstOriginDeposit.commitment), {from: sender}); originUpdateNonce = logs[0].args.leafIndex; firstWithdrawlMerkleRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); // create correct update proposal data for the deposit on origin chain - originDepositData = Helpers.createUpdateProposalData(originChainID, originBlockHeight, firstWithdrawlMerkleRoot); + originDepositData = helpers.createUpdateProposalData(originChainID, originBlockHeight, firstWithdrawlMerkleRoot); originDepositDataHash = Ethers.utils.keccak256(DestAnchorHandlerInstance.address + originDepositData.substr(2)); // deposit on origin chain leads to update addEdge proposal on dest chain @@ -149,17 +145,6 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul originDepositDataHash, { from: relayer1Address } )); - - // relayer2 votes in favor of the update proposal - // because the relayerThreshold is 2, the deposit proposal will become passed - await TruffleAssert.passes(DestBridgeInstance.voteProposal( - originChainID, - originUpdateNonce, - resourceID, - originDepositDataHash, - { from: relayer2Address } - )); - // relayer1 will execute the deposit proposal await TruffleAssert.passes(DestBridgeInstance.executeProposal( originChainID, @@ -168,9 +153,8 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul resourceID, { from: relayer1Address } )); - /* - * generate proof + * sender generate proof */ // insert two commitments into the tree await tree.insert(firstOriginDeposit.commitment); @@ -210,57 +194,29 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul res = await snarkjs.groth16.verify(vKey, publicSignals, proof); assert.strictEqual(res, true); - let isSpent = await DestChainLinkableAnchorInstance.isSpent(Helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(isSpent, false); - // Uncomment to measure gas usage // gas = await anchor.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' }) // console.log('withdraw gas:', gas) let args = [ - Helpers.createRootsBytes(input.roots), - Helpers.toFixedHex(input.nullifierHash), - Helpers.toFixedHex(input.recipient, 20), - Helpers.toFixedHex(input.relayer, 20), - Helpers.toFixedHex(input.fee), - Helpers.toFixedHex(input.refund), + helpers.createRootsBytes(input.roots), + helpers.toFixedHex(input.nullifierHash), + helpers.toFixedHex(input.recipient, 20), + helpers.toFixedHex(input.relayer, 20), + helpers.toFixedHex(input.fee), + helpers.toFixedHex(input.refund), ]; - let result = await Helpers.groth16ExportSolidityCallData(proof, publicSignals); - let fullProof = JSON.parse("[" + result + "]"); - let pi_a = fullProof[0]; - let pi_b = fullProof[1]; - let pi_c = fullProof[2]; - let inputs = fullProof[3]; - assert.strictEqual(true, await verifier.verifyProof( - pi_a, - pi_b, - pi_c, - inputs, - )); - - proofEncoded = [ - pi_a[0], - pi_a[1], - pi_b[0][0], - pi_b[0][1], - pi_b[1][0], - pi_b[1][1], - pi_c[0], - pi_c[1], - ] - .map(elt => elt.substr(2)) - .join(''); - + let proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); /* - * second deposit on origin chain + * sender's second deposit on origin chain anchor */ // deposit on origin chain and define nonce based on events emmited - originDeposit = Helpers.generateDeposit(destChainID, 30); - ({ logs } = await OriginChainLinkableAnchorInstance.deposit(Helpers.toFixedHex(originDeposit.commitment), {from: sender})); + originDeposit = helpers.generateDeposit(destChainID, 30); + ({ logs } = await OriginChainLinkableAnchorInstance.deposit(helpers.toFixedHex(originDeposit.commitment), {from: sender})); originUpdateNonce = logs[0].args.leafIndex; secondWithdrawalMerkleRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); // create correct update proposal data for the deposit on origin chain - originDepositData = Helpers.createUpdateProposalData(originChainID, originBlockHeight + 10, secondWithdrawalMerkleRoot); + originDepositData = helpers.createUpdateProposalData(originChainID, originBlockHeight + 10, secondWithdrawalMerkleRoot); originDepositDataHash = Ethers.utils.keccak256(DestAnchorHandlerInstance.address + originDepositData.substr(2)); /* * Relayers vote on dest chain @@ -274,17 +230,6 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul originDepositDataHash, { from: relayer1Address } )); - - // relayer2 votes in favor of the update proposal - // because the relayerThreshold is 2, the deposit proposal will become passed - await TruffleAssert.passes(DestBridgeInstance.voteProposal( - originChainID, - originUpdateNonce, - resourceID, - originDepositDataHash, - { from: relayer2Address } - )); - // relayer1 will execute the deposit proposal await TruffleAssert.passes(DestBridgeInstance.executeProposal( originChainID, @@ -296,9 +241,9 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul // check initial balances let balanceOperatorBefore = await destChainToken.balanceOf(operator); - let balanceReceiverBefore = await destChainToken.balanceOf(Helpers.toFixedHex(recipient, 20)); + let balanceReceiverBefore = await destChainToken.balanceOf(helpers.toFixedHex(recipient, 20)); /* - * withdraw + * sender withdraws using first commitment */ // mint to anchor and track balance await destChainToken.mint(DestChainLinkableAnchorInstance.address, initialTokenMintAmount); @@ -306,21 +251,15 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul // withdraw ({ logs } = await DestChainLinkableAnchorInstance.withdraw (`0x${proofEncoded}`, ...args, { from: input.relayer, gasPrice: '0' })); - + let balanceDestAnchorAfter = await destChainToken.balanceOf(DestChainLinkableAnchorInstance.address); let balanceOperatorAfter = await destChainToken.balanceOf(input.relayer); - let balanceReceiverAfter = await destChainToken.balanceOf(Helpers.toFixedHex(recipient, 20)); + let balanceReceiverAfter = await destChainToken.balanceOf(helpers.toFixedHex(recipient, 20)); const feeBN = toBN(fee.toString()) assert.strictEqual(balanceDestAnchorAfter.toString(), balanceDestAnchorAfterDeposits.sub(toBN(tokenDenomination)).toString()); assert.strictEqual(balanceOperatorAfter.toString(), balanceOperatorBefore.add(feeBN).toString()); assert.strictEqual(balanceReceiverAfter.toString(), balanceReceiverBefore.add(toBN(tokenDenomination)).sub(feeBN).toString()); - - assert.strictEqual(logs[0].event, 'Withdrawal'); - assert.strictEqual(logs[0].args.nullifierHash, Helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(logs[0].args.relayer, operator); - assert.strictEqual(logs[0].args.fee.toString(), feeBN.toString()); - - isSpent = await DestChainLinkableAnchorInstance.isSpent(Helpers.toFixedHex(input.nullifierHash)); + isSpent = await DestChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); assert(isSpent); /* @@ -361,56 +300,28 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul res = await snarkjs.groth16.verify(vKey, publicSignals, proof); assert.strictEqual(res, true); - isSpent = await DestChainLinkableAnchorInstance.isSpent(Helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(isSpent, false); - args = [ - Helpers.createRootsBytes(input.roots), - Helpers.toFixedHex(input.nullifierHash), - Helpers.toFixedHex(input.recipient, 20), - Helpers.toFixedHex(input.relayer, 20), - Helpers.toFixedHex(input.fee), - Helpers.toFixedHex(input.refund), + helpers.createRootsBytes(input.roots), + helpers.toFixedHex(input.nullifierHash), + helpers.toFixedHex(input.recipient, 20), + helpers.toFixedHex(input.relayer, 20), + helpers.toFixedHex(input.fee), + helpers.toFixedHex(input.refund), ]; - result = await Helpers.groth16ExportSolidityCallData(proof, publicSignals); - fullProof = JSON.parse("[" + result + "]"); - pi_a = fullProof[0]; - pi_b = fullProof[1]; - pi_c = fullProof[2]; - inputs = fullProof[3]; - assert.strictEqual(true, await verifier.verifyProof( - pi_a, - pi_b, - pi_c, - inputs, - )); - - proofEncoded = [ - pi_a[0], - pi_a[1], - pi_b[0][0], - pi_b[0][1], - pi_b[1][0], - pi_b[1][1], - pi_c[0], - pi_c[1], - ] - .map(elt => elt.substr(2)) - .join(''); - + proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); /* * create 30 new deposits on chain so history wraps around and forgets second deposit */ let newBlockHeight = originBlockHeight + 100; for (var i = 0; i < 30; i++) { // deposit on origin chain and define nonce based on events emmited - originDeposit = Helpers.generateDeposit(destChainID, i); - ({ logs } = await OriginChainLinkableAnchorInstance.deposit(Helpers.toFixedHex(originDeposit.commitment), {from: sender})); + originDeposit = helpers.generateDeposit(destChainID, i); + ({ logs } = await OriginChainLinkableAnchorInstance.deposit(helpers.toFixedHex(originDeposit.commitment), {from: sender})); originUpdateNonce = logs[0].args.leafIndex; originMerkleRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); // create correct update proposal data for the deposit on origin chain - originDepositData = Helpers.createUpdateProposalData(originChainID, newBlockHeight + i, originMerkleRoot); + originDepositData = helpers.createUpdateProposalData(originChainID, newBlockHeight + i, originMerkleRoot); originDepositDataHash = Ethers.utils.keccak256(DestAnchorHandlerInstance.address + originDepositData.substr(2)); /* * Relayers vote on dest chain @@ -423,17 +334,6 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul originDepositDataHash, { from: relayer1Address } )); - - // relayer2 votes in favor of the update proposal - // because the relayerThreshold is 2, the deposit proposal will become passed - await TruffleAssert.passes(DestBridgeInstance.voteProposal( - originChainID, - originUpdateNonce, - resourceID, - originDepositDataHash, - { from: relayer2Address } - )); - // relayer1 will execute the deposit proposal await TruffleAssert.passes(DestBridgeInstance.executeProposal( originChainID, @@ -447,6 +347,6 @@ contract('E2E LinkableAnchors - Cross chain withdraw using historical root shoul // withdraw should revert as historical root does not exist await TruffleAssert.reverts(DestChainLinkableAnchorInstance.withdraw (`0x${proofEncoded}`, ...args, { from: input.relayer, gasPrice: '0' }), - "Neighbor root not found"); + 'Neighbor root not found'); }).timeout(0); }) diff --git a/test/integration/simpleWithdrawals.js b/test/integration/simpleWithdrawals.js index 503af2248..9a487cbb1 100644 --- a/test/integration/simpleWithdrawals.js +++ b/test/integration/simpleWithdrawals.js @@ -3,12 +3,12 @@ const Ethers = require('ethers'); const helpers = require('../helpers'); const { toBN } = require('web3-utils') const assert = require('assert'); -const BridgeContract = artifacts.require("Bridge"); -const LinkableAnchorContract = artifacts.require("./LinkableERC20AnchorPoseidon2.sol"); +const BridgeContract = artifacts.require('Bridge'); +const LinkableAnchorContract = artifacts.require('./LinkableERC20AnchorPoseidon2.sol'); const Verifier = artifacts.require('./VerifierPoseidonBridge.sol'); -const Hasher = artifacts.require("PoseidonT3"); -const Token = artifacts.require("ERC20Mock"); -const AnchorHandlerContract = artifacts.require("AnchorHandler"); +const Hasher = artifacts.require('PoseidonT3'); +const Token = artifacts.require('ERC20Mock'); +const AnchorHandlerContract = artifacts.require('AnchorHandler'); const fs = require('fs') const path = require('path'); @@ -17,23 +17,21 @@ let prefix = 'poseidon-test' const snarkjs = require('snarkjs'); const BN = require('bn.js'); const F = require('circomlib').babyJub.F; -const Scalar = require("ffjavascript").Scalar; +const Scalar = require('ffjavascript').Scalar; const MerkleTree = require('../../lib/MerkleTree'); contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { - const relayerThreshold = 2; + const relayerThreshold = 1; const originChainID = 1; const destChainID = 2; const relayer1Address = accounts[3]; - const relayer2Address = accounts[4]; const operator = accounts[6]; - const initialTokenMintAmount = BigInt(1e25); + const tokenDenomination = '1000000000000000000000'; const maxRoots = 1; const merkleTreeHeight = 30; const sender = accounts[5]; - const fee = BigInt((new BN(`${NATIVE_AMOUNT}`).shrn(1)).toString()) || BigInt((new BN(`${1e17}`)).toString()); const refund = BigInt((new BN('0')).toString()); const recipient = helpers.getRandomRecipient(); @@ -49,7 +47,6 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { let destChainToken; let originDeposit; let destDeposit; - let tokenDenomination = '1000000000000000000000'; let tree; let createWitness; let OriginBridgeInstance; @@ -70,8 +67,8 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { beforeEach(async () => { await Promise.all([ // instantiate bridges on both sides - BridgeContract.new(originChainID, [relayer1Address, relayer2Address], relayerThreshold, 0, 100).then(instance => OriginBridgeInstance = instance), - BridgeContract.new(destChainID, [relayer1Address, relayer2Address], relayerThreshold, 0, 100).then(instance => DestBridgeInstance = instance), + BridgeContract.new(originChainID, [relayer1Address], relayerThreshold, 0, 100).then(instance => OriginBridgeInstance = instance), + BridgeContract.new(destChainID, [relayer1Address], relayerThreshold, 0, 100).then(instance => DestBridgeInstance = instance), // create hasher, verifier, and tokens Hasher.new().then(instance => hasher = instance), Verifier.new().then(instance => verifier = instance), @@ -121,11 +118,11 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { ]); createWitness = async (data) => { - const wtns = {type: "mem"}; + const wtns = {type: 'mem'}; await snarkjs.wtns.calculate(data, path.join( - "test", - "fixtures", - "poseidon_bridge_2.wasm" + 'test', + 'fixtures', + 'poseidon_bridge_2.wasm' ), wtns); return wtns; } @@ -137,15 +134,15 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { it('[sanity] bridges configured with threshold and relayers', async () => { assert.equal(await OriginBridgeInstance._chainID(), originChainID); assert.equal(await OriginBridgeInstance._relayerThreshold(), relayerThreshold) - assert.equal((await OriginBridgeInstance._totalRelayers()).toString(), '2') + assert.equal((await OriginBridgeInstance._totalRelayers()).toString(), '1') assert.equal(await DestBridgeInstance._chainID(), destChainID) assert.equal(await DestBridgeInstance._relayerThreshold(), relayerThreshold) - assert.equal((await DestBridgeInstance._totalRelayers()).toString(), '2') + assert.equal((await DestBridgeInstance._totalRelayers()).toString(), '1') }) it('withdrawals on both chains integration', async () => { /* - * User deposits on origin chain + * sender deposits on origin chain */ // minting Tokens await originChainToken.mint(sender, initialTokenMintAmount); @@ -172,17 +169,6 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { originDepositDataHash, { from: relayer1Address } )); - - // relayer2 votes in favor of the update proposal - // because the relayerThreshold is 2, the deposit proposal will become passed - await TruffleAssert.passes(DestBridgeInstance.voteProposal( - originChainID, - originUpdateNonce, - resourceID, - originDepositDataHash, - { from: relayer2Address } - )); - // relayer1 will execute the deposit proposal await TruffleAssert.passes(DestBridgeInstance.executeProposal( originChainID, @@ -191,18 +177,16 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { resourceID, { from: relayer1Address } )); - - const destNeighborRoots = await DestChainLinkableAnchorInstance.getLatestNeighborRoots(); - assert.strictEqual(destNeighborRoots.length, maxRoots); - assert.strictEqual(destNeighborRoots[0], originMerkleRoot); + // check initial balances let balanceOperatorBefore = await destChainToken.balanceOf(operator); let balanceReceiverBefore = await destChainToken.balanceOf(helpers.toFixedHex(recipient, 20)); /* - * User generates proof + * sender generates proof */ + const destNeighborRoots = await DestChainLinkableAnchorInstance.getLatestNeighborRoots(); await tree.insert(originDeposit.commitment); - + let { root, path_elements, path_index } = await tree.path(0); const destNativeRoot = await DestChainLinkableAnchorInstance.getLastRoot(); let input = { @@ -235,10 +219,6 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { let vKey = await snarkjs.zKey.exportVerificationKey('test/fixtures/circuit_final.zkey'); res = await snarkjs.groth16.verify(vKey, publicSignals, proof); assert.strictEqual(res, true); - - let isSpent = await DestChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(isSpent, false); - let args = [ helpers.createRootsBytes(input.roots), helpers.toFixedHex(input.nullifierHash), @@ -248,33 +228,9 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { helpers.toFixedHex(input.refund), ]; - let result = await helpers.groth16ExportSolidityCallData(proof, publicSignals); - let fullProof = JSON.parse("[" + result + "]"); - let pi_a = fullProof[0]; - let pi_b = fullProof[1]; - let pi_c = fullProof[2]; - let inputs = fullProof[3]; - assert.strictEqual(true, await verifier.verifyProof( - pi_a, - pi_b, - pi_c, - inputs, - )); - - proofEncoded = [ - pi_a[0], - pi_a[1], - pi_b[0][0], - pi_b[0][1], - pi_b[1][0], - pi_b[1][1], - pi_c[0], - pi_c[1], - ] - .map(elt => elt.substr(2)) - .join(''); + let proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); /* - * withdraw on dest chain + * sender withdraws on dest chain */ await destChainToken.mint(DestChainLinkableAnchorInstance.address, initialTokenMintAmount); let balanceDestAnchorAfterDeposit = await destChainToken.balanceOf(DestChainLinkableAnchorInstance.address); @@ -288,15 +244,11 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { assert.strictEqual(balanceDestAnchorAfter.toString(), balanceDestAnchorAfterDeposit.sub(toBN(tokenDenomination)).toString()); assert.strictEqual(balanceOperatorAfter.toString(), balanceOperatorBefore.add(feeBN).toString()); assert.strictEqual(balanceReceiverAfter.toString(), balanceReceiverBefore.add(toBN(tokenDenomination)).sub(feeBN).toString()); - - assert.strictEqual(logs[0].event, 'Withdrawal'); - assert.strictEqual(logs[0].args.nullifierHash, helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(logs[0].args.relayer, operator); - assert.strictEqual(logs[0].args.fee.toString(), feeBN.toString()); + isSpent = await DestChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); assert(isSpent); /* - * deposit on dest chain + * sender deposit on dest chain */ // minting Tokens await destChainToken.mint(sender, initialTokenMintAmount); @@ -323,17 +275,6 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { destDepositDataHash, { from: relayer1Address } )); - - // relayer2 votes in favor of the update proposal - // because the relayerThreshold is 2, the update proposal will become passed - await TruffleAssert.passes(OriginBridgeInstance.voteProposal( - destChainID, - destUpdateNonce, - resourceID, - destDepositDataHash, - { from: relayer2Address } - )); - // relayer1 will execute the update proposal await TruffleAssert.passes(OriginBridgeInstance.executeProposal( destChainID, @@ -349,11 +290,11 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { balanceOperatorBefore = await originChainToken.balanceOf(operator); balanceReceiverBefore = await originChainToken.balanceOf(helpers.toFixedHex(recipient, 20)); /* - * generate proof + * sender generates proof */ tree = new MerkleTree(merkleTreeHeight, null, prefix) await tree.insert(destDeposit.commitment); - + ({ root, path_elements, path_index } = await tree.path(0)); const originNativeRoot = await OriginChainLinkableAnchorInstance.getLastRoot(); input = { @@ -386,10 +327,6 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { vKey = await snarkjs.zKey.exportVerificationKey('test/fixtures/circuit_final.zkey'); res = await snarkjs.groth16.verify(vKey, publicSignals, proof); assert.strictEqual(res, true); - - isSpent = await DestChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(isSpent, false); - args = [ helpers.createRootsBytes(input.roots), helpers.toFixedHex(input.nullifierHash), @@ -399,33 +336,9 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { helpers.toFixedHex(input.refund), ]; - result = await helpers.groth16ExportSolidityCallData(proof, publicSignals); - fullProof = JSON.parse("[" + result + "]"); - pi_a = fullProof[0]; - pi_b = fullProof[1]; - pi_c = fullProof[2]; - inputs = fullProof[3]; - assert.strictEqual(true, await verifier.verifyProof( - pi_a, - pi_b, - pi_c, - inputs, - )); - - proofEncoded = [ - pi_a[0], - pi_a[1], - pi_b[0][0], - pi_b[0][1], - pi_b[1][0], - pi_b[1][1], - pi_c[0], - pi_c[1], - ] - .map(elt => elt.substr(2)) - .join(''); + proofEncoded = await helpers.generateWithdrawProofCallData(proof, publicSignals); /* - * withdraw on origin chain + * sender withdraws on origin chain */ await originChainToken.mint(OriginChainLinkableAnchorInstance.address, initialTokenMintAmount); let balanceOriginAnchorAfterDeposit = await originChainToken.balanceOf(OriginChainLinkableAnchorInstance.address); @@ -439,11 +352,6 @@ contract('E2E LinkableAnchors - Cross chain withdrawals', async accounts => { assert.strictEqual(balanceOriginAnchorAfter.toString(), balanceOriginAnchorAfterDeposit.sub(toBN(tokenDenomination)).toString()); assert.strictEqual(balanceOperatorAfter.toString(), balanceOperatorBefore.add(feeBN).toString()); assert.strictEqual(balanceReceiverAfter.toString(), balanceReceiverBefore.add(toBN(tokenDenomination)).sub(feeBN).toString()); - - assert.strictEqual(logs[0].event, 'Withdrawal'); - assert.strictEqual(logs[0].args.nullifierHash, helpers.toFixedHex(input.nullifierHash)); - assert.strictEqual(logs[0].args.relayer, operator); - assert.strictEqual(logs[0].args.fee.toString(), feeBN.toString()); isSpent = await OriginChainLinkableAnchorInstance.isSpent(helpers.toFixedHex(input.nullifierHash)); assert(isSpent); diff --git a/test/token/GovernedTokenWrapper.test.js b/test/token/GovernedTokenWrapper.test.js index b0990906a..92cee3cc9 100644 --- a/test/token/GovernedTokenWrapper.test.js +++ b/test/token/GovernedTokenWrapper.test.js @@ -122,13 +122,13 @@ contract('GovernedTokenWrapper', (accounts) => { await token.mint(root, '10000000000000000000000000'); await token.approve(wrapper.address, '1000000000000000000000000'); await TruffleAssert.reverts( - wrapper.wrap(token.address, '1000000000000000000000000'), + wrapper.wrap(root, token.address, '1000000000000000000000000'), 'Invalid token address', ); await helpers.addTokenToWrapper(gov, wrapper, token, root, states); await TruffleAssert.reverts( - wrapper.wrap(token.address, '1000000000000000000000000'), + wrapper.wrap(root, token.address, '1000000000000000000000000'), 'Invalid token amount', ); }); @@ -140,19 +140,29 @@ contract('GovernedTokenWrapper', (accounts) => { await token.mint(root, '10000000000000000000000000'); await token.approve(wrapper.address, '1000000000000000000000000'); await TruffleAssert.reverts( - wrapper.wrap(token.address, '1000000000000000000000000'), + wrapper.wrap(root, token.address, '1000000000000000000000000'), 'Invalid token address', ); await helpers.addTokenToWrapper(gov, wrapper, token, root, states); - await TruffleAssert.passes(wrapper.wrap(token.address, '1000000000000000000000000')); + await TruffleAssert.passes(wrapper.wrap(root, token.address, '1000000000000000000000000')); await TruffleAssert.reverts( - wrapper.wrap(token.address, '1000000000000000000000000'), + wrapper.wrap(root, token.address, '1000000000000000000000000'), 'Invalid token amount', ); assert.strictEqual((await wrapper.totalSupply()).toString(), '1000000000000000000000000'); }); + it('should fail if wrapper does not have minter role', async () => { + const wrapper = await GovernedTokenWrapper.new(name, symbol, timelock.address, '1000000000000000000000000', {from: accounts[9]}); + const token = await CompToken.new('Token', 'TKN'); + + await token.mint(acct, '10000000000000000000000000'); + await token.approve(wrapper.address, '1000000000000000000000000', {from: acct}); + await helpers.addTokenToWrapper(gov, wrapper, token, root, states); + await TruffleAssert.reverts(wrapper.wrap(acct, token.address, '1000000000000000000000000', {from: acct}), 'ERC20PresetMinterPauser: must have minter role'); + }); + it('should wrap after increasing limit', async () => { const wrapper = await GovernedTokenWrapper.new(name, symbol, timelock.address, '0'); const token = await CompToken.new('Token', 'TKN'); @@ -160,20 +170,20 @@ contract('GovernedTokenWrapper', (accounts) => { await token.mint(root, '10000000000000000000000000'); await token.approve(wrapper.address, amount); await TruffleAssert.reverts( - wrapper.wrap(token.address, amount), + wrapper.wrap(root, token.address, amount), 'Invalid token address', ); await helpers.addTokenToWrapper(gov, wrapper, token, root, states); await TruffleAssert.reverts( - wrapper.wrap(token.address, amount), + wrapper.wrap(root, token.address, amount), 'Invalid token amount', ); await helpers.increaseWrappingLimit(gov, wrapper, amount, root, states); - await TruffleAssert.passes(wrapper.wrap(token.address, amount)); + await TruffleAssert.passes(wrapper.wrap(root, token.address, amount)); await TruffleAssert.reverts( - wrapper.wrap(token.address, '1000000000000000000000000'), + wrapper.wrap(root, token.address, '1000000000000000000000000'), 'Invalid token amount', ); });