miner.test.js 35 KB

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