miner.test.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. /* global artifacts, web3, contract */
  2. require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
  3. const fs = require('fs')
  4. const { toBN } = require('web3-utils')
  5. const { takeSnapshot, revertSnapshot, mineBlock } = require('../scripts/ganacheHelper')
  6. const tornConfig = require('torn-token')
  7. const RLP = require('rlp')
  8. const Controller = require('../src/controller')
  9. const Account = require('../src/account')
  10. const Note = require('../src/note')
  11. const {
  12. toFixedHex,
  13. poseidonHash2,
  14. packEncryptedMessage,
  15. unpackEncryptedMessage,
  16. getExtWithdrawArgsHash,
  17. } = require('../src/utils')
  18. const { getEncryptionPublicKey } = require('eth-sig-util')
  19. const Miner = artifacts.require('MinerMock')
  20. const TornadoTrees = artifacts.require('TornadoTreesMock')
  21. const Torn = artifacts.require('TORNMock')
  22. const RewardSwap = artifacts.require('RewardSwapMock')
  23. const RewardVerifier = artifacts.require('RewardVerifier')
  24. const WithdrawVerifier = artifacts.require('WithdrawVerifier')
  25. const TreeUpdateVerifier = artifacts.require('TreeUpdateVerifier')
  26. const provingKeys = {
  27. rewardCircuit: require('../build/circuits/Reward.json'),
  28. withdrawCircuit: require('../build/circuits/Withdraw.json'),
  29. treeUpdateCircuit: require('../build/circuits/TreeUpdate.json'),
  30. rewardProvingKey: fs.readFileSync('./build/circuits/Reward_proving_key.bin').buffer,
  31. withdrawProvingKey: fs.readFileSync('./build/circuits/Withdraw_proving_key.bin').buffer,
  32. treeUpdateProvingKey: fs.readFileSync('./build/circuits/TreeUpdate_proving_key.bin').buffer,
  33. }
  34. const MerkleTree = require('fixed-merkle-tree')
  35. const Hasher2 = artifacts.require('Hasher2')
  36. const Hasher3 = artifacts.require('Hasher3')
  37. // Set time to beginning of a second
  38. async function timeReset() {
  39. const delay = 1000 - new Date().getMilliseconds()
  40. await new Promise((resolve) => setTimeout(resolve, delay))
  41. await mineBlock()
  42. }
  43. async function getNextAddr(sender, offset = 0) {
  44. const nonce = await web3.eth.getTransactionCount(sender)
  45. return (
  46. '0x' +
  47. web3.utils
  48. .sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
  49. .slice(12)
  50. .substring(14)
  51. )
  52. }
  53. async function registerNote(note, tornadoTrees) {
  54. await tornadoTrees.setBlockNumber(note.depositBlock)
  55. await tornadoTrees.registerDeposit(note.instance, toFixedHex(note.commitment))
  56. await tornadoTrees.setBlockNumber(note.withdrawalBlock)
  57. await tornadoTrees.registerWithdrawal(note.instance, toFixedHex(note.nullifierHash))
  58. return {
  59. depositLeaf: {
  60. instance: note.instance,
  61. hash: toFixedHex(note.commitment),
  62. block: toFixedHex(note.depositBlock),
  63. },
  64. withdrawalLeaf: {
  65. instance: note.instance,
  66. hash: toFixedHex(note.nullifierHash),
  67. block: toFixedHex(note.withdrawalBlock),
  68. },
  69. }
  70. }
  71. contract('Miner', (accounts) => {
  72. let miner
  73. let torn
  74. let rewardSwap
  75. let tornadoTrees
  76. const tornado = '0x3535249DFBb73e21c2aCDC6e42796d920A0379b7'
  77. const tornCap = toBN(tornConfig.torn.cap)
  78. const miningCap = toBN(tornConfig.torn.distribution.miningV2.amount)
  79. const initialTornBalance = toBN(tornConfig.miningV2.initialBalance)
  80. const RATE = toBN(10)
  81. const amount = toBN(15)
  82. // eslint-disable-next-line no-unused-vars
  83. const sender = accounts[0]
  84. const recipient = accounts[1]
  85. // eslint-disable-next-line no-unused-vars
  86. const relayer = accounts[2]
  87. const levels = 20
  88. let snapshotId
  89. const AnotherWeb3 = require('web3')
  90. let contract
  91. let controller
  92. const note1 = new Note({
  93. instance: tornado,
  94. depositBlock: 10,
  95. withdrawalBlock: 10 + 4 * 60 * 24,
  96. })
  97. const note2 = new Note({
  98. instance: tornado,
  99. depositBlock: 10,
  100. withdrawalBlock: 10 + 2 * 4 * 60 * 24,
  101. })
  102. const note3 = new Note({
  103. instance: tornado,
  104. depositBlock: 10,
  105. withdrawalBlock: 10 + 3 * 4 * 60 * 24,
  106. })
  107. const note = note1
  108. const notes = [note1, note2, note3]
  109. const emptyTree = new MerkleTree(levels, [], { hashFunction: poseidonHash2 })
  110. const privateKey = web3.eth.accounts.create().privateKey.slice(2)
  111. const publicKey = getEncryptionPublicKey(privateKey)
  112. const operator = accounts[0]
  113. const thirtyDays = 30 * 24 * 3600
  114. const poolWeight = 1e11
  115. const governance = accounts[9]
  116. before(async () => {
  117. const rewardVerifier = await RewardVerifier.new()
  118. const withdrawVerifier = await WithdrawVerifier.new()
  119. const treeUpdateVerifier = await TreeUpdateVerifier.new()
  120. const hasher2 = await Hasher2.new()
  121. const hasher3 = await Hasher3.new()
  122. tornadoTrees = await TornadoTrees.new(operator, hasher2.address, hasher3.address, levels)
  123. const swapExpectedAddr = await getNextAddr(accounts[0], 1)
  124. const minerExpectedAddr = await getNextAddr(accounts[0], 2)
  125. torn = await Torn.new(sender, thirtyDays, [
  126. { to: swapExpectedAddr, amount: miningCap.toString() },
  127. { to: sender, amount: tornCap.sub(miningCap).toString() },
  128. ])
  129. rewardSwap = await RewardSwap.new(
  130. torn.address,
  131. minerExpectedAddr,
  132. miningCap.toString(),
  133. initialTornBalance.toString(),
  134. poolWeight,
  135. )
  136. miner = await Miner.new(
  137. rewardSwap.address,
  138. governance,
  139. tornadoTrees.address,
  140. [rewardVerifier.address, withdrawVerifier.address, treeUpdateVerifier.address],
  141. toFixedHex(emptyTree.root()),
  142. [{ instance: tornado, value: RATE.toString() }],
  143. )
  144. const depositData = []
  145. const withdrawalData = []
  146. for (const note of notes) {
  147. const { depositLeaf, withdrawalLeaf } = await registerNote(note, tornadoTrees)
  148. depositData.push(depositLeaf)
  149. withdrawalData.push(withdrawalLeaf)
  150. }
  151. await tornadoTrees.updateRoots(depositData, withdrawalData)
  152. const anotherWeb3 = new AnotherWeb3(web3.currentProvider)
  153. contract = new anotherWeb3.eth.Contract(miner.abi, miner.address)
  154. const tornadoTreesContract = new anotherWeb3.eth.Contract(tornadoTrees.abi, tornadoTrees.address)
  155. controller = new Controller({
  156. contract,
  157. tornadoTreesContract,
  158. merkleTreeHeight: levels,
  159. provingKeys,
  160. })
  161. await controller.init()
  162. snapshotId = await takeSnapshot()
  163. })
  164. beforeEach(async () => {
  165. await timeReset()
  166. })
  167. describe('#constructor', () => {
  168. it('should initialize', async () => {
  169. const tokenFromContract = await rewardSwap.torn()
  170. tokenFromContract.should.be.equal(torn.address)
  171. const rewardSwapFromContract = await miner.rewardSwap()
  172. rewardSwapFromContract.should.be.equal(rewardSwap.address)
  173. const rateFromContract = await miner.rates(tornado)
  174. rateFromContract.should.be.eq.BN(RATE)
  175. })
  176. })
  177. describe('#Note.fromString()', () => {
  178. it('should work', () => {
  179. const note = Note.fromString(
  180. 'tornado-eth-1-1-0x3a1f1e0e10b22b15ed8208bc810dd5564d564fd7930874db4d7d58870deb72978fb5fccae2f1554cac71e2cff85f9c8908295647adf2b443a4dd93635d8d',
  181. '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
  182. 10,
  183. 15,
  184. )
  185. note.secret.should.be.eq.BN(toBN('0x8d5d6393dda443b4f2ad47562908899c5ff8cfe271ac4c55f1e2cafcb58f97'))
  186. note.nullifier.should.be.eq.BN(toBN('0x72eb0d87587d4ddb740893d74f564d56d50d81bc0882ed152bb2100e1e1f3a'))
  187. note.nullifierHash.should.be.eq.BN(
  188. toBN('0x33403728f37e70a275acac2eb8297a3231698f9003838ec4cd7115ee2693943'),
  189. )
  190. note.commitment.should.be.eq.BN(
  191. toBN('0x1a08fd10dae9806ce25b62582e44d237c1cb9f8d6bf73e756f18d0e5d7a7351a'),
  192. )
  193. })
  194. })
  195. describe('#Account', () => {
  196. it('should throw on negative amount', () => {
  197. ;(() => new Account({ amount: toBN(-1) })).should.throw('Cannot create an account with negative amount')
  198. })
  199. })
  200. describe('#encrypt', () => {
  201. it('should work', () => {
  202. const account = new Account()
  203. const encryptedAccount = account.encrypt(publicKey)
  204. const encryptedMessage = packEncryptedMessage(encryptedAccount)
  205. const unpackedMessage = unpackEncryptedMessage(encryptedMessage)
  206. const account2 = Account.decrypt(privateKey, unpackedMessage)
  207. account.amount.should.be.eq.BN(account2.amount)
  208. account.secret.should.be.eq.BN(account2.secret)
  209. account.nullifier.should.be.eq.BN(account2.nullifier)
  210. account.commitment.should.be.eq.BN(account2.commitment)
  211. })
  212. })
  213. describe('#reward', () => {
  214. it('should work', async () => {
  215. const zeroAccount = new Account()
  216. const accountCount = await miner.accountCount()
  217. zeroAccount.amount.should.be.eq.BN(toBN(0))
  218. const rewardNullifierBefore = await miner.rewardNullifiers(toFixedHex(note.rewardNullifier))
  219. rewardNullifierBefore.should.be.false
  220. const accountNullifierBefore = await miner.accountNullifiers(toFixedHex(zeroAccount.nullifier))
  221. accountNullifierBefore.should.be.false
  222. const { proof, args, account } = await controller.reward({ account: zeroAccount, note, publicKey })
  223. const { logs } = await miner.reward(proof, args)
  224. logs[0].event.should.be.equal('NewAccount')
  225. logs[0].args.commitment.should.be.equal(toFixedHex(account.commitment))
  226. logs[0].args.index.should.be.eq.BN(accountCount)
  227. logs[0].args.nullifier.should.be.equal(toFixedHex(zeroAccount.nullifierHash))
  228. const encryptedAccount = logs[0].args.encryptedAccount
  229. const account2 = Account.decrypt(privateKey, unpackEncryptedMessage(encryptedAccount))
  230. account.amount.should.be.eq.BN(account2.amount)
  231. account.secret.should.be.eq.BN(account2.secret)
  232. account.nullifier.should.be.eq.BN(account2.nullifier)
  233. account.commitment.should.be.eq.BN(account2.commitment)
  234. const accountCountAfter = await miner.accountCount()
  235. accountCountAfter.should.be.eq.BN(accountCount.add(toBN(1)))
  236. const rootAfter = await miner.getLastAccountRoot()
  237. rootAfter.should.be.equal(args.account.outputRoot)
  238. const rewardNullifierAfter = await miner.rewardNullifiers(toFixedHex(note.rewardNullifier))
  239. rewardNullifierAfter.should.be.true
  240. const accountNullifierAfter = await miner.accountNullifiers(toFixedHex(zeroAccount.nullifierHash))
  241. accountNullifierAfter.should.be.true
  242. account.amount.should.be.eq.BN(toBN(note.withdrawalBlock - note.depositBlock).mul(RATE))
  243. })
  244. it('should send fee to relayer', async () => {
  245. const fee = toBN(3)
  246. const amount = toBN(44)
  247. const delta = toBN('10000') // max floating point error
  248. const claim = await controller.reward({ account: new Account(), note, publicKey, relayer, fee })
  249. await timeReset()
  250. let expectedFeeInTorn = await rewardSwap.getExpectedReturn(fee)
  251. let relayerBalanceBefore = await torn.balanceOf(relayer)
  252. await miner.reward(claim.proof, claim.args)
  253. let relayerBalanceAfter = await torn.balanceOf(relayer)
  254. relayerBalanceAfter.should.be.eq.BN(relayerBalanceBefore.add(expectedFeeInTorn))
  255. const withdrawal = await controller.withdraw({
  256. account: claim.account,
  257. amount,
  258. recipient,
  259. publicKey,
  260. relayer,
  261. fee,
  262. })
  263. await timeReset()
  264. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  265. expectedFeeInTorn = await rewardSwap.getExpectedReturn(amount.add(fee))
  266. expectedFeeInTorn = expectedFeeInTorn.sub(expectedAmountInTorn)
  267. relayerBalanceBefore = await torn.balanceOf(relayer)
  268. const recipientBalanceBefore = await torn.balanceOf(recipient)
  269. await miner.withdraw(withdrawal.proof, withdrawal.args)
  270. const recipientBalanceAfter = await torn.balanceOf(recipient)
  271. relayerBalanceAfter = await torn.balanceOf(relayer)
  272. recipientBalanceAfter.should.be.eq.BN(recipientBalanceBefore.add(expectedAmountInTorn))
  273. relayerBalanceAfter.sub(relayerBalanceBefore).sub(expectedFeeInTorn).should.be.lt.BN(delta)
  274. })
  275. it('should use fallback with outdated tree', async () => {
  276. const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
  277. const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
  278. await miner.reward(tmp.proof, tmp.args)
  279. await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
  280. const update = await controller.treeUpdate(account.commitment)
  281. await miner.reward(proof, args, update.proof, update.args)
  282. const rootAfter = await miner.getLastAccountRoot()
  283. rootAfter.should.be.equal(update.args.newRoot)
  284. })
  285. it('should reject with incorrect insert position', async () => {
  286. const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
  287. await miner.reward(tmp.proof, tmp.args)
  288. const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
  289. const malformedArgs = JSON.parse(JSON.stringify(args))
  290. let fakeIndex = toBN(args.account.outputPathIndices).sub(toBN('1'))
  291. malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
  292. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
  293. fakeIndex = toBN(args.account.outputPathIndices).add(toBN('1'))
  294. malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
  295. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
  296. fakeIndex = toBN(args.account.outputPathIndices).add(toBN('10000000000000000000000000'))
  297. malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
  298. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
  299. await miner.reward(proof, args).should.be.fulfilled
  300. })
  301. it('should reject with incorrect external data hash', async () => {
  302. const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
  303. const malformedArgs = JSON.parse(JSON.stringify(args))
  304. malformedArgs.extDataHash = toFixedHex('0xdeadbeef')
  305. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  306. malformedArgs.extDataHash = toFixedHex('0x00')
  307. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  308. await miner.reward(proof, args).should.be.fulfilled
  309. })
  310. it('should prevent fee overflow', async () => {
  311. const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
  312. const malformedArgs = JSON.parse(JSON.stringify(args))
  313. malformedArgs.fee = toFixedHex(toBN(2).pow(toBN(248)))
  314. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Fee value out of range')
  315. malformedArgs.fee = toFixedHex(toBN(2).pow(toBN(256)).sub(toBN(1)))
  316. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Fee value out of range')
  317. await miner.reward(proof, args).should.be.fulfilled
  318. })
  319. it('should reject with invalid reward rate', async () => {
  320. const { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
  321. const malformedArgs = JSON.parse(JSON.stringify(args))
  322. malformedArgs.instance = miner.address
  323. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward rate')
  324. malformedArgs.rate = toFixedHex(toBN(9999999))
  325. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward rate')
  326. malformedArgs.instance = toFixedHex('0x00', 20)
  327. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward rate')
  328. const anotherInstance = accounts[5]
  329. const rate = toBN(1000)
  330. await miner.setRates([{ instance: anotherInstance, value: rate.toString() }], { from: governance })
  331. malformedArgs.instance = anotherInstance
  332. malformedArgs.rate = toFixedHex(rate)
  333. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid reward proof')
  334. await miner.reward(proof, args).should.be.fulfilled
  335. })
  336. it('should reject for double spend', async () => {
  337. let { proof, args } = await controller.reward({ account: new Account(), note, publicKey })
  338. await miner.reward(proof, args).should.be.fulfilled
  339. ;({ proof, args } = await controller.reward({ account: new Account(), note, publicKey }))
  340. await miner.reward(proof, args).should.be.rejectedWith('Reward has been already spent')
  341. })
  342. it('should reject for invalid proof', async () => {
  343. const claim1 = await controller.reward({ account: new Account(), note, publicKey })
  344. const claim2 = await controller.reward({ account: new Account(), note: note2, publicKey })
  345. await miner.reward(claim2.proof, claim1.args).should.be.rejectedWith('Invalid reward proof')
  346. })
  347. it('should reject for invalid account root', async () => {
  348. const account1 = new Account()
  349. const account2 = new Account()
  350. const account3 = new Account()
  351. const fakeTree = new MerkleTree(
  352. levels,
  353. [account1.commitment, account2.commitment, account3.commitment],
  354. { hashFunction: poseidonHash2 },
  355. )
  356. const { proof, args } = await controller.reward({ account: account1, note, publicKey })
  357. const malformedArgs = JSON.parse(JSON.stringify(args))
  358. malformedArgs.account.inputRoot = toFixedHex(fakeTree.root())
  359. await miner.reward(proof, malformedArgs).should.be.rejectedWith('Invalid account root')
  360. })
  361. it('should reject with outdated account root (treeUpdate proof validation)', async () => {
  362. const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
  363. const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
  364. await miner.reward(tmp.proof, tmp.args)
  365. await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
  366. const update = await controller.treeUpdate(account.commitment)
  367. const tmp2 = await controller.reward({ account: new Account(), note: note3, publicKey })
  368. await miner.reward(tmp2.proof, tmp2.args)
  369. await miner
  370. .reward(proof, args, update.proof, update.args)
  371. .should.be.rejectedWith('Outdated tree update merkle root')
  372. })
  373. it('should reject for incorrect commitment (treeUpdate proof validation)', async () => {
  374. const claim = await controller.reward({ account: new Account(), note, publicKey })
  375. const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
  376. await miner.reward(tmp.proof, tmp.args)
  377. await miner.reward(claim.proof, claim.args).should.be.rejectedWith('Outdated account merkle root')
  378. const anotherAccount = new Account()
  379. const update = await controller.treeUpdate(anotherAccount.commitment)
  380. await miner
  381. .reward(claim.proof, claim.args, update.proof, update.args)
  382. .should.be.rejectedWith('Incorrect commitment inserted')
  383. claim.args.account.outputCommitment = update.args.leaf
  384. await miner
  385. .reward(claim.proof, claim.args, update.proof, update.args)
  386. .should.be.rejectedWith('Invalid reward proof')
  387. })
  388. it('should reject for incorrect account insert index (treeUpdate proof validation)', async () => {
  389. const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
  390. const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
  391. await miner.reward(tmp.proof, tmp.args)
  392. await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
  393. const update = await controller.treeUpdate(account.commitment)
  394. const malformedArgs = JSON.parse(JSON.stringify(update.args))
  395. let fakeIndex = toBN(update.args.pathIndices).sub(toBN('1'))
  396. malformedArgs.pathIndices = toFixedHex(fakeIndex)
  397. await miner
  398. .reward(proof, args, update.proof, malformedArgs)
  399. .should.be.rejectedWith('Incorrect account insert index')
  400. })
  401. it('should reject for invalid tree update proof (treeUpdate proof validation)', async () => {
  402. const { proof, args, account } = await controller.reward({ account: new Account(), note, publicKey })
  403. const tmp = await controller.reward({ account: new Account(), note: note2, publicKey })
  404. await miner.reward(tmp.proof, tmp.args)
  405. await miner.reward(proof, args).should.be.rejectedWith('Outdated account merkle root')
  406. const update = await controller.treeUpdate(account.commitment)
  407. await miner
  408. .reward(proof, args, tmp.proof, update.args)
  409. .should.be.rejectedWith('Invalid tree update proof')
  410. })
  411. it('should work with outdated deposit or withdrawal merkle root', async () => {
  412. const note0 = new Note({
  413. instance: tornado,
  414. depositBlock: 10,
  415. withdrawalBlock: 55,
  416. })
  417. const note4 = new Note({
  418. instance: tornado,
  419. depositBlock: 10,
  420. withdrawalBlock: 55,
  421. })
  422. const note5 = new Note({
  423. instance: tornado,
  424. depositBlock: 10,
  425. withdrawalBlock: 65,
  426. })
  427. const claim1 = await controller.reward({ account: new Account(), note: note3, publicKey })
  428. const note4Leaves = await registerNote(note4, tornadoTrees)
  429. await tornadoTrees.updateRoots([note4Leaves.depositLeaf], [note4Leaves.withdrawalLeaf])
  430. const claim2 = await controller.reward({ account: new Account(), note: note4, publicKey })
  431. for (let i = 0; i < 9; i++) {
  432. const note0Leaves = await registerNote(note0, tornadoTrees)
  433. await tornadoTrees.updateRoots([note0Leaves.depositLeaf], [note0Leaves.withdrawalLeaf])
  434. }
  435. await miner.reward(claim1.proof, claim1.args).should.be.rejectedWith('Incorrect deposit tree root')
  436. await miner.reward(claim2.proof, claim2.args).should.be.fulfilled
  437. const note5Leaves = await registerNote(note5, tornadoTrees)
  438. await tornadoTrees.updateRoots([note5Leaves.depositLeaf], [note5Leaves.withdrawalLeaf])
  439. const claim3 = await controller.reward({ account: new Account(), note: note5, publicKey })
  440. await miner.reward(claim3.proof, claim3.args).should.be.fulfilled
  441. })
  442. })
  443. describe('#withdraw', () => {
  444. let proof, args, account
  445. // prettier-ignore
  446. beforeEach(async () => {
  447. ({ proof, args, account } = await controller.reward({ account: new Account(), note, publicKey }))
  448. await miner.reward(proof, args)
  449. })
  450. it('should work', async () => {
  451. const accountNullifierBefore = await miner.accountNullifiers(toFixedHex(account.nullifierHash))
  452. accountNullifierBefore.should.be.false
  453. const accountCount = await miner.accountCount()
  454. const withdrawSnark = await controller.withdraw({ account, amount, recipient, publicKey })
  455. await timeReset()
  456. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  457. const balanceBefore = await torn.balanceOf(recipient)
  458. const { logs } = await miner.withdraw(withdrawSnark.proof, withdrawSnark.args)
  459. const balanceAfter = await torn.balanceOf(recipient)
  460. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  461. const accountCountAfter = await miner.accountCount()
  462. accountCountAfter.should.be.eq.BN(accountCount.add(toBN(1)))
  463. const rootAfter = await miner.getLastAccountRoot()
  464. rootAfter.should.be.equal(withdrawSnark.args.account.outputRoot)
  465. const accountNullifierAfter = await miner.accountNullifiers(toFixedHex(account.nullifierHash))
  466. accountNullifierAfter.should.be.true
  467. logs[0].event.should.be.equal('NewAccount')
  468. logs[0].args.commitment.should.be.equal(toFixedHex(withdrawSnark.account.commitment))
  469. logs[0].args.index.should.be.eq.BN(accountCount)
  470. logs[0].args.nullifier.should.be.equal(toFixedHex(account.nullifierHash))
  471. const encryptedAccount = logs[0].args.encryptedAccount
  472. const account2 = Account.decrypt(privateKey, unpackEncryptedMessage(encryptedAccount))
  473. withdrawSnark.account.amount.should.be.eq.BN(account2.amount)
  474. withdrawSnark.account.secret.should.be.eq.BN(account2.secret)
  475. withdrawSnark.account.nullifier.should.be.eq.BN(account2.nullifier)
  476. withdrawSnark.account.commitment.should.be.eq.BN(account2.commitment)
  477. })
  478. it('should reject for double spend', async () => {
  479. const withdrawSnark = await controller.withdraw({ account, amount, recipient, publicKey })
  480. await timeReset()
  481. const balanceBefore = await torn.balanceOf(recipient)
  482. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  483. await miner.withdraw(withdrawSnark.proof, withdrawSnark.args)
  484. const balanceAfter = await torn.balanceOf(recipient)
  485. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  486. await miner
  487. .withdraw(withdrawSnark.proof, withdrawSnark.args)
  488. .should.be.rejectedWith('Outdated account state')
  489. })
  490. it('should reject with incorrect insert position', async () => {
  491. const { proof, args } = await controller.withdraw({ account, amount, recipient, publicKey })
  492. const malformedArgs = JSON.parse(JSON.stringify(args))
  493. let fakeIndex = toBN(args.account.outputPathIndices).sub(toBN('1'))
  494. malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
  495. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
  496. fakeIndex = toBN(args.account.outputPathIndices).add(toBN('1'))
  497. malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
  498. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
  499. fakeIndex = toBN(args.account.outputPathIndices).add(toBN('10000000000000000000000000'))
  500. malformedArgs.account.outputPathIndices = toFixedHex(fakeIndex)
  501. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect account insert index')
  502. const balanceBefore = await torn.balanceOf(recipient)
  503. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  504. await miner.withdraw(proof, args)
  505. const balanceAfter = await torn.balanceOf(recipient)
  506. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  507. })
  508. it('should reject with incorrect external data hash', async () => {
  509. const { proof, args } = await controller.withdraw({ account, amount, recipient, publicKey })
  510. const malformedArgs = JSON.parse(JSON.stringify(args))
  511. malformedArgs.extDataHash = toFixedHex('0xdeadbeef')
  512. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  513. malformedArgs.extDataHash = toFixedHex('0x00')
  514. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  515. const balanceBefore = await torn.balanceOf(recipient)
  516. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  517. await miner.withdraw(proof, args)
  518. const balanceAfter = await torn.balanceOf(recipient)
  519. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  520. })
  521. it('should reject for amount overflow', async () => {
  522. const { proof, args } = await controller.withdraw({ account, amount, recipient, publicKey })
  523. const malformedArgs = JSON.parse(JSON.stringify(args))
  524. malformedArgs.amount = toFixedHex(toBN(2).pow(toBN(248)))
  525. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Amount value out of range')
  526. malformedArgs.amount = toFixedHex(toBN(2).pow(toBN(256)).sub(toBN(1)))
  527. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Amount value out of range')
  528. const balanceBefore = await torn.balanceOf(recipient)
  529. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  530. await miner.withdraw(proof, args)
  531. const balanceAfter = await torn.balanceOf(recipient)
  532. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  533. })
  534. it('should reject for fee overflow', async () => {
  535. const fee = account.amount.add(toBN(5))
  536. const fakeAmount = toBN(-5)
  537. const { proof, args } = await controller.withdraw({
  538. account,
  539. amount: fakeAmount,
  540. recipient,
  541. publicKey,
  542. fee,
  543. })
  544. await miner.withdraw(proof, args).should.be.rejectedWith('Amount should be greater than fee')
  545. })
  546. it('should reject for unfair amount', async () => {
  547. const fee = toBN(3)
  548. const amountToWithdraw = amount.sub(fee)
  549. const { proof, args } = await controller.withdraw({
  550. account,
  551. amount: amountToWithdraw,
  552. recipient,
  553. publicKey,
  554. })
  555. const malformedArgs = JSON.parse(JSON.stringify(args))
  556. malformedArgs.amount = toFixedHex(amountToWithdraw.add(amountToWithdraw))
  557. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Invalid withdrawal proof')
  558. await timeReset()
  559. const balanceBefore = await torn.balanceOf(recipient)
  560. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amountToWithdraw)
  561. await miner.withdraw(proof, args)
  562. const balanceAfter = await torn.balanceOf(recipient)
  563. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  564. })
  565. it('can use fallback with outdated tree', async () => {
  566. const tmpReward = await controller.reward({ account: new Account(), note: note2, publicKey })
  567. await miner.reward(tmpReward.proof, tmpReward.args)
  568. const withdrawal = await controller.withdraw({ account, amount, recipient, publicKey })
  569. const tmpWithdraw = await controller.withdraw({
  570. account: tmpReward.account,
  571. amount,
  572. recipient,
  573. publicKey,
  574. })
  575. await miner.withdraw(tmpWithdraw.proof, tmpWithdraw.args)
  576. await miner
  577. .withdraw(withdrawal.proof, withdrawal.args)
  578. .should.be.rejectedWith('Outdated account merkle root')
  579. const update = await controller.treeUpdate(withdrawal.account.commitment)
  580. await timeReset()
  581. const balanceBefore = await torn.balanceOf(recipient)
  582. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  583. await miner.withdraw(withdrawal.proof, withdrawal.args, update.proof, update.args)
  584. const balanceAfter = await torn.balanceOf(recipient)
  585. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  586. const rootAfter = await miner.getLastAccountRoot()
  587. rootAfter.should.be.equal(update.args.newRoot)
  588. })
  589. it('should reject for invalid proof', async () => {
  590. const tmpReward = await controller.reward({ account: new Account(), note: note2, publicKey })
  591. await miner.reward(tmpReward.proof, tmpReward.args)
  592. const withdrawal = await controller.withdraw({ account, amount, recipient, publicKey })
  593. const tmpWithdraw = await controller.withdraw({
  594. account: tmpReward.account,
  595. amount,
  596. recipient,
  597. publicKey,
  598. })
  599. await miner
  600. .withdraw(tmpWithdraw.proof, withdrawal.args)
  601. .should.be.rejectedWith('Invalid withdrawal proof')
  602. })
  603. it('should reject for malformed relayer and recipient address and fee', async () => {
  604. const fakeRelayer = accounts[6]
  605. const fakeRecipient = accounts[7]
  606. const fee = 12
  607. const fakeFee = 123
  608. const { proof, args } = await controller.withdraw({
  609. account,
  610. amount,
  611. recipient,
  612. publicKey,
  613. fee,
  614. relayer,
  615. })
  616. const malformedArgs = JSON.parse(JSON.stringify(args))
  617. malformedArgs.extData.recipient = fakeRecipient
  618. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  619. malformedArgs.extData.recipient = recipient
  620. malformedArgs.extData.relayer = fakeRelayer
  621. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  622. malformedArgs.extData.relayer = relayer
  623. malformedArgs.extData.fee = fakeFee
  624. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Incorrect external data hash')
  625. const extDataHash = getExtWithdrawArgsHash({
  626. fee: fakeFee,
  627. recipient: fakeRecipient,
  628. relayer: fakeRelayer,
  629. encryptedAccount: malformedArgs.extData.encryptedAccount,
  630. })
  631. malformedArgs.extData.fee = fakeFee
  632. malformedArgs.extData.relayer = fakeRelayer
  633. malformedArgs.extData.recipient = fakeRecipient
  634. malformedArgs.extDataHash = extDataHash
  635. await miner.withdraw(proof, malformedArgs).should.be.rejectedWith('Invalid withdrawal proof')
  636. await timeReset()
  637. const balanceBefore = await torn.balanceOf(recipient)
  638. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  639. await miner.withdraw(proof, args)
  640. const balanceAfter = await torn.balanceOf(recipient)
  641. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  642. })
  643. })
  644. describe('#batchReward', () => {
  645. it('should work', async () => {
  646. let account = new Account()
  647. const claim = await controller.reward({ account, note, publicKey })
  648. await miner.reward(claim.proof, claim.args)
  649. const { proofs, args } = await controller.batchReward({
  650. account: claim.account,
  651. notes: notes.slice(1),
  652. publicKey,
  653. })
  654. await miner.batchReward(args)
  655. account = proofs.slice(-1)[0].account
  656. const amount = toBN(55)
  657. const rewardSnark = await controller.withdraw({ account, amount, recipient, publicKey })
  658. await timeReset()
  659. const balanceBefore = await torn.balanceOf(recipient)
  660. const expectedAmountInTorn = await rewardSwap.getExpectedReturn(amount)
  661. await miner.withdraw(rewardSnark.proof, rewardSnark.args)
  662. const balanceAfter = await torn.balanceOf(recipient)
  663. balanceAfter.should.be.eq.BN(balanceBefore.add(expectedAmountInTorn))
  664. })
  665. })
  666. describe('#isKnownAccountRoot', () => {
  667. it('should work', async () => {
  668. const claim1 = await controller.reward({ account: new Account(), note: note1, publicKey })
  669. await miner.reward(claim1.proof, claim1.args)
  670. const claim2 = await controller.reward({ account: new Account(), note: note2, publicKey })
  671. await miner.reward(claim2.proof, claim2.args)
  672. const tree = new MerkleTree(levels, [], { hashFunction: poseidonHash2 })
  673. await miner.isKnownAccountRoot(toFixedHex(tree.root()), 0).should.eventually.be.true
  674. tree.insert(claim1.account.commitment)
  675. await miner.isKnownAccountRoot(toFixedHex(tree.root()), 1).should.eventually.be.true
  676. tree.insert(claim2.account.commitment)
  677. await miner.isKnownAccountRoot(toFixedHex(tree.root()), 2).should.eventually.be.true
  678. await miner.isKnownAccountRoot(toFixedHex(tree.root()), 1).should.eventually.be.false
  679. await miner.isKnownAccountRoot(toFixedHex(tree.root()), 5).should.eventually.be.false
  680. await miner.isKnownAccountRoot(toFixedHex(1234), 1).should.eventually.be.false
  681. await miner.isKnownAccountRoot(toFixedHex(0), 0).should.eventually.be.false
  682. await miner.isKnownAccountRoot(toFixedHex(0), 5).should.eventually.be.false
  683. })
  684. })
  685. describe('#setRates', () => {
  686. it('should reject for invalid rates', async () => {
  687. const bigNum = toBN(2).pow(toBN(128))
  688. await miner
  689. .setRates([{ instance: tornado, value: bigNum.toString() }], { from: governance })
  690. .should.be.rejectedWith('Incorrect rate')
  691. })
  692. })
  693. describe('#setVerifiers', () => {
  694. it('onlyGovernance can set new verifiers', async () => {
  695. const verifiers = [
  696. '0x0000000000000000000000000000000000000001',
  697. '0x0000000000000000000000000000000000000002',
  698. '0x0000000000000000000000000000000000000003',
  699. ]
  700. await miner.setVerifiers(verifiers).should.be.rejectedWith('Only governance can perform this action')
  701. await miner.setVerifiers(verifiers, { from: governance })
  702. const rewardVerifier = await miner.rewardVerifier()
  703. rewardVerifier.should.be.equal(verifiers[0])
  704. const withdrawVerifier = await miner.withdrawVerifier()
  705. withdrawVerifier.should.be.equal(verifiers[1])
  706. const treeUpdateVerifier = await miner.treeUpdateVerifier()
  707. treeUpdateVerifier.should.be.equal(verifiers[2])
  708. })
  709. })
  710. afterEach(async () => {
  711. await revertSnapshot(snapshotId.result)
  712. // eslint-disable-next-line require-atomic-updates
  713. snapshotId = await takeSnapshot()
  714. })
  715. })