857 lines
35 KiB
JavaScript
857 lines
35 KiB
JavaScript
/* 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 { takeSnapshot, revertSnapshot, mineBlock } = require('../scripts/ganacheHelper')
|
|
const tornConfig = require('torn-token')
|
|
const RLP = require('rlp')
|
|
const Controller = require('../src/controller')
|
|
const Account = require('../src/account')
|
|
const Note = require('../src/note')
|
|
const {
|
|
toFixedHex,
|
|
poseidonHash,
|
|
poseidonHash2,
|
|
packEncryptedMessage,
|
|
unpackEncryptedMessage,
|
|
getExtWithdrawArgsHash,
|
|
} = require('../src/utils')
|
|
const { getEncryptionPublicKey } = require('eth-sig-util')
|
|
const Miner = artifacts.require('MinerMock')
|
|
const TornadoTrees = artifacts.require('TornadoTreesMock')
|
|
const TornadoTreesV1 = artifacts.require('TornadoTreesV1Mock')
|
|
const Torn = artifacts.require('TORNMock')
|
|
const RewardSwap = artifacts.require('RewardSwapMock')
|
|
const RewardVerifier = artifacts.require('RewardVerifier')
|
|
const WithdrawVerifier = artifacts.require('WithdrawVerifier')
|
|
const TreeUpdateVerifier = artifacts.require('TreeUpdateVerifier')
|
|
const provingKeys = {
|
|
rewardCircuit: require('../build/circuits/Reward.json'),
|
|
withdrawCircuit: require('../build/circuits/Withdraw.json'),
|
|
treeUpdateCircuit: require('../build/circuits/TreeUpdate.json'),
|
|
rewardProvingKey: fs.readFileSync('./build/circuits/Reward_proving_key.bin').buffer,
|
|
withdrawProvingKey: fs.readFileSync('./build/circuits/Withdraw_proving_key.bin').buffer,
|
|
treeUpdateProvingKey: fs.readFileSync('./build/circuits/TreeUpdate_proving_key.bin').buffer,
|
|
}
|
|
const MerkleTree = require('@tornado/fixed-merkle-tree')
|
|
|
|
// Set time to beginning of a second
|
|
async function timeReset() {
|
|
const delay = 1000 - new Date().getMilliseconds()
|
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
await mineBlock()
|
|
}
|
|
|
|
async function getNextAddr(sender, offset = 0) {
|
|
const nonce = await web3.eth.getTransactionCount(sender)
|
|
return (
|
|
'0x' +
|
|
web3.utils
|
|
.sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
|
|
.slice(12)
|
|
.substring(14)
|
|
)
|
|
}
|
|
|
|
async function registerNote(note, tornadoTrees) {
|
|
await tornadoTrees.register(
|
|
note.instance,
|
|
toFixedHex(note.commitment),
|
|
toFixedHex(note.nullifierHash),
|
|
note.depositBlock,
|
|
note.withdrawalBlock,
|
|
)
|
|
|
|
return {
|
|
depositLeaf: {
|
|
instance: note.instance,
|
|
hash: toFixedHex(note.commitment),
|
|
block: toFixedHex(note.depositBlock),
|
|
},
|
|
withdrawalLeaf: {
|
|
instance: note.instance,
|
|
hash: toFixedHex(note.nullifierHash),
|
|
block: toFixedHex(note.withdrawalBlock),
|
|
},
|
|
}
|
|
}
|
|
|
|
contract('Miner', (accounts) => {
|
|
let miner
|
|
let torn
|
|
let rewardSwap
|
|
let tornadoTrees
|
|
const tornado = '0x3535249DFBb73e21c2aCDC6e42796d920A0379b7'
|
|
const tornCap = toBN(tornConfig.torn.cap)
|
|
const miningCap = toBN(tornConfig.torn.distribution.miningV2.amount)
|
|
const initialTornBalance = toBN(tornConfig.miningV2.initialBalance)
|
|
const RATE = toBN(10)
|
|
const amount = toBN(15)
|
|
// eslint-disable-next-line no-unused-vars
|
|
const sender = accounts[0]
|
|
const recipient = accounts[1]
|
|
// eslint-disable-next-line no-unused-vars
|
|
const relayer = accounts[2]
|
|
const levels = 20
|
|
let snapshotId
|
|
const AnotherWeb3 = require('web3')
|
|
let contract
|
|
let controller
|
|
const note1 = new Note({
|
|
instance: tornado,
|
|
depositBlock: 10,
|
|
withdrawalBlock: 10 + 4 * 60 * 24,
|
|
})
|
|
const note2 = new Note({
|
|
instance: tornado,
|
|
depositBlock: 10,
|
|
withdrawalBlock: 10 + 2 * 4 * 60 * 24,
|
|
})
|
|
const note3 = new Note({
|
|
instance: tornado,
|
|
depositBlock: 10,
|
|
withdrawalBlock: 10 + 3 * 4 * 60 * 24,
|
|
})
|
|
const note = note1
|
|
const notes = [note1, note2, note3]
|
|
|
|
const emptyTree = new MerkleTree(levels, [], { hashFunction: poseidonHash2 })
|
|
const privateKey = web3.eth.accounts.create().privateKey.slice(2)
|
|
const publicKey = getEncryptionPublicKey(privateKey)
|
|
const operator = accounts[0]
|
|
const verifier = accounts[1]
|
|
const thirtyDays = 30 * 24 * 3600
|
|
const poolWeight = 1e11
|
|
const governance = accounts[9]
|
|
let depositTree
|
|
let withdrawalTree
|
|
|
|
before(async () => {
|
|
const rewardVerifier = await RewardVerifier.new()
|
|
const withdrawVerifier = await WithdrawVerifier.new()
|
|
const treeUpdateVerifier = await TreeUpdateVerifier.new()
|
|
const tornadoTreesV1 = await TornadoTreesV1.new(
|
|
0,
|
|
0,
|
|
toFixedHex(emptyTree.root()),
|
|
toFixedHex(emptyTree.root()),
|
|
)
|
|
|
|
tornadoTrees = await TornadoTrees.new(operator, tornadoTreesV1.address, {
|
|
depositsFrom: 0,
|
|
depositsStep: 0,
|
|
withdrawalsFrom: 0,
|
|
withdrawalsStep: 0,
|
|
})
|
|
await tornadoTrees.initialize(operator, verifier)
|
|
const swapExpectedAddr = await getNextAddr(accounts[0], 1)
|
|
const minerExpectedAddr = await getNextAddr(accounts[0], 2)
|
|
torn = await Torn.new(sender, thirtyDays, [
|
|
{ to: swapExpectedAddr, amount: miningCap.toString() },
|
|
{ to: sender, amount: tornCap.sub(miningCap).toString() },
|
|
])
|
|
rewardSwap = await RewardSwap.new(
|
|
torn.address,
|
|
minerExpectedAddr,
|
|
miningCap.toString(),
|
|
initialTornBalance.toString(),
|
|
poolWeight,
|
|
)
|
|
miner = await Miner.new(
|
|
rewardSwap.address,
|
|
governance,
|
|
tornadoTrees.address,
|
|
[rewardVerifier.address, withdrawVerifier.address, treeUpdateVerifier.address],
|
|
toFixedHex(emptyTree.root()),
|
|
[{ instance: tornado, value: RATE.toString() }],
|
|
)
|
|
|
|
depositTree = new MerkleTree(levels, [], { hashFunction: poseidonHash2 })
|
|
withdrawalTree = new MerkleTree(levels, [], { hashFunction: poseidonHash2 })
|
|
for (const note of notes) {
|
|
const { depositLeaf, withdrawalLeaf } = await registerNote(note, tornadoTrees)
|
|
depositTree.insert(poseidonHash([depositLeaf.instance, depositLeaf.hash, depositLeaf.block]))
|
|
withdrawalTree.insert(
|
|
poseidonHash([withdrawalLeaf.instance, withdrawalLeaf.hash, withdrawalLeaf.block]),
|
|
)
|
|
}
|
|
|
|
await tornadoTrees.updateRoots(toFixedHex(depositTree.root()), toFixedHex(withdrawalTree.root()))
|
|
|
|
const anotherWeb3 = new AnotherWeb3(web3.currentProvider)
|
|
contract = new anotherWeb3.eth.Contract(miner.abi, miner.address)
|
|
const tornadoTreesContract = new anotherWeb3.eth.Contract(tornadoTrees.abi, tornadoTrees.address)
|
|
controller = new Controller({
|
|
contract,
|
|
tornadoTreesContract,
|
|
merkleTreeHeight: levels,
|
|
provingKeys,
|
|
})
|
|
await controller.init()
|
|
snapshotId = await takeSnapshot()
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
await timeReset()
|
|
})
|
|
|
|
describe('#constructor', () => {
|
|
it('should initialize', async () => {
|
|
const tokenFromContract = await rewardSwap.torn()
|
|
tokenFromContract.should.be.equal(torn.address)
|
|
|
|
const rewardSwapFromContract = await miner.rewardSwap()
|
|
rewardSwapFromContract.should.be.equal(rewardSwap.address)
|
|
|
|
const rateFromContract = await miner.rates(tornado)
|
|
rateFromContract.should.be.eq.BN(RATE)
|
|
})
|
|
})
|
|
|
|
describe('#Note.fromString()', () => {
|
|
it('should work', () => {
|
|
const note = Note.fromString(
|
|
'tornado-eth-1-1-0x3a1f1e0e10b22b15ed8208bc810dd5564d564fd7930874db4d7d58870deb72978fb5fccae2f1554cac71e2cff85f9c8908295647adf2b443a4dd93635d8d',
|
|
'0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
|
10,
|
|
15,
|
|
)
|
|
note.secret.should.be.eq.BN(toBN('0x8d5d6393dda443b4f2ad47562908899c5ff8cfe271ac4c55f1e2cafcb58f97'))
|
|
note.nullifier.should.be.eq.BN(toBN('0x72eb0d87587d4ddb740893d74f564d56d50d81bc0882ed152bb2100e1e1f3a'))
|
|
note.nullifierHash.should.be.eq.BN(
|
|
toBN('0x33403728f37e70a275acac2eb8297a3231698f9003838ec4cd7115ee2693943'),
|
|
)
|
|
note.commitment.should.be.eq.BN(
|
|
toBN('0x1a08fd10dae9806ce25b62582e44d237c1cb9f8d6bf73e756f18d0e5d7a7351a'),
|
|
)
|
|
const note1 = Note.fromString(
|
|
'tornado-eth-1-5-0x00b9787ae8b877cc612acfc3d46eb1303c618ef386cb4383b75be5b300ac9b94818f3f7a589b4667612e879dbfd44198231c86aaa412a3770fd86d8fc045',
|
|
'0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
|
10,
|
|
15,
|
|
)
|
|
note1.secret.should.be.eq.BN(toBN('0x45c08f6dd80f77a312a4aa861c239841d4bf9d872e6167469b587a3f8f8194'))
|
|
note1.nullifier.should.be.eq.BN(
|
|
toBN('0x9bac00b3e55bb78343cb86f38e613c30b16ed4c3cf2a61cc77b8e87a78b900'),
|
|
)
|
|
note1.nullifierHash.should.be.eq.BN(
|
|
toBN('0x1892018e434ed0476992c7e05e6022b69eacf746a978191117194d180104aed1'),
|
|
)
|
|
note1.commitment.should.be.eq.BN(
|
|
toBN('0x270a865e2bf7de26b0a6de08c527f4d13e2b49026f6aaeeea0866a9dd39e19f6'),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#Account', () => {
|
|
it('should throw on negative amount', () => {
|
|
;(() => new Account({ amount: toBN(-1) })).should.throw('Cannot create an account with negative amount')
|
|
})
|
|
})
|
|
|
|
describe('#encrypt', () => {
|
|
it('should work', () => {
|
|
const account = new Account()
|
|
const encryptedAccount = account.encrypt(publicKey)
|
|
const encryptedMessage = packEncryptedMessage(encryptedAccount)
|
|
const unpackedMessage = unpackEncryptedMessage(encryptedMessage)
|
|
const account2 = Account.decrypt(privateKey, unpackedMessage)
|
|
|
|
account.amount.should.be.eq.BN(account2.amount)
|
|
account.secret.should.be.eq.BN(account2.secret)
|
|
account.nullifier.should.be.eq.BN(account2.nullifier)
|
|
account.commitment.should.be.eq.BN(account2.commitment)
|
|
})
|
|
})
|
|
|
|
describe('#reward', () => {
|
|
it('should work', async () => {
|
|
const zeroAccount = new Account()
|
|
const accountCount = await miner.accountCount()
|
|
|
|
zeroAccount.amount.should.be.eq.BN(toBN(0))
|
|
|
|
const rewardNullifierBefore = await miner.rewardNullifiers(toFixedHex(note.rewardNullifier))
|
|
rewardNullifierBefore.should.be.false
|
|
const accountNullifierBefore = await miner.accountNullifiers(toFixedHex(zeroAccount.nullifier))
|
|
accountNullifierBefore.should.be.false
|
|
|
|
const { proof, args, account } = await controller.reward({ account: zeroAccount, note, publicKey })
|
|
const { logs } = await miner.reward(proof, args)
|
|
logs[0].event.should.be.equal('NewAccount')
|
|
logs[0].args.commitment.should.be.equal(toFixedHex(account.commitment))
|
|
logs[0].args.index.should.be.eq.BN(accountCount)
|
|
|
|
logs[0].args.nullifier.should.be.equal(toFixedHex(zeroAccount.nullifierHash))
|
|
|
|
const encryptedAccount = logs[0].args.encryptedAccount
|
|
const account2 = Account.decrypt(privateKey, unpackEncryptedMessage(encryptedAccount))
|
|
account.amount.should.be.eq.BN(account2.amount)
|
|
account.secret.should.be.eq.BN(account2.secret)
|
|
account.nullifier.should.be.eq.BN(account2.nullifier)
|
|
account.commitment.should.be.eq.BN(account2.commitment)
|
|
|
|
const accountCountAfter = await miner.accountCount()
|
|
accountCountAfter.should.be.eq.BN(accountCount.add(toBN(1)))
|
|
const rootAfter = await miner.getLastAccountRoot()
|
|
rootAfter.should.be.equal(args.account.outputRoot)
|
|
const rewardNullifierAfter = await miner.rewardNullifiers(toFixedHex(note.rewardNullifier))
|
|
rewardNullifierAfter.should.be.true
|
|
const accountNullifierAfter = await miner.accountNullifiers(toFixedHex(zeroAccount.nullifierHash))
|
|
accountNullifierAfter.should.be.true
|
|
|
|
account.amount.should.be.eq.BN(toBN(note.withdrawalBlock - note.depositBlock).mul(RATE))
|
|
})
|
|
|
|
it('should send fee to relayer', async () => {
|
|
const fee = toBN(3)
|
|
const amount = toBN(44)
|
|
const delta = toBN('10000') // max floating point error
|
|
|
|
const claim = await controller.reward({ account: new Account(), note, publicKey, relayer, fee })
|
|
await timeReset()
|
|
let expectedFeeInTorn = await rewardSwap.getExpectedReturn(fee)
|
|
let relayerBalanceBefore = await torn.balanceOf(relayer)
|
|
await miner.reward(claim.proof, claim.args)
|
|
let relayerBalanceAfter = await torn.balanceOf(relayer)
|
|
relayerBalanceAfter.should.be.eq.BN(relayerBalanceBefore.add(expectedFeeInTorn))
|
|
|
|
const withdrawal = await controller.withdraw({
|
|
account: claim.account,
|
|
amount,
|
|
recipient,
|
|
publicKey,
|
|
relayer,
|
|
fee,
|
|
})
|
|
await timeReset()
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
expectedFeeInTorn = await rewardSwap.getExpectedReturn(amount.add(fee))
|
|
expectedFeeInTorn = expectedFeeInTorn.sub(expectedAmountInTorn)
|
|
|
|
relayerBalanceBefore = await torn.balanceOf(relayer)
|
|
const recipientBalanceBefore = await torn.balanceOf(recipient)
|
|
await miner.withdraw(withdrawal.proof, withdrawal.args)
|
|
const recipientBalanceAfter = await torn.balanceOf(recipient)
|
|
relayerBalanceAfter = await torn.balanceOf(relayer)
|
|
|
|
recipientBalanceAfter.should.be.eq.BN(recipientBalanceBefore.add(expectedAmountInTorn))
|
|
relayerBalanceAfter.sub(relayerBalanceBefore).sub(expectedFeeInTorn).should.be.lt.BN(delta)
|
|
})
|
|
|
|
it('should use fallback with outdated tree', async () => {
|
|
const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
|
|
|
|
const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmp.proof, tmp.args)
|
|
|
|
await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
|
|
|
|
const update = await controller.treeUpdate(account.commitment)
|
|
await miner.reward(proof, args, update.proof, update.args)
|
|
|
|
const rootAfter = await miner.getLastAccountRoot()
|
|
rootAfter.should.be.equal(update.args.newRoot)
|
|
})
|
|
|
|
it('should reject with incorrect insert position', async () => {
|
|
const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmp.proof, tmp.args)
|
|
|
|
const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
let fakeIndex = toBN(args.account.outputPathIndices).sub(toBN('1'))
|
|
malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
|
|
|
|
fakeIndex = toBN(args.account.outputPathIndices).add(toBN('1'))
|
|
malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
|
|
|
|
fakeIndex = toBN(args.account.outputPathIndices).add(toBN('10000000000000000000000000'))
|
|
malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
|
|
|
|
await miner.reward(proof, args).should.be.fulfilled
|
|
})
|
|
|
|
it('should reject with incorrect external data hash', async () => {
|
|
const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.extDataHash = toFixedHex('0xdeadbeef')
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
malformedArgs.extDataHash = toFixedHex('0x00')
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
await miner.reward(proof, args).should.be.fulfilled
|
|
})
|
|
|
|
it('should prevent fee overflow', async () => {
|
|
const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.fee = toFixedHex(toBN(2).pow(toBN(248)))
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Fee value out of range')
|
|
|
|
malformedArgs.fee = toFixedHex(toBN(2).pow(toBN(256)).sub(toBN(1)))
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Fee value out of range')
|
|
|
|
await miner.reward(proof, args).should.be.fulfilled
|
|
})
|
|
|
|
it('should reject with invalid reward rate', async () => {
|
|
const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.instance = miner.address
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward rate')
|
|
|
|
malformedArgs.rate = toFixedHex(toBN(9999999))
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward rate')
|
|
|
|
malformedArgs.instance = toFixedHex('0x00', 20)
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward rate')
|
|
|
|
const anotherInstance = accounts[5]
|
|
const rate = toBN(1000)
|
|
await miner.setRates([{ instance: anotherInstance, value: rate.toString() }], { from: governance })
|
|
|
|
malformedArgs.instance = anotherInstance
|
|
malformedArgs.rate = toFixedHex(rate)
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward proof')
|
|
|
|
await miner.reward(proof, args).should.be.fulfilled
|
|
})
|
|
|
|
it('should reject for double spend', async () => {
|
|
let { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
|
|
await miner.reward(proof, args).should.be.fulfilled
|
|
;({ proof, args } = await controller.reward({ account: new Account(), note, publicKey }))
|
|
await miner.reward(proof, args).should.be.rejectedWith('Reward has been already spent')
|
|
})
|
|
|
|
it('should reject for invalid proof', async () => {
|
|
const claim1 = await controller.reward({ account: new Account(), note, publicKey })
|
|
const claim2 = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
|
|
await miner.reward(claim2.proof, claim1.args).should.be.rejectedWith('Invalid reward proof')
|
|
})
|
|
|
|
it('should reject for invalid account root', async () => {
|
|
const account1 = new Account()
|
|
const account2 = new Account()
|
|
const account3 = new Account()
|
|
|
|
const fakeTree = new MerkleTree(
|
|
levels,
|
|
[account1.commitment, account2.commitment, account3.commitment],
|
|
{ hashFunction: poseidonHash2 },
|
|
)
|
|
const { proof, args } = await controller.reward({ account: account1, note, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
malformedArgs.account.inputRoot = toFixedHex(fakeTree.root())
|
|
await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid account root')
|
|
})
|
|
|
|
it('should reject with outdated account root (treeUpdate proof validation)', async () => {
|
|
const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
|
|
|
|
const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmp.proof, tmp.args)
|
|
|
|
await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
|
|
|
|
const update = await controller.treeUpdate(account.commitment)
|
|
|
|
const tmp2 = await controller.reward({ account: new Account(), note: note3, publicKey })
|
|
await miner.reward(tmp2.proof, tmp2.args)
|
|
|
|
await miner
|
|
.reward(proof, args, update.proof, update.args)
|
|
.should.be.rejectedWith('Outdated tree update merkle root')
|
|
})
|
|
|
|
it('should reject for incorrect commitment (treeUpdate proof validation)', async () => {
|
|
const claim = await controller.reward({ account: new Account(), note, publicKey })
|
|
|
|
const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmp.proof, tmp.args)
|
|
|
|
await miner.reward(claim.proof, claim.args).should.be.rejectedWith('Outdated account merkle root')
|
|
const anotherAccount = new Account()
|
|
const update = await controller.treeUpdate(anotherAccount.commitment)
|
|
|
|
await miner
|
|
.reward(claim.proof, claim.args, update.proof, update.args)
|
|
.should.be.rejectedWith('Incorrect commitment inserted')
|
|
|
|
claim.args.account.outputCommitment = update.args.leaf
|
|
await miner
|
|
.reward(claim.proof, claim.args, update.proof, update.args)
|
|
.should.be.rejectedWith('Invalid reward proof')
|
|
})
|
|
|
|
it('should reject for incorrect account insert index (treeUpdate proof validation)', async () => {
|
|
const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
|
|
|
|
const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmp.proof, tmp.args)
|
|
|
|
await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
|
|
|
|
const update = await controller.treeUpdate(account.commitment)
|
|
const malformedArgs = JSON.parse(JSON.stringify(update.args))
|
|
|
|
let fakeIndex = toBN(update.args.pathIndices).sub(toBN('1'))
|
|
malformedArgs.pathIndices = toFixedHex(fakeIndex)
|
|
|
|
await miner
|
|
.reward(proof, args, update.proof, malformedArgs)
|
|
.should.be.rejectedWith('Incorrect account insert index')
|
|
})
|
|
|
|
it('should reject for invalid tree update proof (treeUpdate proof validation)', async () => {
|
|
const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
|
|
|
|
const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmp.proof, tmp.args)
|
|
|
|
await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
|
|
|
|
const update = await controller.treeUpdate(account.commitment)
|
|
await miner
|
|
.reward(proof, args, tmp.proof, update.args)
|
|
.should.be.rejectedWith('Invalid tree update proof')
|
|
})
|
|
})
|
|
|
|
describe('#withdraw', () => {
|
|
let proof, args, account
|
|
// prettier-ignore
|
|
beforeEach(async () => {
|
|
({ proof, args, account } = await controller.reward({ account: new Account(), note, publicKey }))
|
|
await miner.reward(proof, args)
|
|
})
|
|
|
|
it('should work', async () => {
|
|
const accountNullifierBefore = await miner.accountNullifiers(toFixedHex(account.nullifierHash))
|
|
accountNullifierBefore.should.be.false
|
|
|
|
const accountCount = await miner.accountCount()
|
|
const withdrawSnark = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
await timeReset()
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const { logs } = await miner.withdraw(withdrawSnark.proof, withdrawSnark.args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
|
|
const accountCountAfter = await miner.accountCount()
|
|
accountCountAfter.should.be.eq.BN(accountCount.add(toBN(1)))
|
|
const rootAfter = await miner.getLastAccountRoot()
|
|
rootAfter.should.be.equal(withdrawSnark.args.account.outputRoot)
|
|
const accountNullifierAfter = await miner.accountNullifiers(toFixedHex(account.nullifierHash))
|
|
accountNullifierAfter.should.be.true
|
|
|
|
logs[0].event.should.be.equal('NewAccount')
|
|
logs[0].args.commitment.should.be.equal(toFixedHex(withdrawSnark.account.commitment))
|
|
logs[0].args.index.should.be.eq.BN(accountCount)
|
|
logs[0].args.nullifier.should.be.equal(toFixedHex(account.nullifierHash))
|
|
|
|
const encryptedAccount = logs[0].args.encryptedAccount
|
|
const account2 = Account.decrypt(privateKey, unpackEncryptedMessage(encryptedAccount))
|
|
withdrawSnark.account.amount.should.be.eq.BN(account2.amount)
|
|
withdrawSnark.account.secret.should.be.eq.BN(account2.secret)
|
|
withdrawSnark.account.nullifier.should.be.eq.BN(account2.nullifier)
|
|
withdrawSnark.account.commitment.should.be.eq.BN(account2.commitment)
|
|
})
|
|
|
|
it('should reject for double spend', async () => {
|
|
const withdrawSnark = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
await timeReset()
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(withdrawSnark.proof, withdrawSnark.args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
|
|
await miner
|
|
.withdraw(withdrawSnark.proof, withdrawSnark.args)
|
|
.should.be.rejectedWith('Outdated account state')
|
|
})
|
|
|
|
it('should reject with incorrect insert position', async () => {
|
|
const { proof, args } = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
let fakeIndex = toBN(args.account.outputPathIndices).sub(toBN('1'))
|
|
malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
|
|
|
|
fakeIndex = toBN(args.account.outputPathIndices).add(toBN('1'))
|
|
malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
|
|
|
|
fakeIndex = toBN(args.account.outputPathIndices).add(toBN('10000000000000000000000000'))
|
|
malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
|
|
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(proof, args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
})
|
|
|
|
it('should reject with incorrect external data hash', async () => {
|
|
const { proof, args } = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.extDataHash = toFixedHex('0xdeadbeef')
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
malformedArgs.extDataHash = toFixedHex('0x00')
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(proof, args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
})
|
|
|
|
it('should reject for amount overflow', async () => {
|
|
const { proof, args } = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.amount = toFixedHex(toBN(2).pow(toBN(248)))
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Amount value out of range')
|
|
|
|
malformedArgs.amount = toFixedHex(toBN(2).pow(toBN(256)).sub(toBN(1)))
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Amount value out of range')
|
|
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(proof, args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
})
|
|
|
|
it('should reject for fee overflow', async () => {
|
|
const fee = account.amount.add(toBN(5))
|
|
const fakeAmount = toBN(-5)
|
|
const { proof, args } = await controller.withdraw({
|
|
account,
|
|
amount: fakeAmount,
|
|
recipient,
|
|
publicKey,
|
|
fee,
|
|
})
|
|
await miner.withdraw(proof, args).should.be.rejectedWith('Amount should be greater than fee')
|
|
})
|
|
|
|
it('should reject for unfair amount', async () => {
|
|
const fee = toBN(3)
|
|
const amountToWithdraw = amount.sub(fee)
|
|
const { proof, args } = await controller.withdraw({
|
|
account,
|
|
amount: amountToWithdraw,
|
|
recipient,
|
|
publicKey,
|
|
})
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.amount = toFixedHex(amountToWithdraw.add(amountToWithdraw))
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Invalid withdrawal proof')
|
|
|
|
await timeReset()
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amountToWithdraw)
|
|
await miner.withdraw(proof, args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
})
|
|
|
|
it('can use fallback with outdated tree', async () => {
|
|
const tmpReward = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmpReward.proof, tmpReward.args)
|
|
|
|
const withdrawal = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
|
|
const tmpWithdraw = await controller.withdraw({
|
|
account: tmpReward.account,
|
|
amount,
|
|
recipient,
|
|
publicKey,
|
|
})
|
|
await miner.withdraw(tmpWithdraw.proof, tmpWithdraw.args)
|
|
|
|
await miner
|
|
.withdraw(withdrawal.proof, withdrawal.args)
|
|
.should.be.rejectedWith('Outdated account merkle root')
|
|
|
|
const update = await controller.treeUpdate(withdrawal.account.commitment)
|
|
await timeReset()
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(withdrawal.proof, withdrawal.args, update.proof, update.args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
|
|
const rootAfter = await miner.getLastAccountRoot()
|
|
rootAfter.should.be.equal(update.args.newRoot)
|
|
})
|
|
|
|
it('should reject for invalid proof', async () => {
|
|
const tmpReward = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(tmpReward.proof, tmpReward.args)
|
|
|
|
const withdrawal = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
const tmpWithdraw = await controller.withdraw({
|
|
account: tmpReward.account,
|
|
amount,
|
|
recipient,
|
|
publicKey,
|
|
})
|
|
|
|
await miner
|
|
.withdraw(tmpWithdraw.proof, withdrawal.args)
|
|
.should.be.rejectedWith('Invalid withdrawal proof')
|
|
})
|
|
|
|
it('should reject for malformed relayer and recipient address and fee', async () => {
|
|
const fakeRelayer = accounts[6]
|
|
const fakeRecipient = accounts[7]
|
|
const fee = 12
|
|
const fakeFee = 123
|
|
const { proof, args } = await controller.withdraw({
|
|
account,
|
|
amount,
|
|
recipient,
|
|
publicKey,
|
|
fee,
|
|
relayer,
|
|
})
|
|
const malformedArgs = JSON.parse(JSON.stringify(args))
|
|
|
|
malformedArgs.extData.recipient = fakeRecipient
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
malformedArgs.extData.recipient = recipient
|
|
malformedArgs.extData.relayer = fakeRelayer
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
malformedArgs.extData.relayer = relayer
|
|
malformedArgs.extData.fee = fakeFee
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
|
|
|
|
const extDataHash = getExtWithdrawArgsHash({
|
|
fee: fakeFee,
|
|
recipient: fakeRecipient,
|
|
relayer: fakeRelayer,
|
|
encryptedAccount: malformedArgs.extData.encryptedAccount,
|
|
})
|
|
malformedArgs.extData.fee = fakeFee
|
|
malformedArgs.extData.relayer = fakeRelayer
|
|
malformedArgs.extData.recipient = fakeRecipient
|
|
malformedArgs.extDataHash = extDataHash
|
|
await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Invalid withdrawal proof')
|
|
|
|
await timeReset()
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(proof, args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
})
|
|
})
|
|
|
|
describe('#batchReward', () => {
|
|
it('should work', async () => {
|
|
let account = new Account()
|
|
const claim = await controller.reward({ account, note, publicKey })
|
|
await miner.reward(claim.proof, claim.args)
|
|
|
|
const { proofs, args } = await controller.batchReward({
|
|
account: claim.account,
|
|
notes: notes.slice(1),
|
|
publicKey,
|
|
})
|
|
await miner.batchReward(args)
|
|
|
|
account = proofs.slice(-1)[0].account
|
|
const amount = toBN(55)
|
|
const rewardSnark = await controller.withdraw({ account, amount, recipient, publicKey })
|
|
await timeReset()
|
|
const balanceBefore = await torn.balanceOf(recipient)
|
|
const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
|
|
await miner.withdraw(rewardSnark.proof, rewardSnark.args)
|
|
const balanceAfter = await torn.balanceOf(recipient)
|
|
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
|
|
})
|
|
})
|
|
|
|
describe('#isKnownAccountRoot', () => {
|
|
it('should work', async () => {
|
|
const claim1 = await controller.reward({ account: new Account(), note: note1, publicKey })
|
|
await miner.reward(claim1.proof, claim1.args)
|
|
|
|
const claim2 = await controller.reward({ account: new Account(), note: note2, publicKey })
|
|
await miner.reward(claim2.proof, claim2.args)
|
|
|
|
const tree = new MerkleTree(levels, [], { hashFunction: poseidonHash2 })
|
|
await miner.isKnownAccountRoot(toFixedHex(tree.root()), 0).should.eventually.be.true
|
|
|
|
tree.insert(claim1.account.commitment)
|
|
await miner.isKnownAccountRoot(toFixedHex(tree.root()), 1).should.eventually.be.true
|
|
|
|
tree.insert(claim2.account.commitment)
|
|
await miner.isKnownAccountRoot(toFixedHex(tree.root()), 2).should.eventually.be.true
|
|
|
|
await miner.isKnownAccountRoot(toFixedHex(tree.root()), 1).should.eventually.be.false
|
|
await miner.isKnownAccountRoot(toFixedHex(tree.root()), 5).should.eventually.be.false
|
|
await miner.isKnownAccountRoot(toFixedHex(1234), 1).should.eventually.be.false
|
|
await miner.isKnownAccountRoot(toFixedHex(0), 0).should.eventually.be.false
|
|
await miner.isKnownAccountRoot(toFixedHex(0), 5).should.eventually.be.false
|
|
})
|
|
})
|
|
|
|
describe('#setRates', () => {
|
|
it('should reject for invalid rates', async () => {
|
|
const bigNum = toBN(2).pow(toBN(128))
|
|
await miner
|
|
.setRates([{ instance: tornado, value: bigNum.toString() }], { from: governance })
|
|
.should.be.rejectedWith('Incorrect rate')
|
|
})
|
|
})
|
|
|
|
describe('#setVerifiers', () => {
|
|
it('onlyGovernance can set new verifiers', async () => {
|
|
const verifiers = [
|
|
'0x0000000000000000000000000000000000000001',
|
|
'0x0000000000000000000000000000000000000002',
|
|
'0x0000000000000000000000000000000000000003',
|
|
]
|
|
await miner.setVerifiers(verifiers).should.be.rejectedWith('Only governance can perform this action')
|
|
await miner.setVerifiers(verifiers, { from: governance })
|
|
|
|
const rewardVerifier = await miner.rewardVerifier()
|
|
rewardVerifier.should.be.equal(verifiers[0])
|
|
const withdrawVerifier = await miner.withdrawVerifier()
|
|
withdrawVerifier.should.be.equal(verifiers[1])
|
|
const treeUpdateVerifier = await miner.treeUpdateVerifier()
|
|
treeUpdateVerifier.should.be.equal(verifiers[2])
|
|
})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await revertSnapshot(snapshotId.result)
|
|
// eslint-disable-next-line require-atomic-updates
|
|
snapshotId = await takeSnapshot()
|
|
})
|
|
})
|