/* eslint-disable prettier/prettier */ /* global artifacts, web3, contract */ require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should() const fs = require('fs') const { toBN } = require('web3-utils') const Web3Utils = require('web3-utils') const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper') const Storage = artifacts.require('./Storage.sol') const Shifter = artifacts.require('./XFTanon.sol') const BadRecipient = artifacts.require('./BadRecipient.sol') const XFT = artifacts.require('./XFTMock.sol') const XFTOLDMock = artifacts.require('./XFTOLDMock.sol') const Token = artifacts.require('anonUSD') const SwapRouter = artifacts.require('./ISwapRouter.sol') const UniV3Pool = artifacts.require('./IUniswapV3Pool.sol') const ERC20 = artifacts.require('./IERC20.sol') const TokenSwap = artifacts.require('./TokenSwap.sol') const Oracle = artifacts.require('./Oracle.sol') const { ETH_AMOUNT, MERKLE_TREE_HEIGHT, FEE_AMOUNT, tokenStyle, weth9, uniswap } = require('../config.json') const burnerRole = Web3Utils.keccak256("BURNER_ROLE") const minterRole = Web3Utils.keccak256("MINTER_ROLE") const websnarkUtils = require('websnark/src/utils') const buildGroth16 = require('websnark/src/groth16') const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts const snarkjs = require('snarkjs') const bigInt = snarkjs.bigInt const crypto = require('crypto') const subtle = crypto.subtle const circomlib = require('circomlib') const MerkleTree = require('fixed-merkle-tree') const { use } = require('chai') const relayerConfig = require('../../relayer/config.json') const nonEOAConfig = require ('../../noneoa-relayer/config.json') require = require('esm')(module) let allShifters = require('../shifters.ts').shifters let storagePassword = web3.utils.keccak256('nowledgeIsPower'); let storageHash = web3.utils.keccak256(storagePassword); let xcrypt; const cardinalityLaunch = 10 //How many observations to save in a pool, at laumch const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes)) const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0] const toFixedHex = (number, length = 32) => '0x' + bigInt(number) .toString(16) .padStart(length * 2, '0') const getRandomRecipient = () => rbigint(20) const unlockAccount = async (address) => { let provider = web3.currentProvider; return new Promise((res, rej) => { provider.send({ method: 'evm_addAccount', params: [address, ""] }, (d) => { provider.send({ method: 'personal_unlockAccount', params: [address, ""] }, (d) => { res(address); }); }); }); } let encryptNote; function generateDeposit() { let deposit = { secret: rbigint(31), nullifier: rbigint(31), } const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]) deposit.commitment = pedersenHash(preimage) return deposit } advanceTimeAndBlock = async (time) => { await advanceTime(time); await advanceBlock(); return Promise.resolve(web3.eth.getBlock('latest')); } advanceTime = (time) => { return new Promise((resolve, reject) => { web3.currentProvider.send({ jsonrpc: "2.0", method: "evm_increaseTime", params: [time], id: new Date().getTime() }, (err, result) => { if (err) { return reject(err); } return resolve(result); }); }); } advanceBlock = () => { return new Promise((resolve, reject) => { web3.currentProvider.send({ jsonrpc: "2.0", method: "evm_mine", id: new Date().getTime() }, (err, result) => { if (err) { return reject(err); } const newBlockHash = web3.eth.getBlock('latest').hash; return resolve(newBlockHash) }); }); } contract('XFTShifter', (accounts) => { let shifter, shifterBTC, shifterETH let xft let xftOld let token let storage let badRecipient const sender = accounts[0] const operator = accounts[0] const levels = MERKLE_TREE_HEIGHT || 16 let tokenDenomination = tokenStyle.anonUSD.denoms[0] + "000000000000000000" || '1000000000000000000' // 1 ether let uniFee = 3000 let snapshotId let tree const fee = FEE_AMOUNT || '1' /* in aUSD */ const refund = ETH_AMOUNT || '1000000000000000000' // 1 ether let recipient = getRandomRecipient() const relayer = accounts[1] const relayerNonEOA = accounts[2] let groth16 let circuit let proving_key before(async () => { tree = new MerkleTree(levels) USDtree = new MerkleTree(levels) shifter = await Shifter.at(allShifters['anonUSD500'].shifter) // Use a specific shifter shifterETH = await Shifter.at(allShifters['anonETH1'].shifter) shifterBTC = await Shifter.at(allShifters['anonBTC1'].shifter) storage = await Storage.deployed() oracle = await Oracle.deployed() xftOld = await XFTOLDMock.deployed() xcrypt = async (data, password) => { // IV is 0 for the test, but frontend uses the deposit index const iv = (await subtle.digest('SHA-256', (password + (await storage.getDepositsLength(shifter.address, sender, storageHash))))).slice(0, 16); const dataBuffer = new Uint8Array(Buffer.from(data.slice(2), 'hex')); const keyBuffer = new Uint8Array(Buffer.from(password.slice(2), 'hex')); const key = await subtle.importKey('raw', keyBuffer, { name: 'AES-GCM' }, false, ['encrypt']); try { let encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, key, dataBuffer); return "0x" + Buffer.from(encrypted).toString('hex').slice(0, 124); } catch (err) { console.log(err) throw new Error("The data provided couldn't be encrypted or decrypted, please check the inputs"); } } encryptNote = async (deposit, key) => await xcrypt(`0x${deposit.nullifier.toString(16).padStart(62, '0')}${deposit.secret.toString(16).padStart(62, '0')}`, key); xft = await XFT.at(allShifters['XFT'].contract) token = await Token.at(allShifters['anonUSD500'].contract) tokenETH = await Token.at(allShifters['anonETH1'].contract) tokenBTC = await Token.at(allShifters['anonBTC1'].contract) weth = await ERC20.at(weth9) let shifterPoolETH = await shifterETH.tokenPool() let shifterPoolUSD = await shifter.tokenPool() let shifterPoolBTC = await shifterBTC.tokenPool() let poolETH = await UniV3Pool.at(shifterPoolETH) let poolUSD = await UniV3Pool.at(shifterPoolUSD) let poolBTC = await UniV3Pool.at(shifterPoolBTC) await poolETH.increaseObservationCardinalityNext.sendTransaction(cardinalityLaunch, {from: sender}) await poolUSD.increaseObservationCardinalityNext.sendTransaction(cardinalityLaunch, {from: sender}) await poolBTC.increaseObservationCardinalityNext.sendTransaction(cardinalityLaunch, {from: sender}) badRecipient = await BadRecipient.new() groth16 = await buildGroth16() circuit = require('../build/circuits/withdraw.json') proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer // Give the shifter roles await xft.grantRole(burnerRole, shifter.address) await xft.grantRole(burnerRole, shifterETH.address) await token.grantRole(minterRole, shifter.address) await tokenETH.grantRole(minterRole, shifterETH.address) // Give sender roles await xft.grantRole(minterRole, sender) await token.grantRole(minterRole, sender) await tokenETH.grantRole(minterRole, sender) // Snapshot here snapshotId = await takeSnapshot() }) describe('#constructor', () => { it('should initialize', async () => { const tokenFromContract = await shifter.xft() tokenFromContract.should.be.equal(xft.address) }) }) describe('#grantRole Burner', () => { it('should work', async () => { // Checks if contract has role granted already and revokes role if true let hasRole = await xft.hasRole(burnerRole, shifter.address) if (hasRole) { await xft.revokeRole(burnerRole, shifter.address) } let { logs } = await xft.grantRole(burnerRole, shifter.address) logs[0].address.should.be.equal(xft.address) logs[0].event.should.be.equal('RoleGranted') logs[0].args.role.should.be.equal(Web3Utils.keccak256("BURNER_ROLE")) logs[0].args.account.should.be.equal(shifter.address) }) }) describe('#grantRole Minter', () => { it('should work', async () => { // Checks if contract has role granted already and revokes role if true let hasRole = await token.hasRole(minterRole, shifter.address) if (hasRole) { await token.revokeRole(minterRole, shifter.address) } let { logs } = await token.grantRole(minterRole, shifter.address) logs[0].address.should.be.equal(token.address) logs[0].event.should.be.equal('RoleGranted') logs[0].args.role.should.be.equal(Web3Utils.keccak256("MINTER_ROLE")) logs[0].args.account.should.be.equal(shifter.address) }) }) describe('#TokenSwap', () => { it('should reject if not activated', async () => { const holder = accounts[0] // Checks if contract has role granted already and grants role if false let hasRole = await xft.hasRole(minterRole, tokenSwap.address) if (!hasRole) { await xft.grantRole(minterRole, tokenSwap.address) } let balanceBefore = await xftOld.balanceOf(holder) await xftOld.approve.sendTransaction(tokenSwap.address, balanceBefore, {from: holder}) let { reason } = await tokenSwap.upgrade.sendTransaction({from: holder}).should.be.rejected reason.should.be.equal("You can't upgrade yet"); }) it('should work after activation', async () => { const holder = accounts[0] // Checks if contract has role granted already and grants role if false let hasRole = await xft.hasRole(minterRole, tokenSwap.address) if (!hasRole) { await xft.grantRole(minterRole, tokenSwap.address) } await tokenSwap.setActive.sendTransaction({from: holder}); let balanceBefore = await xftOld.balanceOf(holder) let balanceNewBefore = await xft.balanceOf(holder) await xftOld.approve.sendTransaction(tokenSwap.address, balanceBefore, {from: holder}) let { logs } = await tokenSwap.upgrade.sendTransaction({from: holder}) let balanceOld = await xftOld.balanceOf(holder) let balanceNew = await xft.balanceOf(holder) let balanceContract = await xft.balanceOf(tokenSwap.address) // Tests logs[0].event.should.be.equal('XFTUpgraded') logs[0].args.user.should.be.equal(holder) logs[0].args.amount.should.be.eq.BN(balanceBefore) balanceNew.should.be.eq.BN(balanceBefore.add(balanceNewBefore)) balanceOld.should.be.eq.BN(0) balanceContract.should.be.eq.BN(0) }) it('should reject if contract does not have minterRole', async () => { const holder = accounts[0] await tokenSwap.setActive.sendTransaction({from: holder}); let hasRole = await xft.hasRole(minterRole, tokenSwap.address) if (hasRole) { await xft.revokeRole(minterRole, tokenSwap.address) } let balanceBefore = await xftOld.balanceOf(holder) await xftOld.approve.sendTransaction(tokenSwap.address, balanceBefore, {from: holder}) let error = await tokenSwap.upgrade.sendTransaction({ from: holder }).should.be.rejected error.reason.should.be.equal('Caller is not a minter') }) }) describe('#deposit', () => { it('should work', async () => { const commitment = toFixedHex(43) let role0 = await xft.hasRole(burnerRole, shifter.address) role0.should.be.equal(true) await xft.approve(shifter.address, tokenDenomination) hexNote = await encryptNote(generateDeposit(), storagePassword); let { logs } = await shifter.deposit(commitment, hexNote, storageHash, { from: sender, value: "50000000000000000" }) logs[0].event.should.be.equal('Deposit') logs[0].args.commitment.should.be.equal(commitment) logs[0].args.leafIndex.should.be.eq.BN(0) }) it('should reject insufficient balance', async () => { const commitment = toFixedHex(43) await xft.approve(shifter.address, tokenDenomination) let user = accounts[5] hexNote = await encryptNote(generateDeposit(), storagePassword); let error = await shifter.deposit(commitment, hexNote, storageHash, { from: user, value: "50000000000000000" }).should.be.rejected error.reason.should.be.equal('Insufficient Balance') }) it('should revert if contract is not assigned burnerRole', async () => { const commitment = toFixedHex(43) // Checks if contract has role granted already and revokes role if true let hasRole = await xft.hasRole(burnerRole, shifter.address) if (hasRole) { await xft.revokeRole(burnerRole, shifter.address) } await xft.approve(shifter.address, tokenDenomination) hexNote = await encryptNote(generateDeposit(), storagePassword); let error = await shifter.deposit(commitment, hexNote, storageHash, { from: sender, value: "50000000000000000" }).should.be.rejected error.reason.should.be.equal('Caller is not a burner') }) }) describe('#withdraw', () => { it('should work (and fetch the note onchain)', async () => { const deposit = generateDeposit() const user = sender; const cost = await shifter.getCost(tokenDenomination); await xft.mint(user, cost + "0") // Give it 10x the cost tree.insert(deposit.commitment) const balanceUserBefore = await xft.balanceOf(user) await xft.approve(shifter.address, tokenDenomination, { from: user }) hexNote = await encryptNote(deposit, storagePassword); await shifter.deposit(toFixedHex(deposit.commitment), hexNote, storageHash, { from: sender, gasPrice: '0', value: "50000000000000000" }) const latestDeposit = await storage.getLatestDeposit(shifter.address, user, storageHash) hexNote = await xcrypt(latestDeposit, storagePassword); // Chainlink Feed only has 8 decimals of precision. // Price changes on the fly, can never have accurate price due to atomicity of txs unless no oracles change in between txs const { pathElements, pathIndices } = tree.path(0) // Circuit input const input = stringifyBigInts({ // public root: tree.root(), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), relayer, recipient, fee, refund, // private nullifier: deposit.nullifier, secret: deposit.secret, pathElements: pathElements, pathIndices: pathIndices, }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const { proof } = websnarkUtils.toSolidityInput(proofData) const balanceShifterBefore = await xft.balanceOf(shifter.address) const balanceRelayerBefore = await token.balanceOf(relayer) const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20)) const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20)) const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer) let isSpent = await shifter.isSpent(toFixedHex(input.nullifierHash)) isSpent.should.be.equal(false) const args = [ toFixedHex(input.root), toFixedHex(input.nullifierHash), toFixedHex(input.recipient, 20), toFixedHex(input.relayer, 20), toFixedHex(input.fee), toFixedHex(input.refund), ] const { logs } = await shifter.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' }) const balanceRelayerAfter = await token.balanceOf(relayer) const ethBalanceOperatorAfter = await web3.eth.getBalance(operator) const balanceReceiverAfter = await token.balanceOf(toFixedHex(recipient, 20)) const ethBalanceReceiverAfter = await web3.eth.getBalance(toFixedHex(recipient, 20)) const ethBalanceRelayerAfter = await web3.eth.getBalance(relayer) const feeBN = toBN(fee.toString()) balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore).add(feeBN)) balanceReceiverAfter.should.be.eq.BN( toBN(balanceReceiverBefore).add(toBN(tokenDenomination).sub(feeBN)), ) ethBalanceOperatorAfter.should.be.eq.BN(toBN(ethBalanceOperatorBefore)) ethBalanceReceiverAfter.should.be.eq.BN(toBN(ethBalanceReceiverBefore).add(toBN(refund)).add(toBN("50000000000000000"))) //Adds the deposited ether ToDo: Fix hardcoded value ethBalanceRelayerAfter.should.be.eq.BN(toBN(ethBalanceRelayerBefore).sub(toBN(refund))) logs[0].args.nullifierHash.should.be.equal(toFixedHex(input.nullifierHash)) logs[0].args.relayer.should.be.eq.BN(relayer) logs[0].args.fee.should.be.eq.BN(feeBN) isSpent = await shifter.isSpent(toFixedHex(input.nullifierHash)) isSpent.should.be.equal(true) }) it('should reject with wrong refund value', async () => { const deposit = generateDeposit() const user = accounts[4] const cost = await shifter.getCost(tokenDenomination); await xft.mint(user, cost + "0") // Give it 10x the cost tree.insert(deposit.commitment) await xft.approve(shifter.address, tokenDenomination, { from: user }) await shifter.deposit(toFixedHex(deposit.commitment), toFixedHex(rbigint(62)), storageHash, { from: user, gasPrice: '0', value: "50000000000000000" }) const { pathElements, pathIndices } = tree.path(0) // Circuit input const input = stringifyBigInts({ // public root: tree.root(), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), relayer, recipient, fee, refund, // private nullifier: deposit.nullifier, secret: deposit.secret, pathElements: pathElements, pathIndices: pathIndices, }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const { proof } = websnarkUtils.toSolidityInput(proofData) const args = [ toFixedHex(input.root), toFixedHex(input.nullifierHash), toFixedHex(input.recipient, 20), toFixedHex(input.relayer, 20), toFixedHex(input.fee), toFixedHex(input.refund), ] let { reason } = await shifter.withdraw(proof, ...args, { value: 1, from: relayer, gasPrice: '0' }) .should.be.rejected reason.should.be.equal('Incorrect refund amount received by the contract') ; ({ reason } = await shifter.withdraw(proof, ...args, { value: toBN(refund).mul(toBN(2)), from: relayer, gasPrice: '0', }).should.be.rejected) reason.should.be.equal('Incorrect refund amount received by the contract') }) it('should revert if contract not assigned minterRole', async () => { const deposit = generateDeposit() hexNote = await encryptNote(generateDeposit(), storagePassword); const user = accounts[4] const cost = await shifter.getCost(tokenDenomination); await xft.mint(user, cost + "0") // Give it 10x the cost // Checks if contract has role granted already and revokes role if true let hasRole = await token.hasRole(minterRole, shifter.address) if (hasRole) { await token.revokeRole(minterRole, shifter.address) } tree.insert(deposit.commitment) const balanceUserBefore = await xft.balanceOf(user) await xft.approve(shifter.address, tokenDenomination, { from: user }) await shifter.deposit(toFixedHex(deposit.commitment), hexNote, storageHash, { from: user, gasPrice: '0', value: "50000000000000000" }) const balanceUserAfter = await xft.balanceOf(user) const { pathElements, pathIndices } = tree.path(0) // Circuit input const input = stringifyBigInts({ // public root: tree.root(), nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)), relayer, recipient, fee, refund, // private nullifier: deposit.nullifier, secret: deposit.secret, pathElements: pathElements, pathIndices: pathIndices, }) const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key) const { proof } = websnarkUtils.toSolidityInput(proofData) const balanceShifterBefore = await xft.balanceOf(shifter.address) const balanceRelayerBefore = await token.balanceOf(relayer) const balanceReceiverBefore = await token.balanceOf(toFixedHex(recipient, 20)) const ethBalanceOperatorBefore = await web3.eth.getBalance(operator) const ethBalanceReceiverBefore = await web3.eth.getBalance(toFixedHex(recipient, 20)) const ethBalanceRelayerBefore = await web3.eth.getBalance(relayer) let isSpent = await shifter.isSpent(toFixedHex(input.nullifierHash)) isSpent.should.be.equal(false) const args = [ toFixedHex(input.root), toFixedHex(input.nullifierHash), toFixedHex(input.recipient, 20), toFixedHex(input.relayer, 20), toFixedHex(input.fee), toFixedHex(input.refund), ] let { reason } = await shifter.withdraw(proof, ...args, { value: refund, from: relayer, gasPrice: '0' }).should.be.rejected reason.should.be.equal('Caller is not a minter') }) }) describe('#simpleShift', () => { it('should work', async () => { const user = accounts[4] const amount = "10000"; const chainlinkFeed = await shifter.chainlinkFeed(); const xftPool = await shifter.xftPool(); const tokenPool = await shifter.tokenPool(); await token.mint(user, amount) const cost = await oracle.getCostSimpleShift(amount, chainlinkFeed, xftPool, tokenPool, {from: user}) await shifter.simpleShift(amount, user, { from: user }) senderTokenBalance = await xft.balanceOf(user) senderUSDBalance = await token.balanceOf(user) // Find a way to atomically check balances senderTokenBalance.should.be.eq.BN(cost) senderUSDBalance.should.be.eq.BN(toBN(0)) }) it('should revert if sender has insufficient balance', async () => { let { reason } = await shifter.simpleShift(tokenDenomination, sender, { from: sender }).should.be.rejected reason.should.be.equal('Insufficient balance') }) }) describe('#Paused', () => { it('should work', async () => { await shifter.pause({ from: sender }) let isPaused = await shifter.paused() isPaused.should.be.equal(true) let { reason } = await shifter.simpleShift(tokenDenomination, sender, { from: sender }).should.be.rejected reason.should.be.equal('Pausable: paused') }) }) afterEach(async () => { await revertSnapshot(snapshotId.result) // eslint-disable-next-line require-atomic-updates snapshotId = await takeSnapshot() tree = new MerkleTree(levels) }) })