mirror of
https://github.com/tornadocash/tornado-anonymity-mining.git
synced 2024-11-21 17:27:08 +01:00
initial
This commit is contained in:
commit
080d0f8366
2
.codecov.yml
Normal file
2
.codecov.yml
Normal file
@ -0,0 +1,2 @@
|
||||
coverage:
|
||||
range: '100...100'
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
5
.env.example
Normal file
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
||||
MERKLE_TREE_HEIGHT=20
|
||||
PRIVATE_KEY=
|
||||
INFURA_KEY=97c8bf358b9942a9853fab1ba93dc5b3
|
||||
TORN=
|
||||
GOVERNANCE=
|
27
.eslintrc
Normal file
27
.eslintrc
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"require-await": "error",
|
||||
"prettier/prettier": ["error", { "printWidth": 110 }]
|
||||
}
|
||||
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sol linguist-language=Solidity
|
85
.github/workflows/build.yml
vendored
Normal file
85
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
# - uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: 12
|
||||
# - run: yarn install
|
||||
# # Disabled until the repo is public because of performance issues
|
||||
# # - run: yarn test
|
||||
# - run: yarn lint
|
||||
# - name: Telegram Failure Notification
|
||||
# uses: appleboy/telegram-action@0.0.7
|
||||
# if: failure()
|
||||
# with:
|
||||
# message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
# format: markdown
|
||||
# to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
# token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
#
|
||||
# publish:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: build
|
||||
# if: startsWith(github.ref, 'refs/tags')
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v2
|
||||
#
|
||||
# - name: Install dependencies
|
||||
# run: yarn install
|
||||
#
|
||||
# - name: NPM login
|
||||
# # NPM doesn't understand env vars and needs auth file lol
|
||||
# run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
# env:
|
||||
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
#
|
||||
# - name: Set vars
|
||||
# id: vars
|
||||
# run: |
|
||||
# echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
# echo "::set-output name=repo_name::$(echo ${GITHUB_REPOSITORY#*/})"
|
||||
#
|
||||
# - name: Check package.json version vs tag
|
||||
# run: |
|
||||
# [ ${{ steps.vars.outputs.version }} = $(grep '"version":' package.json | grep -o "[0-9.]*") ] || (echo "Git tag doesn't match version in package.json" && false)
|
||||
#
|
||||
# - name: Publish to npm
|
||||
# run: npm publish
|
||||
#
|
||||
# - name: Create GitHub Release Draft
|
||||
# uses: actions/create-release@v1
|
||||
# with:
|
||||
# tag_name: ${{ github.ref }}
|
||||
# release_name: Release ${{ steps.vars.outputs.version }}
|
||||
# draft: true
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
#
|
||||
# - name: Telegram Notification
|
||||
# uses: appleboy/telegram-action@0.0.7
|
||||
# with:
|
||||
# message: 🚀 Published a [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) version [${{ steps.vars.outputs.version }}](https://hub.docker.com/repository/docker/${{ github.repository }}) to docker hub
|
||||
# format: markdown
|
||||
# to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
# token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
#
|
||||
# - name: Telegram Failure Notification
|
||||
# uses: appleboy/telegram-action@0.0.7
|
||||
# if: failure()
|
||||
# with:
|
||||
# message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) because of ${{ env.GITHUB_ACTOR }}
|
||||
# format: markdown
|
||||
# to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
# token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
types
|
||||
coverage
|
||||
coverage.json
|
||||
.secret
|
||||
.infura
|
||||
.DS_Store
|
||||
build
|
||||
circuits/*.json
|
||||
circuits/*.bin
|
||||
.env
|
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@ -0,0 +1,8 @@
|
||||
.vscode
|
||||
build
|
||||
circuits
|
||||
scripts
|
||||
contracts/RewardVerifier.sol
|
||||
contracts/WithdrawVerifier.sol
|
||||
contracts/TreeUpdateVerifier.sol
|
||||
contracts/FloatMath.sol
|
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"printWidth": 110,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"printWidth": 130
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
5
.solcover.js
Normal file
5
.solcover.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
copyPackages: ['@openzeppelin/contracts'],
|
||||
testrpcOptions: '-d --accounts 10 --port 8555',
|
||||
skipFiles: ['Migrations.sol'],
|
||||
}
|
13
.solhint.json
Normal file
13
.solhint.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "solhint:recommended",
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 110
|
||||
}
|
||||
],
|
||||
"quotes": ["error", "double"]
|
||||
},
|
||||
"plugins": ["prettier"]
|
||||
}
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Truffle
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Tornado.cash anonymity mining [![Build Status](https://github.com/tornadocash/tornado-anonymity-mining/workflows/build/badge.svg)](https://github.com/tornadocash/tornado-anonymity-mining/actions)
|
||||
|
||||
## Dependencies
|
||||
|
||||
1. node 12
|
||||
2. yarn
|
||||
3. zkutil (`brew install rust && cargo install zkutil`)
|
||||
|
||||
## Start
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
$ cp .env.example .env
|
||||
$ yarn circuit
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
Deploy to Kovan:
|
||||
|
||||
```bash
|
||||
$ yarn deploy:kovan
|
||||
```
|
71
circuits/MerkleTree.circom
Normal file
71
circuits/MerkleTree.circom
Normal file
@ -0,0 +1,71 @@
|
||||
include "../node_modules/circomlib/circuits/poseidon.circom";
|
||||
include "../node_modules/circomlib/circuits/bitify.circom";
|
||||
|
||||
// Computes Poseidon([left, right])
|
||||
template HashLeftRight() {
|
||||
signal input left;
|
||||
signal input right;
|
||||
signal output hash;
|
||||
|
||||
component hasher = Poseidon(2);
|
||||
hasher.inputs[0] <== left;
|
||||
hasher.inputs[1] <== right;
|
||||
hash <== hasher.out;
|
||||
}
|
||||
|
||||
// if s == 0 returns [in[0], in[1]]
|
||||
// if s == 1 returns [in[1], in[0]]
|
||||
template DualMux() {
|
||||
signal input in[2];
|
||||
signal input s;
|
||||
signal output out[2];
|
||||
|
||||
s * (1 - s) === 0;
|
||||
out[0] <== (in[1] - in[0])*s + in[0];
|
||||
out[1] <== (in[0] - in[1])*s + in[1];
|
||||
}
|
||||
|
||||
// Verifies that merkle proof is correct for given merkle root and a leaf
|
||||
// pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
|
||||
template RawMerkleTree(levels) {
|
||||
signal input leaf;
|
||||
signal input pathElements[levels];
|
||||
signal input pathIndices[levels];
|
||||
|
||||
signal output root;
|
||||
|
||||
component selectors[levels];
|
||||
component hashers[levels];
|
||||
|
||||
for (var i = 0; i < levels; i++) {
|
||||
selectors[i] = DualMux();
|
||||
selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
|
||||
selectors[i].in[1] <== pathElements[i];
|
||||
selectors[i].s <== pathIndices[i];
|
||||
|
||||
hashers[i] = HashLeftRight();
|
||||
hashers[i].left <== selectors[i].out[0];
|
||||
hashers[i].right <== selectors[i].out[1];
|
||||
}
|
||||
|
||||
root <== hashers[levels - 1].hash;
|
||||
}
|
||||
|
||||
template MerkleTree(levels) {
|
||||
signal input leaf;
|
||||
signal input pathElements[levels];
|
||||
signal input pathIndices;
|
||||
signal output root;
|
||||
|
||||
component indexBits = Num2Bits(levels);
|
||||
indexBits.in <== pathIndices;
|
||||
|
||||
component tree = RawMerkleTree(levels)
|
||||
tree.leaf <== leaf;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
tree.pathIndices[i] <== indexBits.out[i];
|
||||
tree.pathElements[i] <== pathElements[i];
|
||||
}
|
||||
|
||||
root <== tree.root
|
||||
}
|
33
circuits/MerkleTreeUpdater.circom
Normal file
33
circuits/MerkleTreeUpdater.circom
Normal file
@ -0,0 +1,33 @@
|
||||
include "./MerkleTree.circom";
|
||||
|
||||
// inserts a leaf into a tree
|
||||
// checks that tree previously contained zero in the same position
|
||||
template MerkleTreeUpdater(levels, zeroLeaf) {
|
||||
signal input oldRoot;
|
||||
signal input newRoot;
|
||||
signal input leaf;
|
||||
signal input pathIndices;
|
||||
signal private input pathElements[levels];
|
||||
|
||||
// Compute indexBits once for both trees
|
||||
// Since Num2Bits is non deterministic, 2 duplicate calls to it cannot be
|
||||
// optimized by circom compiler
|
||||
component indexBits = Num2Bits(levels);
|
||||
indexBits.in <== pathIndices;
|
||||
|
||||
component treeBefore = RawMerkleTree(levels);
|
||||
for(var i = 0; i < levels; i++) {
|
||||
treeBefore.pathIndices[i] <== indexBits.out[i];
|
||||
treeBefore.pathElements[i] <== pathElements[i];
|
||||
}
|
||||
treeBefore.leaf <== zeroLeaf;
|
||||
treeBefore.root === oldRoot;
|
||||
|
||||
component treeAfter = RawMerkleTree(levels);
|
||||
for(var i = 0; i < levels; i++) {
|
||||
treeAfter.pathIndices[i] <== indexBits.out[i];
|
||||
treeAfter.pathElements[i] <== pathElements[i];
|
||||
}
|
||||
treeAfter.leaf <== leaf;
|
||||
treeAfter.root === newRoot;
|
||||
}
|
150
circuits/Reward.circom
Normal file
150
circuits/Reward.circom
Normal file
@ -0,0 +1,150 @@
|
||||
include "../node_modules/circomlib/circuits/poseidon.circom";
|
||||
include "../node_modules/circomlib/circuits/bitify.circom";
|
||||
include "../node_modules/circomlib/circuits/comparators.circom";
|
||||
include "./Utils.circom";
|
||||
include "./MerkleTree.circom";
|
||||
include "./MerkleTreeUpdater.circom";
|
||||
|
||||
template Reward(levels, zeroLeaf) {
|
||||
signal input rate;
|
||||
signal input fee;
|
||||
signal input instance;
|
||||
signal input rewardNullifier;
|
||||
signal input extDataHash;
|
||||
|
||||
signal private input noteSecret;
|
||||
signal private input noteNullifier;
|
||||
|
||||
signal private input inputAmount;
|
||||
signal private input inputSecret;
|
||||
signal private input inputNullifier;
|
||||
signal input inputRoot;
|
||||
signal private input inputPathElements[levels];
|
||||
signal private input inputPathIndices;
|
||||
signal input inputNullifierHash;
|
||||
|
||||
signal private input outputAmount;
|
||||
signal private input outputSecret;
|
||||
signal private input outputNullifier;
|
||||
signal input outputRoot;
|
||||
signal input outputPathIndices;
|
||||
signal private input outputPathElements[levels];
|
||||
signal input outputCommitment;
|
||||
|
||||
signal private input depositBlock;
|
||||
signal input depositRoot;
|
||||
signal private input depositPathIndices;
|
||||
signal private input depositPathElements[levels];
|
||||
|
||||
signal private input withdrawalBlock;
|
||||
signal input withdrawalRoot;
|
||||
signal private input withdrawalPathIndices;
|
||||
signal private input withdrawalPathElements[levels];
|
||||
|
||||
// Check amount invariant
|
||||
inputAmount + rate * (withdrawalBlock - depositBlock) === outputAmount + fee;
|
||||
|
||||
// === check input and output accounts and block range ===
|
||||
// Check that amounts fit into 248 bits to prevent overflow
|
||||
// Fee range is checked by the smart contract
|
||||
// Technically block range check could be skipped because it can't be large enough
|
||||
// negative number that `outputAmount` fits into 248 bits
|
||||
component inputAmountCheck = Num2Bits(248);
|
||||
component outputAmountCheck = Num2Bits(248);
|
||||
component blockRangeCheck = Num2Bits(32);
|
||||
inputAmountCheck.in <== inputAmount;
|
||||
outputAmountCheck.in <== outputAmount;
|
||||
blockRangeCheck.in <== withdrawalBlock - depositBlock;
|
||||
|
||||
// Compute input commitment
|
||||
component inputHasher = Poseidon(3);
|
||||
inputHasher.inputs[0] <== inputAmount;
|
||||
inputHasher.inputs[1] <== inputSecret;
|
||||
inputHasher.inputs[2] <== inputNullifier;
|
||||
|
||||
// Verify that input commitment exists in the tree
|
||||
component inputTree = MerkleTree(levels);
|
||||
inputTree.leaf <== inputHasher.out;
|
||||
inputTree.pathIndices <== inputPathIndices;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
inputTree.pathElements[i] <== inputPathElements[i];
|
||||
}
|
||||
|
||||
// Check merkle proof only if amount is non-zero
|
||||
component checkRoot = ForceEqualIfEnabled();
|
||||
checkRoot.in[0] <== inputRoot;
|
||||
checkRoot.in[1] <== inputTree.root;
|
||||
checkRoot.enabled <== inputAmount;
|
||||
|
||||
// Verify input nullifier hash
|
||||
component inputNullifierHasher = Poseidon(1);
|
||||
inputNullifierHasher.inputs[0] <== inputNullifier;
|
||||
inputNullifierHasher.out === inputNullifierHash;
|
||||
|
||||
// Compute and verify output commitment
|
||||
component outputHasher = Poseidon(3);
|
||||
outputHasher.inputs[0] <== outputAmount;
|
||||
outputHasher.inputs[1] <== outputSecret;
|
||||
outputHasher.inputs[2] <== outputNullifier;
|
||||
outputHasher.out === outputCommitment;
|
||||
|
||||
// Update accounts tree with output account commitment
|
||||
component accountTreeUpdater = MerkleTreeUpdater(levels, zeroLeaf);
|
||||
accountTreeUpdater.oldRoot <== inputRoot;
|
||||
accountTreeUpdater.newRoot <== outputRoot;
|
||||
accountTreeUpdater.leaf <== outputCommitment;
|
||||
accountTreeUpdater.pathIndices <== outputPathIndices;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
accountTreeUpdater.pathElements[i] <== outputPathElements[i];
|
||||
}
|
||||
|
||||
// === check deposit and withdrawal ===
|
||||
// Compute tornado.cash commitment and nullifier
|
||||
component noteHasher = TornadoCommitmentHasher();
|
||||
noteHasher.nullifier <== noteNullifier;
|
||||
noteHasher.secret <== noteSecret;
|
||||
|
||||
// Compute deposit commitment
|
||||
component depositHasher = Poseidon(3);
|
||||
depositHasher.inputs[0] <== instance;
|
||||
depositHasher.inputs[1] <== noteHasher.commitment;
|
||||
depositHasher.inputs[2] <== depositBlock;
|
||||
|
||||
// Verify that deposit commitment exists in the tree
|
||||
component depositTree = MerkleTree(levels);
|
||||
depositTree.leaf <== depositHasher.out;
|
||||
depositTree.pathIndices <== depositPathIndices;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
depositTree.pathElements[i] <== depositPathElements[i];
|
||||
}
|
||||
depositTree.root === depositRoot;
|
||||
|
||||
// Compute withdrawal commitment
|
||||
component withdrawalHasher = Poseidon(3);
|
||||
withdrawalHasher.inputs[0] <== instance;
|
||||
withdrawalHasher.inputs[1] <== noteHasher.nullifierHash;
|
||||
withdrawalHasher.inputs[2] <== withdrawalBlock;
|
||||
|
||||
// Verify that withdrawal commitment exists in the tree
|
||||
component withdrawalTree = MerkleTree(levels);
|
||||
withdrawalTree.leaf <== withdrawalHasher.out;
|
||||
withdrawalTree.pathIndices <== withdrawalPathIndices;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
withdrawalTree.pathElements[i] <== withdrawalPathElements[i];
|
||||
}
|
||||
withdrawalTree.root === withdrawalRoot;
|
||||
|
||||
// Compute reward nullifier
|
||||
component rewardNullifierHasher = Poseidon(1);
|
||||
rewardNullifierHasher.inputs[0] <== noteNullifier;
|
||||
rewardNullifierHasher.out === rewardNullifier;
|
||||
|
||||
// Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof
|
||||
// Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
|
||||
// Squares are used to prevent optimizer from removing those constraints
|
||||
signal extDataHashSquare;
|
||||
extDataHashSquare <== extDataHash * extDataHash;
|
||||
}
|
||||
|
||||
// zeroLeaf = keccak256("tornado") % FIELD_SIZE
|
||||
component main = Reward(20, 21663839004416932945382355908790599225266501822907911457504978515578255421292);
|
4
circuits/TreeUpdate.circom
Normal file
4
circuits/TreeUpdate.circom
Normal file
@ -0,0 +1,4 @@
|
||||
include "./MerkleTreeUpdater.circom";
|
||||
|
||||
// zeroLeaf = keccak256("tornado") % FIELD_SIZE
|
||||
component main = MerkleTreeUpdater(20, 21663839004416932945382355908790599225266501822907911457504978515578255421292);
|
25
circuits/Utils.circom
Normal file
25
circuits/Utils.circom
Normal file
@ -0,0 +1,25 @@
|
||||
include "../node_modules/circomlib/circuits/bitify.circom";
|
||||
include "../node_modules/circomlib/circuits/pedersen.circom";
|
||||
|
||||
// computes Pedersen(nullifier + secret)
|
||||
template TornadoCommitmentHasher() {
|
||||
signal input nullifier;
|
||||
signal input secret;
|
||||
signal output commitment;
|
||||
signal output nullifierHash;
|
||||
|
||||
component commitmentHasher = Pedersen(496);
|
||||
component nullifierHasher = Pedersen(248);
|
||||
component nullifierBits = Num2Bits(248);
|
||||
component secretBits = Num2Bits(248);
|
||||
nullifierBits.in <== nullifier;
|
||||
secretBits.in <== secret;
|
||||
for (var i = 0; i < 248; i++) {
|
||||
nullifierHasher.in[i] <== nullifierBits.out[i];
|
||||
commitmentHasher.in[i] <== nullifierBits.out[i];
|
||||
commitmentHasher.in[i + 248] <== secretBits.out[i];
|
||||
}
|
||||
|
||||
commitment <== commitmentHasher.out[0];
|
||||
nullifierHash <== nullifierHasher.out[0];
|
||||
}
|
83
circuits/Withdraw.circom
Normal file
83
circuits/Withdraw.circom
Normal file
@ -0,0 +1,83 @@
|
||||
include "../node_modules/circomlib/circuits/poseidon.circom";
|
||||
include "../node_modules/circomlib/circuits/bitify.circom";
|
||||
include "./Utils.circom";
|
||||
include "./MerkleTree.circom";
|
||||
include "./MerkleTreeUpdater.circom";
|
||||
|
||||
template Withdraw(levels, zeroLeaf) {
|
||||
// fee is included into the `amount` input
|
||||
signal input amount;
|
||||
signal input extDataHash;
|
||||
|
||||
signal private input inputAmount;
|
||||
signal private input inputSecret;
|
||||
signal private input inputNullifier;
|
||||
signal input inputRoot;
|
||||
signal private input inputPathIndices;
|
||||
signal private input inputPathElements[levels];
|
||||
signal input inputNullifierHash;
|
||||
|
||||
signal private input outputAmount;
|
||||
signal private input outputSecret;
|
||||
signal private input outputNullifier;
|
||||
signal input outputRoot;
|
||||
signal input outputPathIndices;
|
||||
signal private input outputPathElements[levels];
|
||||
signal input outputCommitment;
|
||||
|
||||
// Verify amount invariant
|
||||
inputAmount === outputAmount + amount;
|
||||
|
||||
// Check that amounts fit into 248 bits to prevent overflow
|
||||
// Amount range is checked by the smart contract
|
||||
component inputAmountCheck = Num2Bits(248);
|
||||
component outputAmountCheck = Num2Bits(248);
|
||||
inputAmountCheck.in <== inputAmount;
|
||||
outputAmountCheck.in <== outputAmount;
|
||||
|
||||
// Compute input commitment
|
||||
component inputHasher = Poseidon(3);
|
||||
inputHasher.inputs[0] <== inputAmount;
|
||||
inputHasher.inputs[1] <== inputSecret;
|
||||
inputHasher.inputs[2] <== inputNullifier;
|
||||
|
||||
// Verify that input commitment exists in the tree
|
||||
component tree = MerkleTree(levels);
|
||||
tree.leaf <== inputHasher.out;
|
||||
tree.pathIndices <== inputPathIndices;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
tree.pathElements[i] <== inputPathElements[i];
|
||||
}
|
||||
tree.root === inputRoot;
|
||||
|
||||
// Verify input nullifier hash
|
||||
component nullifierHasher = Poseidon(1);
|
||||
nullifierHasher.inputs[0] <== inputNullifier;
|
||||
nullifierHasher.out === inputNullifierHash;
|
||||
|
||||
// Compute and verify output commitment
|
||||
component outputHasher = Poseidon(3);
|
||||
outputHasher.inputs[0] <== outputAmount;
|
||||
outputHasher.inputs[1] <== outputSecret;
|
||||
outputHasher.inputs[2] <== outputNullifier;
|
||||
outputHasher.out === outputCommitment;
|
||||
|
||||
// Update accounts tree with output account commitment
|
||||
component treeUpdater = MerkleTreeUpdater(levels, zeroLeaf);
|
||||
treeUpdater.oldRoot <== inputRoot;
|
||||
treeUpdater.newRoot <== outputRoot;
|
||||
treeUpdater.leaf <== outputCommitment;
|
||||
treeUpdater.pathIndices <== outputPathIndices;
|
||||
for (var i = 0; i < levels; i++) {
|
||||
treeUpdater.pathElements[i] <== outputPathElements[i];
|
||||
}
|
||||
|
||||
// Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof
|
||||
// Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
|
||||
// Squares are used to prevent optimizer from removing those constraints
|
||||
signal extDataHashSquare;
|
||||
extDataHashSquare <== extDataHash * extDataHash;
|
||||
}
|
||||
|
||||
// zeroLeaf = keccak256("tornado") % FIELD_SIZE
|
||||
component main = Withdraw(20, 21663839004416932945382355908790599225266501822907911457504978515578255421292);
|
35
compileHasher.js
Normal file
35
compileHasher.js
Normal file
@ -0,0 +1,35 @@
|
||||
// Generates Hasher artifact at compile-time using Truffle's external compiler
|
||||
// mechanism
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const genContract = require('circomlib/src/poseidon_gencontract.js')
|
||||
|
||||
// where Truffle will expect to find the results of the external compiler
|
||||
// command
|
||||
const outputPath = path.join(__dirname, 'build', 'contracts')
|
||||
const outputPath2 = path.join(outputPath, 'Hasher2.json')
|
||||
const outputPath3 = path.join(outputPath, 'Hasher3.json')
|
||||
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true })
|
||||
}
|
||||
|
||||
function main() {
|
||||
const contract2 = {
|
||||
contractName: 'Hasher2',
|
||||
abi: genContract.generateABI(2),
|
||||
bytecode: genContract.createCode(2),
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath2, JSON.stringify(contract2, null, 2))
|
||||
|
||||
const contract3 = {
|
||||
contractName: 'Hasher3',
|
||||
abi: genContract.generateABI(3),
|
||||
bytecode: genContract.createCode(3),
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath3, JSON.stringify(contract3, null, 2))
|
||||
}
|
||||
|
||||
main()
|
314
contracts/Miner.sol
Normal file
314
contracts/Miner.sol
Normal file
@ -0,0 +1,314 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./interfaces/IVerifier.sol";
|
||||
import "./interfaces/IRewardSwap.sol";
|
||||
import "./TornadoTrees.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/math/SafeMath.sol";
|
||||
import "torn-token/contracts/ENS.sol";
|
||||
|
||||
contract Miner is EnsResolve {
|
||||
using SafeMath for uint256;
|
||||
|
||||
IVerifier public rewardVerifier;
|
||||
IVerifier public withdrawVerifier;
|
||||
IVerifier public treeUpdateVerifier;
|
||||
IRewardSwap public immutable rewardSwap;
|
||||
address public immutable governance;
|
||||
TornadoTrees public tornadoTrees;
|
||||
|
||||
mapping(bytes32 => bool) public accountNullifiers;
|
||||
mapping(bytes32 => bool) public rewardNullifiers;
|
||||
mapping(address => uint256) public rates;
|
||||
|
||||
uint256 public accountCount;
|
||||
uint256 public constant ACCOUNT_ROOT_HISTORY_SIZE = 100;
|
||||
bytes32[ACCOUNT_ROOT_HISTORY_SIZE] public accountRoots;
|
||||
|
||||
event NewAccount(bytes32 commitment, bytes32 nullifier, bytes encryptedAccount, uint256 index);
|
||||
event RateChanged(address instance, uint256 value);
|
||||
event VerifiersUpdated(address reward, address withdraw, address treeUpdate);
|
||||
|
||||
struct TreeUpdateArgs {
|
||||
bytes32 oldRoot;
|
||||
bytes32 newRoot;
|
||||
bytes32 leaf;
|
||||
uint256 pathIndices;
|
||||
}
|
||||
|
||||
struct AccountUpdate {
|
||||
bytes32 inputRoot;
|
||||
bytes32 inputNullifierHash;
|
||||
bytes32 outputRoot;
|
||||
uint256 outputPathIndices;
|
||||
bytes32 outputCommitment;
|
||||
}
|
||||
|
||||
struct RewardExtData {
|
||||
address relayer;
|
||||
bytes encryptedAccount;
|
||||
}
|
||||
|
||||
struct RewardArgs {
|
||||
uint256 rate;
|
||||
uint256 fee;
|
||||
address instance;
|
||||
bytes32 rewardNullifier;
|
||||
bytes32 extDataHash;
|
||||
bytes32 depositRoot;
|
||||
bytes32 withdrawalRoot;
|
||||
RewardExtData extData;
|
||||
AccountUpdate account;
|
||||
}
|
||||
|
||||
struct WithdrawExtData {
|
||||
uint256 fee;
|
||||
address recipient;
|
||||
address relayer;
|
||||
bytes encryptedAccount;
|
||||
}
|
||||
|
||||
struct WithdrawArgs {
|
||||
uint256 amount;
|
||||
bytes32 extDataHash;
|
||||
WithdrawExtData extData;
|
||||
AccountUpdate account;
|
||||
}
|
||||
|
||||
struct Rate {
|
||||
bytes32 instance;
|
||||
uint256 value;
|
||||
}
|
||||
|
||||
modifier onlyGovernance() {
|
||||
require(msg.sender == governance, "Only governance can perform this action");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
bytes32 _rewardSwap,
|
||||
bytes32 _governance,
|
||||
bytes32 _tornadoTrees,
|
||||
bytes32[3] memory _verifiers,
|
||||
bytes32 _accountRoot,
|
||||
Rate[] memory _rates
|
||||
) public {
|
||||
rewardSwap = IRewardSwap(resolve(_rewardSwap));
|
||||
governance = resolve(_governance);
|
||||
tornadoTrees = TornadoTrees(resolve(_tornadoTrees));
|
||||
|
||||
// insert empty tree root without incrementing accountCount counter
|
||||
accountRoots[0] = _accountRoot;
|
||||
|
||||
_setRates(_rates);
|
||||
// prettier-ignore
|
||||
_setVerifiers([
|
||||
IVerifier(resolve(_verifiers[0])),
|
||||
IVerifier(resolve(_verifiers[1])),
|
||||
IVerifier(resolve(_verifiers[2]))
|
||||
]);
|
||||
}
|
||||
|
||||
function reward(bytes memory _proof, RewardArgs memory _args) public {
|
||||
reward(_proof, _args, new bytes(0), TreeUpdateArgs(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
function batchReward(bytes[] calldata _rewardArgs) external {
|
||||
for (uint256 i = 0; i < _rewardArgs.length; i++) {
|
||||
(bytes memory proof, RewardArgs memory args) = abi.decode(_rewardArgs[i], (bytes, RewardArgs));
|
||||
reward(proof, args);
|
||||
}
|
||||
}
|
||||
|
||||
function reward(
|
||||
bytes memory _proof,
|
||||
RewardArgs memory _args,
|
||||
bytes memory _treeUpdateProof,
|
||||
TreeUpdateArgs memory _treeUpdateArgs
|
||||
) public {
|
||||
validateAccountUpdate(_args.account, _treeUpdateProof, _treeUpdateArgs);
|
||||
tornadoTrees.validateRoots(_args.depositRoot, _args.withdrawalRoot);
|
||||
require(_args.extDataHash == keccak248(abi.encode(_args.extData)), "Incorrect external data hash");
|
||||
require(_args.fee < 2**248, "Fee value out of range");
|
||||
require(_args.rate == rates[_args.instance] && _args.rate > 0, "Invalid reward rate");
|
||||
require(!rewardNullifiers[_args.rewardNullifier], "Reward has been already spent");
|
||||
require(
|
||||
rewardVerifier.verifyProof(
|
||||
_proof,
|
||||
[
|
||||
uint256(_args.rate),
|
||||
uint256(_args.fee),
|
||||
uint256(_args.instance),
|
||||
uint256(_args.rewardNullifier),
|
||||
uint256(_args.extDataHash),
|
||||
uint256(_args.account.inputRoot),
|
||||
uint256(_args.account.inputNullifierHash),
|
||||
uint256(_args.account.outputRoot),
|
||||
uint256(_args.account.outputPathIndices),
|
||||
uint256(_args.account.outputCommitment),
|
||||
uint256(_args.depositRoot),
|
||||
uint256(_args.withdrawalRoot)
|
||||
]
|
||||
),
|
||||
"Invalid reward proof"
|
||||
);
|
||||
|
||||
accountNullifiers[_args.account.inputNullifierHash] = true;
|
||||
rewardNullifiers[_args.rewardNullifier] = true;
|
||||
insertAccountRoot(_args.account.inputRoot == getLastAccountRoot() ? _args.account.outputRoot : _treeUpdateArgs.newRoot);
|
||||
if (_args.fee > 0) {
|
||||
rewardSwap.swap(_args.extData.relayer, _args.fee);
|
||||
}
|
||||
|
||||
emit NewAccount(
|
||||
_args.account.outputCommitment,
|
||||
_args.account.inputNullifierHash,
|
||||
_args.extData.encryptedAccount,
|
||||
accountCount - 1
|
||||
);
|
||||
}
|
||||
|
||||
function withdraw(bytes memory _proof, WithdrawArgs memory _args) public {
|
||||
withdraw(_proof, _args, new bytes(0), TreeUpdateArgs(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
function withdraw(
|
||||
bytes memory _proof,
|
||||
WithdrawArgs memory _args,
|
||||
bytes memory _treeUpdateProof,
|
||||
TreeUpdateArgs memory _treeUpdateArgs
|
||||
) public {
|
||||
validateAccountUpdate(_args.account, _treeUpdateProof, _treeUpdateArgs);
|
||||
require(_args.extDataHash == keccak248(abi.encode(_args.extData)), "Incorrect external data hash");
|
||||
require(_args.amount < 2**248, "Amount value out of range");
|
||||
require(
|
||||
withdrawVerifier.verifyProof(
|
||||
_proof,
|
||||
[
|
||||
uint256(_args.amount),
|
||||
uint256(_args.extDataHash),
|
||||
uint256(_args.account.inputRoot),
|
||||
uint256(_args.account.inputNullifierHash),
|
||||
uint256(_args.account.outputRoot),
|
||||
uint256(_args.account.outputPathIndices),
|
||||
uint256(_args.account.outputCommitment)
|
||||
]
|
||||
),
|
||||
"Invalid withdrawal proof"
|
||||
);
|
||||
|
||||
insertAccountRoot(_args.account.inputRoot == getLastAccountRoot() ? _args.account.outputRoot : _treeUpdateArgs.newRoot);
|
||||
accountNullifiers[_args.account.inputNullifierHash] = true;
|
||||
// allow submitting noop withdrawals (amount == 0)
|
||||
uint256 amount = _args.amount.sub(_args.extData.fee, "Amount should be greater than fee");
|
||||
if (amount > 0) {
|
||||
rewardSwap.swap(_args.extData.recipient, amount);
|
||||
}
|
||||
// Note. The relayer swap rate always will be worse than estimated
|
||||
if (_args.extData.fee > 0) {
|
||||
rewardSwap.swap(_args.extData.relayer, _args.extData.fee);
|
||||
}
|
||||
|
||||
emit NewAccount(
|
||||
_args.account.outputCommitment,
|
||||
_args.account.inputNullifierHash,
|
||||
_args.extData.encryptedAccount,
|
||||
accountCount - 1
|
||||
);
|
||||
}
|
||||
|
||||
function setRates(Rate[] memory _rates) external onlyGovernance {
|
||||
_setRates(_rates);
|
||||
}
|
||||
|
||||
function setVerifiers(IVerifier[3] calldata _verifiers) external onlyGovernance {
|
||||
_setVerifiers(_verifiers);
|
||||
}
|
||||
|
||||
function setTornadoTreesContract(TornadoTrees _tornadoTrees) external onlyGovernance {
|
||||
tornadoTrees = _tornadoTrees;
|
||||
}
|
||||
|
||||
function setPoolWeight(uint256 _newWeight) external onlyGovernance {
|
||||
rewardSwap.setPoolWeight(_newWeight);
|
||||
}
|
||||
|
||||
// ------VIEW-------
|
||||
|
||||
/**
|
||||
@dev Whether the root is present in the root history
|
||||
*/
|
||||
function isKnownAccountRoot(bytes32 _root, uint256 _index) public view returns (bool) {
|
||||
return _root != 0 && accountRoots[_index % ACCOUNT_ROOT_HISTORY_SIZE] == _root;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Returns the last root
|
||||
*/
|
||||
function getLastAccountRoot() public view returns (bytes32) {
|
||||
return accountRoots[accountCount % ACCOUNT_ROOT_HISTORY_SIZE];
|
||||
}
|
||||
|
||||
// -----INTERNAL-------
|
||||
|
||||
function keccak248(bytes memory _data) internal pure returns (bytes32) {
|
||||
return keccak256(_data) & 0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
||||
}
|
||||
|
||||
function validateTreeUpdate(
|
||||
bytes memory _proof,
|
||||
TreeUpdateArgs memory _args,
|
||||
bytes32 _commitment
|
||||
) internal view {
|
||||
require(_proof.length > 0, "Outdated account merkle root");
|
||||
require(_args.oldRoot == getLastAccountRoot(), "Outdated tree update merkle root");
|
||||
require(_args.leaf == _commitment, "Incorrect commitment inserted");
|
||||
require(_args.pathIndices == accountCount, "Incorrect account insert index");
|
||||
require(
|
||||
treeUpdateVerifier.verifyProof(
|
||||
_proof,
|
||||
[uint256(_args.oldRoot), uint256(_args.newRoot), uint256(_args.leaf), uint256(_args.pathIndices)]
|
||||
),
|
||||
"Invalid tree update proof"
|
||||
);
|
||||
}
|
||||
|
||||
function validateAccountUpdate(
|
||||
AccountUpdate memory _account,
|
||||
bytes memory _treeUpdateProof,
|
||||
TreeUpdateArgs memory _treeUpdateArgs
|
||||
) internal view {
|
||||
require(!accountNullifiers[_account.inputNullifierHash], "Outdated account state");
|
||||
if (_account.inputRoot != getLastAccountRoot()) {
|
||||
// _account.outputPathIndices (= last tree leaf index) is always equal to root index in the history mapping
|
||||
// because we always generate a new root for each new leaf
|
||||
require(isKnownAccountRoot(_account.inputRoot, _account.outputPathIndices), "Invalid account root");
|
||||
validateTreeUpdate(_treeUpdateProof, _treeUpdateArgs, _account.outputCommitment);
|
||||
} else {
|
||||
require(_account.outputPathIndices == accountCount, "Incorrect account insert index");
|
||||
}
|
||||
}
|
||||
|
||||
function insertAccountRoot(bytes32 _root) internal {
|
||||
accountRoots[++accountCount % ACCOUNT_ROOT_HISTORY_SIZE] = _root;
|
||||
}
|
||||
|
||||
function _setRates(Rate[] memory _rates) internal {
|
||||
for (uint256 i = 0; i < _rates.length; i++) {
|
||||
require(_rates[i].value < 2**128, "Incorrect rate");
|
||||
address instance = resolve(_rates[i].instance);
|
||||
rates[instance] = _rates[i].value;
|
||||
emit RateChanged(instance, _rates[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
function _setVerifiers(IVerifier[3] memory _verifiers) internal {
|
||||
rewardVerifier = _verifiers[0];
|
||||
withdrawVerifier = _verifiers[1];
|
||||
treeUpdateVerifier = _verifiers[2];
|
||||
emit VerifiersUpdated(address(_verifiers[0]), address(_verifiers[1]), address(_verifiers[2]));
|
||||
}
|
||||
}
|
95
contracts/RewardSwap.sol
Normal file
95
contracts/RewardSwap.sol
Normal file
@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
|
||||
import "torn-token/contracts/ENS.sol";
|
||||
import "./utils/FloatMath.sol";
|
||||
|
||||
/**
|
||||
Let's imagine we have 1M TORN tokens for anonymity mining to distribute during 1 year (~31536000 seconds).
|
||||
The contract should constantly add liquidity to a pool of claimed rewards to TORN (REWD/TORN). At any time user can exchange REWD->TORN using
|
||||
this pool. The rate depends on current available TORN liquidity - the more TORN are withdrawn the worse the swap rate is.
|
||||
|
||||
The contract starts with some virtual balance liquidity and adds some TORN tokens every second to the balance. Users will decrease
|
||||
this balance by swaps.
|
||||
|
||||
Exchange rate can be calculated as following:
|
||||
BalanceAfter = BalanceBefore * e^(-rewardAmount/poolWeight)
|
||||
tokens = BalanceBefore - BalanceAfter
|
||||
*/
|
||||
|
||||
contract RewardSwap is EnsResolve {
|
||||
using SafeMath for uint256;
|
||||
|
||||
uint256 public constant DURATION = 365 days;
|
||||
|
||||
IERC20 public immutable torn;
|
||||
address public immutable miner;
|
||||
uint256 public immutable startTimestamp;
|
||||
uint256 public immutable initialLiquidity;
|
||||
uint256 public immutable liquidity;
|
||||
uint256 public tokensSold;
|
||||
uint256 public poolWeight;
|
||||
|
||||
event Swap(address indexed recipient, uint256 pTORN, uint256 TORN);
|
||||
event PoolWeightUpdated(uint256 newWeight);
|
||||
|
||||
modifier onlyMiner() {
|
||||
require(msg.sender == miner, "Only Miner contract can call");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
bytes32 _torn,
|
||||
bytes32 _miner,
|
||||
uint256 _miningCap,
|
||||
uint256 _initialLiquidity,
|
||||
uint256 _poolWeight
|
||||
) public {
|
||||
require(_initialLiquidity <= _miningCap, "Initial liquidity should be lower than mining cap");
|
||||
torn = IERC20(resolve(_torn));
|
||||
miner = resolve(_miner);
|
||||
initialLiquidity = _initialLiquidity;
|
||||
liquidity = _miningCap.sub(_initialLiquidity);
|
||||
poolWeight = _poolWeight;
|
||||
startTimestamp = getTimestamp();
|
||||
}
|
||||
|
||||
function swap(address _recipient, uint256 _amount) external onlyMiner returns (uint256) {
|
||||
uint256 tokens = getExpectedReturn(_amount);
|
||||
tokensSold += tokens;
|
||||
require(torn.transfer(_recipient, tokens), "transfer failed");
|
||||
emit Swap(_recipient, _amount, tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev
|
||||
*/
|
||||
function getExpectedReturn(uint256 _amount) public view returns (uint256) {
|
||||
uint256 oldBalance = tornVirtualBalance();
|
||||
int128 pow = FloatMath.neg(FloatMath.divu(_amount, poolWeight));
|
||||
int128 exp = FloatMath.exp(pow);
|
||||
uint256 newBalance = FloatMath.mulu(exp, oldBalance);
|
||||
return oldBalance.sub(newBalance);
|
||||
}
|
||||
|
||||
function tornVirtualBalance() public view returns (uint256) {
|
||||
uint256 passedTime = getTimestamp().sub(startTimestamp);
|
||||
if (passedTime < DURATION) {
|
||||
return initialLiquidity.add(liquidity.mul(passedTime).div(DURATION)).sub(tokensSold);
|
||||
} else {
|
||||
return torn.balanceOf(address(this));
|
||||
}
|
||||
}
|
||||
|
||||
function setPoolWeight(uint256 _newWeight) external onlyMiner {
|
||||
poolWeight = _newWeight;
|
||||
emit PoolWeightUpdated(_newWeight);
|
||||
}
|
||||
|
||||
function getTimestamp() public view virtual returns (uint256) {
|
||||
return block.timestamp;
|
||||
}
|
||||
}
|
87
contracts/TornadoProxy.sol
Normal file
87
contracts/TornadoProxy.sol
Normal file
@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/math/Math.sol";
|
||||
import "./interfaces/ITornadoInstance.sol";
|
||||
import "./interfaces/ITornadoTrees.sol";
|
||||
import "torn-token/contracts/ENS.sol";
|
||||
|
||||
contract TornadoProxy is EnsResolve {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
event EncryptedNote(address indexed sender, bytes encryptedNote);
|
||||
|
||||
ITornadoTrees public immutable tornadoTrees;
|
||||
address public immutable governance;
|
||||
|
||||
mapping(ITornadoInstance => bool) public instances;
|
||||
modifier onlyGovernance() {
|
||||
require(msg.sender == governance, "Not authorized");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
bytes32 _tornadoTrees,
|
||||
bytes32 _governance,
|
||||
bytes32[] memory _instances
|
||||
) public {
|
||||
tornadoTrees = ITornadoTrees(resolve(_tornadoTrees));
|
||||
governance = resolve(_governance);
|
||||
|
||||
for (uint256 i = 0; i < _instances.length; i++) {
|
||||
instances[ITornadoInstance(resolve(_instances[i]))] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function deposit(ITornadoInstance _tornado, bytes32 _commitment, bytes calldata _encryptedNote) external payable {
|
||||
require(instances[_tornado], "The instance is not supported");
|
||||
|
||||
_tornado.deposit{ value: msg.value }(_commitment);
|
||||
tornadoTrees.registerDeposit(address(_tornado), _commitment);
|
||||
emit EncryptedNote(msg.sender, _encryptedNote);
|
||||
}
|
||||
|
||||
function updateInstance(ITornadoInstance _instance, bool _update) external onlyGovernance {
|
||||
instances[_instance] = _update;
|
||||
}
|
||||
|
||||
function withdraw(
|
||||
ITornadoInstance _tornado,
|
||||
bytes calldata _proof,
|
||||
bytes32 _root,
|
||||
bytes32 _nullifierHash,
|
||||
address payable _recipient,
|
||||
address payable _relayer,
|
||||
uint256 _fee,
|
||||
uint256 _refund
|
||||
) external payable {
|
||||
require(instances[_tornado], "The instance is not supported");
|
||||
|
||||
_tornado.withdraw{ value: msg.value }(_proof, _root, _nullifierHash, _recipient, _relayer, _fee, _refund);
|
||||
tornadoTrees.registerWithdrawal(address(_tornado), _nullifierHash);
|
||||
}
|
||||
|
||||
/// @dev Method to claim junk and accidentally sent tokens
|
||||
function rescueTokens(
|
||||
IERC20 _token,
|
||||
address payable _to,
|
||||
uint256 _balance
|
||||
) external onlyGovernance {
|
||||
require(_to != address(0), "TORN: can not send to zero address");
|
||||
|
||||
if (_token == IERC20(0)) {
|
||||
// for Ether
|
||||
uint256 totalBalance = address(this).balance;
|
||||
uint256 balance = _balance == 0 ? totalBalance : Math.min(totalBalance, _balance);
|
||||
_to.transfer(balance);
|
||||
} else {
|
||||
// any other erc20
|
||||
uint256 totalBalance = _token.balanceOf(address(this));
|
||||
uint256 balance = _balance == 0 ? totalBalance : Math.min(totalBalance, _balance);
|
||||
require(balance > 0, "TORN: trying to send 0 balance");
|
||||
_token.safeTransfer(_to, balance);
|
||||
}
|
||||
}
|
||||
}
|
132
contracts/TornadoTrees.sol
Normal file
132
contracts/TornadoTrees.sol
Normal file
@ -0,0 +1,132 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "torn-token/contracts/ENS.sol";
|
||||
import "./utils/OwnableMerkleTree.sol";
|
||||
import "./interfaces/ITornadoTrees.sol";
|
||||
import "./interfaces/IHasher.sol";
|
||||
|
||||
contract TornadoTrees is ITornadoTrees, EnsResolve {
|
||||
OwnableMerkleTree public immutable depositTree;
|
||||
OwnableMerkleTree public immutable withdrawalTree;
|
||||
IHasher public immutable hasher;
|
||||
address public immutable tornadoProxy;
|
||||
|
||||
bytes32[] public deposits;
|
||||
uint256 public lastProcessedDepositLeaf;
|
||||
|
||||
bytes32[] public withdrawals;
|
||||
uint256 public lastProcessedWithdrawalLeaf;
|
||||
|
||||
event DepositData(address instance, bytes32 indexed hash, uint256 block, uint256 index);
|
||||
event WithdrawalData(address instance, bytes32 indexed hash, uint256 block, uint256 index);
|
||||
|
||||
struct TreeLeaf {
|
||||
address instance;
|
||||
bytes32 hash;
|
||||
uint256 block;
|
||||
}
|
||||
|
||||
modifier onlyTornadoProxy {
|
||||
require(msg.sender == tornadoProxy, "Not authorized");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
bytes32 _tornadoProxy,
|
||||
bytes32 _hasher2,
|
||||
bytes32 _hasher3,
|
||||
uint32 _levels
|
||||
) public {
|
||||
tornadoProxy = resolve(_tornadoProxy);
|
||||
hasher = IHasher(resolve(_hasher3));
|
||||
depositTree = new OwnableMerkleTree(_levels, IHasher(resolve(_hasher2)));
|
||||
withdrawalTree = new OwnableMerkleTree(_levels, IHasher(resolve(_hasher2)));
|
||||
}
|
||||
|
||||
function registerDeposit(address _instance, bytes32 _commitment) external override onlyTornadoProxy {
|
||||
deposits.push(keccak256(abi.encode(_instance, _commitment, blockNumber())));
|
||||
}
|
||||
|
||||
function registerWithdrawal(address _instance, bytes32 _nullifier) external override onlyTornadoProxy {
|
||||
withdrawals.push(keccak256(abi.encode(_instance, _nullifier, blockNumber())));
|
||||
}
|
||||
|
||||
function updateRoots(TreeLeaf[] calldata _deposits, TreeLeaf[] calldata _withdrawals) external {
|
||||
if (_deposits.length > 0) updateDepositTree(_deposits);
|
||||
if (_withdrawals.length > 0) updateWithdrawalTree(_withdrawals);
|
||||
}
|
||||
|
||||
function updateDepositTree(TreeLeaf[] calldata _deposits) public {
|
||||
bytes32[] memory leaves = new bytes32[](_deposits.length);
|
||||
uint256 offset = lastProcessedDepositLeaf;
|
||||
|
||||
for (uint256 i = 0; i < _deposits.length; i++) {
|
||||
TreeLeaf memory deposit = _deposits[i];
|
||||
bytes32 leafHash = keccak256(abi.encode(deposit.instance, deposit.hash, deposit.block));
|
||||
require(deposits[offset + i] == leafHash, "Incorrect deposit");
|
||||
|
||||
leaves[i] = hasher.poseidon([bytes32(uint256(deposit.instance)), deposit.hash, bytes32(deposit.block)]);
|
||||
delete deposits[offset + i];
|
||||
|
||||
emit DepositData(deposit.instance, deposit.hash, deposit.block, offset + i);
|
||||
}
|
||||
|
||||
lastProcessedDepositLeaf = offset + _deposits.length;
|
||||
depositTree.bulkInsert(leaves);
|
||||
}
|
||||
|
||||
function updateWithdrawalTree(TreeLeaf[] calldata _withdrawals) public {
|
||||
bytes32[] memory leaves = new bytes32[](_withdrawals.length);
|
||||
uint256 offset = lastProcessedWithdrawalLeaf;
|
||||
|
||||
for (uint256 i = 0; i < _withdrawals.length; i++) {
|
||||
TreeLeaf memory withdrawal = _withdrawals[i];
|
||||
bytes32 leafHash = keccak256(abi.encode(withdrawal.instance, withdrawal.hash, withdrawal.block));
|
||||
require(withdrawals[offset + i] == leafHash, "Incorrect withdrawal");
|
||||
|
||||
leaves[i] = hasher.poseidon([bytes32(uint256(withdrawal.instance)), withdrawal.hash, bytes32(withdrawal.block)]);
|
||||
delete withdrawals[offset + i];
|
||||
|
||||
emit WithdrawalData(withdrawal.instance, withdrawal.hash, withdrawal.block, offset + i);
|
||||
}
|
||||
|
||||
lastProcessedWithdrawalLeaf = offset + _withdrawals.length;
|
||||
withdrawalTree.bulkInsert(leaves);
|
||||
}
|
||||
|
||||
function validateRoots(bytes32 _depositRoot, bytes32 _withdrawalRoot) public view {
|
||||
require(depositTree.isKnownRoot(_depositRoot), "Incorrect deposit tree root");
|
||||
require(withdrawalTree.isKnownRoot(_withdrawalRoot), "Incorrect withdrawal tree root");
|
||||
}
|
||||
|
||||
function depositRoot() external view returns (bytes32) {
|
||||
return depositTree.getLastRoot();
|
||||
}
|
||||
|
||||
function withdrawalRoot() external view returns (bytes32) {
|
||||
return withdrawalTree.getLastRoot();
|
||||
}
|
||||
|
||||
function getRegisteredDeposits() external view returns (bytes32[] memory _deposits) {
|
||||
uint256 count = deposits.length - lastProcessedDepositLeaf;
|
||||
_deposits = new bytes32[](count);
|
||||
for (uint256 i = 0; i < count; i++) {
|
||||
_deposits[i] = deposits[lastProcessedDepositLeaf + i];
|
||||
}
|
||||
}
|
||||
|
||||
function getRegisteredWithdrawals() external view returns (bytes32[] memory _withdrawals) {
|
||||
uint256 count = withdrawals.length - lastProcessedWithdrawalLeaf;
|
||||
_withdrawals = new bytes32[](count);
|
||||
for (uint256 i = 0; i < count; i++) {
|
||||
_withdrawals[i] = withdrawals[lastProcessedWithdrawalLeaf + i];
|
||||
}
|
||||
}
|
||||
|
||||
function blockNumber() public view virtual returns (uint256) {
|
||||
return block.number;
|
||||
}
|
||||
}
|
9
contracts/interfaces/IHasher.sol
Normal file
9
contracts/interfaces/IHasher.sol
Normal file
@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
interface IHasher {
|
||||
function poseidon(bytes32[2] calldata inputs) external pure returns (bytes32);
|
||||
|
||||
function poseidon(bytes32[3] calldata inputs) external pure returns (bytes32);
|
||||
}
|
9
contracts/interfaces/IRewardSwap.sol
Normal file
9
contracts/interfaces/IRewardSwap.sol
Normal file
@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
interface IRewardSwap {
|
||||
function swap(address recipient, uint256 amount) external returns (uint256);
|
||||
|
||||
function setPoolWeight(uint256 newWeight) external;
|
||||
}
|
17
contracts/interfaces/ITornadoInstance.sol
Normal file
17
contracts/interfaces/ITornadoInstance.sol
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
interface ITornadoInstance {
|
||||
function deposit(bytes32 commitment) external payable;
|
||||
|
||||
function withdraw(
|
||||
bytes calldata proof,
|
||||
bytes32 root,
|
||||
bytes32 nullifierHash,
|
||||
address payable recipient,
|
||||
address payable relayer,
|
||||
uint256 fee,
|
||||
uint256 refund
|
||||
) external payable;
|
||||
}
|
9
contracts/interfaces/ITornadoTrees.sol
Normal file
9
contracts/interfaces/ITornadoTrees.sol
Normal file
@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
interface ITornadoTrees {
|
||||
function registerDeposit(address instance, bytes32 commitment) external;
|
||||
|
||||
function registerWithdrawal(address instance, bytes32 nullifier) external;
|
||||
}
|
11
contracts/interfaces/IVerifier.sol
Normal file
11
contracts/interfaces/IVerifier.sol
Normal file
@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
interface IVerifier {
|
||||
function verifyProof(bytes calldata proof, uint256[4] calldata input) external view returns (bool);
|
||||
|
||||
function verifyProof(bytes calldata proof, uint256[7] calldata input) external view returns (bool);
|
||||
|
||||
function verifyProof(bytes calldata proof, uint256[12] calldata input) external view returns (bool);
|
||||
}
|
18
contracts/mocks/MerkleTreeWithHistoryMock.sol
Normal file
18
contracts/mocks/MerkleTreeWithHistoryMock.sol
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../utils/MerkleTreeWithHistory.sol";
|
||||
|
||||
contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory {
|
||||
constructor(uint32 _treeLevels, IHasher _hasher) public MerkleTreeWithHistory(_treeLevels, _hasher) {}
|
||||
|
||||
function insert(bytes32 _leaf) external returns (uint32 index) {
|
||||
return _insert(_leaf);
|
||||
}
|
||||
|
||||
function bulkInsert(bytes32[] memory _leaves) external {
|
||||
_bulkInsert(_leaves);
|
||||
}
|
||||
}
|
23
contracts/mocks/MinerMock.sol
Normal file
23
contracts/mocks/MinerMock.sol
Normal file
@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../Miner.sol";
|
||||
|
||||
contract MinerMock is Miner {
|
||||
uint256 public timestamp;
|
||||
|
||||
constructor(
|
||||
bytes32 _rewardSwap,
|
||||
bytes32 _governance,
|
||||
bytes32 _tornadoTrees,
|
||||
bytes32[3] memory verifiers,
|
||||
bytes32 _accountRoot,
|
||||
Rate[] memory _rates
|
||||
) public Miner(_rewardSwap, _governance, _tornadoTrees, verifiers, _accountRoot, _rates) {}
|
||||
|
||||
function resolve(bytes32 _addr) public view override returns (address) {
|
||||
return address(uint160(uint256(_addr) >> (12 * 8)));
|
||||
}
|
||||
}
|
33
contracts/mocks/RewardSwapMock.sol
Normal file
33
contracts/mocks/RewardSwapMock.sol
Normal file
@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
import "../RewardSwap.sol";
|
||||
|
||||
contract RewardSwapMock is RewardSwap {
|
||||
uint256 public timestamp;
|
||||
|
||||
constructor(
|
||||
bytes32 _torn,
|
||||
bytes32 _miner,
|
||||
uint256 _miningCap,
|
||||
uint256 _initialLiquidity,
|
||||
uint256 _poolWeight
|
||||
) public RewardSwap(_torn, _miner, _miningCap, _initialLiquidity, _poolWeight) {
|
||||
timestamp = block.timestamp;
|
||||
}
|
||||
|
||||
function setTimestamp(uint256 _timestamp) public {
|
||||
timestamp = _timestamp;
|
||||
}
|
||||
|
||||
function resolve(bytes32 _addr) public view override returns (address) {
|
||||
return address(uint160(uint256(_addr) >> (12 * 8)));
|
||||
}
|
||||
|
||||
function getTimestamp() public view override returns (uint256) {
|
||||
if (timestamp == 0) {
|
||||
return block.timestamp;
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
}
|
5
contracts/mocks/TORNMock.sol
Normal file
5
contracts/mocks/TORNMock.sol
Normal file
@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
import "torn-token/contracts/mocks/TORNMock.sol";
|
30
contracts/mocks/TornadoTreesMock.sol
Normal file
30
contracts/mocks/TornadoTreesMock.sol
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../TornadoTrees.sol";
|
||||
|
||||
contract TornadoTreesMock is TornadoTrees {
|
||||
uint256 public timestamp;
|
||||
uint256 public currentBlock;
|
||||
|
||||
constructor(
|
||||
bytes32 _tornadoProxy,
|
||||
bytes32 _hasher2,
|
||||
bytes32 _hasher3,
|
||||
uint32 _levels
|
||||
) public TornadoTrees(_tornadoProxy, _hasher2, _hasher3, _levels) {}
|
||||
|
||||
function resolve(bytes32 _addr) public view override returns (address) {
|
||||
return address(uint160(uint256(_addr) >> (12 * 8)));
|
||||
}
|
||||
|
||||
function setBlockNumber(uint256 _blockNumber) public {
|
||||
currentBlock = _blockNumber;
|
||||
}
|
||||
|
||||
function blockNumber() public view override returns (uint256) {
|
||||
return currentBlock == 0 ? block.number : currentBlock;
|
||||
}
|
||||
}
|
11
contracts/utils/Echoer.sol
Normal file
11
contracts/utils/Echoer.sol
Normal file
@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
contract Echoer {
|
||||
event Echo(address indexed who, bytes data);
|
||||
|
||||
function echo(bytes calldata _data) external {
|
||||
emit Echo(msg.sender, _data);
|
||||
}
|
||||
}
|
710
contracts/utils/FloatMath.sol
Normal file
710
contracts/utils/FloatMath.sol
Normal file
@ -0,0 +1,710 @@
|
||||
// SPDX-License-Identifier: BSD-4-Clause
|
||||
/*
|
||||
* ABDK Math 64.64 Smart Contract Library. Copyright © 2019 by ABDK Consulting.
|
||||
* Author: Mikhail Vladimirov <mikhail.vladimirov@gmail.com>
|
||||
*/
|
||||
pragma solidity ^0.5.0 || ^0.6.0 || ^0.7.0;
|
||||
|
||||
/**
|
||||
* Smart contract library of mathematical functions operating with signed
|
||||
* 64.64-bit fixed point numbers. Signed 64.64-bit fixed point number is
|
||||
* basically a simple fraction whose numerator is signed 128-bit integer and
|
||||
* denominator is 2^64. As long as denominator is always the same, there is no
|
||||
* need to store it, thus in Solidity signed 64.64-bit fixed point numbers are
|
||||
* represented by int128 type holding only the numerator.
|
||||
*/
|
||||
library FloatMath {
|
||||
/*
|
||||
* Minimum value signed 64.64-bit fixed point number may have.
|
||||
*/
|
||||
int128 private constant MIN_64x64 = -0x80000000000000000000000000000000;
|
||||
|
||||
/*
|
||||
* Maximum value signed 64.64-bit fixed point number may have.
|
||||
*/
|
||||
int128 private constant MAX_64x64 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
|
||||
|
||||
/**
|
||||
* Convert signed 256-bit integer number into signed 64.64-bit fixed point
|
||||
* number. Revert on overflow.
|
||||
*
|
||||
* @param x signed 256-bit integer number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function fromInt (int256 x) internal pure returns (int128) {
|
||||
require (x >= -0x8000000000000000 && x <= 0x7FFFFFFFFFFFFFFF);
|
||||
return int128 (x << 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed 64.64 fixed point number into signed 64-bit integer number
|
||||
* rounding down.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64-bit integer number
|
||||
*/
|
||||
function toInt (int128 x) internal pure returns (int64) {
|
||||
return int64 (x >> 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert unsigned 256-bit integer number into signed 64.64-bit fixed point
|
||||
* number. Revert on overflow.
|
||||
*
|
||||
* @param x unsigned 256-bit integer number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function fromUInt (uint256 x) internal pure returns (int128) {
|
||||
require (x <= 0x7FFFFFFFFFFFFFFF);
|
||||
return int128 (x << 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed 64.64 fixed point number into unsigned 64-bit integer
|
||||
* number rounding down. Revert on underflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return unsigned 64-bit integer number
|
||||
*/
|
||||
function toUInt (int128 x) internal pure returns (uint64) {
|
||||
require (x >= 0);
|
||||
return uint64 (x >> 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed 128.128 fixed point number into signed 64.64-bit fixed point
|
||||
* number rounding down. Revert on overflow.
|
||||
*
|
||||
* @param x signed 128.128-bin fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function from128x128 (int256 x) internal pure returns (int128) {
|
||||
int256 result = x >> 64;
|
||||
require (result >= MIN_64x64 && result <= MAX_64x64);
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed 64.64 fixed point number into signed 128.128 fixed point
|
||||
* number.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 128.128 fixed point number
|
||||
*/
|
||||
function to128x128 (int128 x) internal pure returns (int256) {
|
||||
return int256 (x) << 64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x + y. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function add (int128 x, int128 y) internal pure returns (int128) {
|
||||
int256 result = int256(x) + y;
|
||||
require (result >= MIN_64x64 && result <= MAX_64x64);
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x - y. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function sub (int128 x, int128 y) internal pure returns (int128) {
|
||||
int256 result = int256(x) - y;
|
||||
require (result >= MIN_64x64 && result <= MAX_64x64);
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x * y rounding down. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function mul (int128 x, int128 y) internal pure returns (int128) {
|
||||
int256 result = int256(x) * y >> 64;
|
||||
require (result >= MIN_64x64 && result <= MAX_64x64);
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x * y rounding towards zero, where x is signed 64.64 fixed point
|
||||
* number and y is signed 256-bit integer number. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64 fixed point number
|
||||
* @param y signed 256-bit integer number
|
||||
* @return signed 256-bit integer number
|
||||
*/
|
||||
function muli (int128 x, int256 y) internal pure returns (int256) {
|
||||
if (x == MIN_64x64) {
|
||||
require (y >= -0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF &&
|
||||
y <= 0x1000000000000000000000000000000000000000000000000);
|
||||
return -y << 63;
|
||||
} else {
|
||||
bool negativeResult = false;
|
||||
if (x < 0) {
|
||||
x = -x;
|
||||
negativeResult = true;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = -y; // We rely on overflow behavior here
|
||||
negativeResult = !negativeResult;
|
||||
}
|
||||
uint256 absoluteResult = mulu (x, uint256 (y));
|
||||
if (negativeResult) {
|
||||
require (absoluteResult <=
|
||||
0x8000000000000000000000000000000000000000000000000000000000000000);
|
||||
return -int256 (absoluteResult); // We rely on overflow behavior here
|
||||
} else {
|
||||
require (absoluteResult <=
|
||||
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
return int256 (absoluteResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x * y rounding down, where x is signed 64.64 fixed point number
|
||||
* and y is unsigned 256-bit integer number. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64 fixed point number
|
||||
* @param y unsigned 256-bit integer number
|
||||
* @return unsigned 256-bit integer number
|
||||
*/
|
||||
function mulu (int128 x, uint256 y) internal pure returns (uint256) {
|
||||
if (y == 0) return 0;
|
||||
|
||||
require (x >= 0);
|
||||
|
||||
uint256 lo = (uint256 (x) * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) >> 64;
|
||||
uint256 hi = uint256 (x) * (y >> 128);
|
||||
|
||||
require (hi <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
hi <<= 64;
|
||||
|
||||
require (hi <=
|
||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - lo);
|
||||
return hi + lo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x / y rounding towards zero. Revert on overflow or when y is
|
||||
* zero.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function div (int128 x, int128 y) internal pure returns (int128) {
|
||||
require (y != 0);
|
||||
int256 result = (int256 (x) << 64) / y;
|
||||
require (result >= MIN_64x64 && result <= MAX_64x64);
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x / y rounding towards zero, where x and y are signed 256-bit
|
||||
* integer numbers. Revert on overflow or when y is zero.
|
||||
*
|
||||
* @param x signed 256-bit integer number
|
||||
* @param y signed 256-bit integer number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function divi (int256 x, int256 y) internal pure returns (int128) {
|
||||
require (y != 0);
|
||||
|
||||
bool negativeResult = false;
|
||||
if (x < 0) {
|
||||
x = -x; // We rely on overflow behavior here
|
||||
negativeResult = true;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = -y; // We rely on overflow behavior here
|
||||
negativeResult = !negativeResult;
|
||||
}
|
||||
uint128 absoluteResult = divuu (uint256 (x), uint256 (y));
|
||||
if (negativeResult) {
|
||||
require (absoluteResult <= 0x80000000000000000000000000000000);
|
||||
return -int128 (absoluteResult); // We rely on overflow behavior here
|
||||
} else {
|
||||
require (absoluteResult <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
return int128 (absoluteResult); // We rely on overflow behavior here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x / y rounding towards zero, where x and y are unsigned 256-bit
|
||||
* integer numbers. Revert on overflow or when y is zero.
|
||||
*
|
||||
* @param x unsigned 256-bit integer number
|
||||
* @param y unsigned 256-bit integer number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function divu (uint256 x, uint256 y) internal pure returns (int128) {
|
||||
require (y != 0);
|
||||
uint128 result = divuu (x, y);
|
||||
require (result <= uint128 (MAX_64x64));
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate -x. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function neg (int128 x) internal pure returns (int128) {
|
||||
require (x != MIN_64x64);
|
||||
return -x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate |x|. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function abs (int128 x) internal pure returns (int128) {
|
||||
require (x != MIN_64x64);
|
||||
return x < 0 ? -x : x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate 1 / x rounding towards zero. Revert on overflow or when x is
|
||||
* zero.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function inv (int128 x) internal pure returns (int128) {
|
||||
require (x != 0);
|
||||
int256 result = int256 (0x100000000000000000000000000000000) / x;
|
||||
require (result >= MIN_64x64 && result <= MAX_64x64);
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate arithmetics average of x and y, i.e. (x + y) / 2 rounding down.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function avg (int128 x, int128 y) internal pure returns (int128) {
|
||||
return int128 ((int256 (x) + int256 (y)) >> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate geometric average of x and y, i.e. sqrt (x * y) rounding down.
|
||||
* Revert on overflow or in case x * y is negative.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function gavg (int128 x, int128 y) internal pure returns (int128) {
|
||||
int256 m = int256 (x) * int256 (y);
|
||||
require (m >= 0);
|
||||
require (m <
|
||||
0x4000000000000000000000000000000000000000000000000000000000000000);
|
||||
return int128 (sqrtu (uint256 (m)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x^y assuming 0^0 is 1, where x is signed 64.64 fixed point number
|
||||
* and y is unsigned 256-bit integer number. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @param y uint256 value
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function pow (int128 x, uint256 y) internal pure returns (int128) {
|
||||
uint256 absoluteResult;
|
||||
bool negativeResult = false;
|
||||
if (x >= 0) {
|
||||
absoluteResult = powu (uint256 (x) << 63, y);
|
||||
} else {
|
||||
// We rely on overflow behavior here
|
||||
absoluteResult = powu (uint256 (uint128 (-x)) << 63, y);
|
||||
negativeResult = y & 1 > 0;
|
||||
}
|
||||
|
||||
absoluteResult >>= 63;
|
||||
|
||||
if (negativeResult) {
|
||||
require (absoluteResult <= 0x80000000000000000000000000000000);
|
||||
return -int128 (absoluteResult); // We rely on overflow behavior here
|
||||
} else {
|
||||
require (absoluteResult <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
return int128 (absoluteResult); // We rely on overflow behavior here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate sqrt (x) rounding down. Revert if x < 0.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function sqrt (int128 x) internal pure returns (int128) {
|
||||
require (x >= 0);
|
||||
return int128 (sqrtu (uint256 (x) << 64));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate binary logarithm of x. Revert if x <= 0.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function log_2 (int128 x) internal pure returns (int128) {
|
||||
require (x > 0);
|
||||
|
||||
int256 msb = 0;
|
||||
int256 xc = x;
|
||||
if (xc >= 0x10000000000000000) { xc >>= 64; msb += 64; }
|
||||
if (xc >= 0x100000000) { xc >>= 32; msb += 32; }
|
||||
if (xc >= 0x10000) { xc >>= 16; msb += 16; }
|
||||
if (xc >= 0x100) { xc >>= 8; msb += 8; }
|
||||
if (xc >= 0x10) { xc >>= 4; msb += 4; }
|
||||
if (xc >= 0x4) { xc >>= 2; msb += 2; }
|
||||
if (xc >= 0x2) msb += 1; // No need to shift xc anymore
|
||||
|
||||
int256 result = msb - 64 << 64;
|
||||
uint256 ux = uint256 (x) << uint256 (127 - msb);
|
||||
for (int256 bit = 0x8000000000000000; bit > 0; bit >>= 1) {
|
||||
ux *= ux;
|
||||
uint256 b = ux >> 255;
|
||||
ux >>= 127 + b;
|
||||
result += bit * int256 (b);
|
||||
}
|
||||
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate natural logarithm of x. Revert if x <= 0.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function ln (int128 x) internal pure returns (int128) {
|
||||
require (x > 0);
|
||||
|
||||
return int128 (
|
||||
uint256 (log_2 (x)) * 0xB17217F7D1CF79ABC9E3B39803F2F6AF >> 128);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate binary exponent of x. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function exp_2 (int128 x) internal pure returns (int128) {
|
||||
require (x < 0x400000000000000000); // Overflow
|
||||
|
||||
if (x < -0x400000000000000000) return 0; // Underflow
|
||||
|
||||
uint256 result = 0x80000000000000000000000000000000;
|
||||
|
||||
if (x & 0x8000000000000000 > 0)
|
||||
result = result * 0x16A09E667F3BCC908B2FB1366EA957D3E >> 128;
|
||||
if (x & 0x4000000000000000 > 0)
|
||||
result = result * 0x1306FE0A31B7152DE8D5A46305C85EDEC >> 128;
|
||||
if (x & 0x2000000000000000 > 0)
|
||||
result = result * 0x1172B83C7D517ADCDF7C8C50EB14A791F >> 128;
|
||||
if (x & 0x1000000000000000 > 0)
|
||||
result = result * 0x10B5586CF9890F6298B92B71842A98363 >> 128;
|
||||
if (x & 0x800000000000000 > 0)
|
||||
result = result * 0x1059B0D31585743AE7C548EB68CA417FD >> 128;
|
||||
if (x & 0x400000000000000 > 0)
|
||||
result = result * 0x102C9A3E778060EE6F7CACA4F7A29BDE8 >> 128;
|
||||
if (x & 0x200000000000000 > 0)
|
||||
result = result * 0x10163DA9FB33356D84A66AE336DCDFA3F >> 128;
|
||||
if (x & 0x100000000000000 > 0)
|
||||
result = result * 0x100B1AFA5ABCBED6129AB13EC11DC9543 >> 128;
|
||||
if (x & 0x80000000000000 > 0)
|
||||
result = result * 0x10058C86DA1C09EA1FF19D294CF2F679B >> 128;
|
||||
if (x & 0x40000000000000 > 0)
|
||||
result = result * 0x1002C605E2E8CEC506D21BFC89A23A00F >> 128;
|
||||
if (x & 0x20000000000000 > 0)
|
||||
result = result * 0x100162F3904051FA128BCA9C55C31E5DF >> 128;
|
||||
if (x & 0x10000000000000 > 0)
|
||||
result = result * 0x1000B175EFFDC76BA38E31671CA939725 >> 128;
|
||||
if (x & 0x8000000000000 > 0)
|
||||
result = result * 0x100058BA01FB9F96D6CACD4B180917C3D >> 128;
|
||||
if (x & 0x4000000000000 > 0)
|
||||
result = result * 0x10002C5CC37DA9491D0985C348C68E7B3 >> 128;
|
||||
if (x & 0x2000000000000 > 0)
|
||||
result = result * 0x1000162E525EE054754457D5995292026 >> 128;
|
||||
if (x & 0x1000000000000 > 0)
|
||||
result = result * 0x10000B17255775C040618BF4A4ADE83FC >> 128;
|
||||
if (x & 0x800000000000 > 0)
|
||||
result = result * 0x1000058B91B5BC9AE2EED81E9B7D4CFAB >> 128;
|
||||
if (x & 0x400000000000 > 0)
|
||||
result = result * 0x100002C5C89D5EC6CA4D7C8ACC017B7C9 >> 128;
|
||||
if (x & 0x200000000000 > 0)
|
||||
result = result * 0x10000162E43F4F831060E02D839A9D16D >> 128;
|
||||
if (x & 0x100000000000 > 0)
|
||||
result = result * 0x100000B1721BCFC99D9F890EA06911763 >> 128;
|
||||
if (x & 0x80000000000 > 0)
|
||||
result = result * 0x10000058B90CF1E6D97F9CA14DBCC1628 >> 128;
|
||||
if (x & 0x40000000000 > 0)
|
||||
result = result * 0x1000002C5C863B73F016468F6BAC5CA2B >> 128;
|
||||
if (x & 0x20000000000 > 0)
|
||||
result = result * 0x100000162E430E5A18F6119E3C02282A5 >> 128;
|
||||
if (x & 0x10000000000 > 0)
|
||||
result = result * 0x1000000B1721835514B86E6D96EFD1BFE >> 128;
|
||||
if (x & 0x8000000000 > 0)
|
||||
result = result * 0x100000058B90C0B48C6BE5DF846C5B2EF >> 128;
|
||||
if (x & 0x4000000000 > 0)
|
||||
result = result * 0x10000002C5C8601CC6B9E94213C72737A >> 128;
|
||||
if (x & 0x2000000000 > 0)
|
||||
result = result * 0x1000000162E42FFF037DF38AA2B219F06 >> 128;
|
||||
if (x & 0x1000000000 > 0)
|
||||
result = result * 0x10000000B17217FBA9C739AA5819F44F9 >> 128;
|
||||
if (x & 0x800000000 > 0)
|
||||
result = result * 0x1000000058B90BFCDEE5ACD3C1CEDC823 >> 128;
|
||||
if (x & 0x400000000 > 0)
|
||||
result = result * 0x100000002C5C85FE31F35A6A30DA1BE50 >> 128;
|
||||
if (x & 0x200000000 > 0)
|
||||
result = result * 0x10000000162E42FF0999CE3541B9FFFCF >> 128;
|
||||
if (x & 0x100000000 > 0)
|
||||
result = result * 0x100000000B17217F80F4EF5AADDA45554 >> 128;
|
||||
if (x & 0x80000000 > 0)
|
||||
result = result * 0x10000000058B90BFBF8479BD5A81B51AD >> 128;
|
||||
if (x & 0x40000000 > 0)
|
||||
result = result * 0x1000000002C5C85FDF84BD62AE30A74CC >> 128;
|
||||
if (x & 0x20000000 > 0)
|
||||
result = result * 0x100000000162E42FEFB2FED257559BDAA >> 128;
|
||||
if (x & 0x10000000 > 0)
|
||||
result = result * 0x1000000000B17217F7D5A7716BBA4A9AE >> 128;
|
||||
if (x & 0x8000000 > 0)
|
||||
result = result * 0x100000000058B90BFBE9DDBAC5E109CCE >> 128;
|
||||
if (x & 0x4000000 > 0)
|
||||
result = result * 0x10000000002C5C85FDF4B15DE6F17EB0D >> 128;
|
||||
if (x & 0x2000000 > 0)
|
||||
result = result * 0x1000000000162E42FEFA494F1478FDE05 >> 128;
|
||||
if (x & 0x1000000 > 0)
|
||||
result = result * 0x10000000000B17217F7D20CF927C8E94C >> 128;
|
||||
if (x & 0x800000 > 0)
|
||||
result = result * 0x1000000000058B90BFBE8F71CB4E4B33D >> 128;
|
||||
if (x & 0x400000 > 0)
|
||||
result = result * 0x100000000002C5C85FDF477B662B26945 >> 128;
|
||||
if (x & 0x200000 > 0)
|
||||
result = result * 0x10000000000162E42FEFA3AE53369388C >> 128;
|
||||
if (x & 0x100000 > 0)
|
||||
result = result * 0x100000000000B17217F7D1D351A389D40 >> 128;
|
||||
if (x & 0x80000 > 0)
|
||||
result = result * 0x10000000000058B90BFBE8E8B2D3D4EDE >> 128;
|
||||
if (x & 0x40000 > 0)
|
||||
result = result * 0x1000000000002C5C85FDF4741BEA6E77E >> 128;
|
||||
if (x & 0x20000 > 0)
|
||||
result = result * 0x100000000000162E42FEFA39FE95583C2 >> 128;
|
||||
if (x & 0x10000 > 0)
|
||||
result = result * 0x1000000000000B17217F7D1CFB72B45E1 >> 128;
|
||||
if (x & 0x8000 > 0)
|
||||
result = result * 0x100000000000058B90BFBE8E7CC35C3F0 >> 128;
|
||||
if (x & 0x4000 > 0)
|
||||
result = result * 0x10000000000002C5C85FDF473E242EA38 >> 128;
|
||||
if (x & 0x2000 > 0)
|
||||
result = result * 0x1000000000000162E42FEFA39F02B772C >> 128;
|
||||
if (x & 0x1000 > 0)
|
||||
result = result * 0x10000000000000B17217F7D1CF7D83C1A >> 128;
|
||||
if (x & 0x800 > 0)
|
||||
result = result * 0x1000000000000058B90BFBE8E7BDCBE2E >> 128;
|
||||
if (x & 0x400 > 0)
|
||||
result = result * 0x100000000000002C5C85FDF473DEA871F >> 128;
|
||||
if (x & 0x200 > 0)
|
||||
result = result * 0x10000000000000162E42FEFA39EF44D91 >> 128;
|
||||
if (x & 0x100 > 0)
|
||||
result = result * 0x100000000000000B17217F7D1CF79E949 >> 128;
|
||||
if (x & 0x80 > 0)
|
||||
result = result * 0x10000000000000058B90BFBE8E7BCE544 >> 128;
|
||||
if (x & 0x40 > 0)
|
||||
result = result * 0x1000000000000002C5C85FDF473DE6ECA >> 128;
|
||||
if (x & 0x20 > 0)
|
||||
result = result * 0x100000000000000162E42FEFA39EF366F >> 128;
|
||||
if (x & 0x10 > 0)
|
||||
result = result * 0x1000000000000000B17217F7D1CF79AFA >> 128;
|
||||
if (x & 0x8 > 0)
|
||||
result = result * 0x100000000000000058B90BFBE8E7BCD6D >> 128;
|
||||
if (x & 0x4 > 0)
|
||||
result = result * 0x10000000000000002C5C85FDF473DE6B2 >> 128;
|
||||
if (x & 0x2 > 0)
|
||||
result = result * 0x1000000000000000162E42FEFA39EF358 >> 128;
|
||||
if (x & 0x1 > 0)
|
||||
result = result * 0x10000000000000000B17217F7D1CF79AB >> 128;
|
||||
|
||||
result >>= uint256 (63 - (x >> 64));
|
||||
require (result <= uint256 (MAX_64x64));
|
||||
|
||||
return int128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate natural exponent of x. Revert on overflow.
|
||||
*
|
||||
* @param x signed 64.64-bit fixed point number
|
||||
* @return signed 64.64-bit fixed point number
|
||||
*/
|
||||
function exp (int128 x) internal pure returns (int128) {
|
||||
require (x < 0x400000000000000000); // Overflow
|
||||
|
||||
if (x < -0x400000000000000000) return 0; // Underflow
|
||||
|
||||
return exp_2 (
|
||||
int128 (int256 (x) * 0x171547652B82FE1777D0FFDA0D23A7D12 >> 128));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x / y rounding towards zero, where x and y are unsigned 256-bit
|
||||
* integer numbers. Revert on overflow or when y is zero.
|
||||
*
|
||||
* @param x unsigned 256-bit integer number
|
||||
* @param y unsigned 256-bit integer number
|
||||
* @return unsigned 64.64-bit fixed point number
|
||||
*/
|
||||
function divuu (uint256 x, uint256 y) private pure returns (uint128) {
|
||||
require (y != 0);
|
||||
|
||||
uint256 result;
|
||||
|
||||
if (x <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
|
||||
result = (x << 64) / y;
|
||||
else {
|
||||
uint256 msb = 192;
|
||||
uint256 xc = x >> 192;
|
||||
if (xc >= 0x100000000) { xc >>= 32; msb += 32; }
|
||||
if (xc >= 0x10000) { xc >>= 16; msb += 16; }
|
||||
if (xc >= 0x100) { xc >>= 8; msb += 8; }
|
||||
if (xc >= 0x10) { xc >>= 4; msb += 4; }
|
||||
if (xc >= 0x4) { xc >>= 2; msb += 2; }
|
||||
if (xc >= 0x2) msb += 1; // No need to shift xc anymore
|
||||
|
||||
result = (x << 255 - msb) / ((y - 1 >> msb - 191) + 1);
|
||||
require (result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
|
||||
uint256 hi = result * (y >> 128);
|
||||
uint256 lo = result * (y & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
|
||||
uint256 xh = x >> 192;
|
||||
uint256 xl = x << 64;
|
||||
|
||||
if (xl < lo) xh -= 1;
|
||||
xl -= lo; // We rely on overflow behavior here
|
||||
lo = hi << 128;
|
||||
if (xl < lo) xh -= 1;
|
||||
xl -= lo; // We rely on overflow behavior here
|
||||
|
||||
assert (xh == hi >> 128);
|
||||
|
||||
result += xl / y;
|
||||
}
|
||||
|
||||
require (result <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
|
||||
return uint128 (result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate x^y assuming 0^0 is 1, where x is unsigned 129.127 fixed point
|
||||
* number and y is unsigned 256-bit integer number. Revert on overflow.
|
||||
*
|
||||
* @param x unsigned 129.127-bit fixed point number
|
||||
* @param y uint256 value
|
||||
* @return unsigned 129.127-bit fixed point number
|
||||
*/
|
||||
function powu (uint256 x, uint256 y) private pure returns (uint256) {
|
||||
if (y == 0) return 0x80000000000000000000000000000000;
|
||||
else if (x == 0) return 0;
|
||||
else {
|
||||
int256 msb = 0;
|
||||
uint256 xc = x;
|
||||
if (xc >= 0x100000000000000000000000000000000) { xc >>= 128; msb += 128; }
|
||||
if (xc >= 0x10000000000000000) { xc >>= 64; msb += 64; }
|
||||
if (xc >= 0x100000000) { xc >>= 32; msb += 32; }
|
||||
if (xc >= 0x10000) { xc >>= 16; msb += 16; }
|
||||
if (xc >= 0x100) { xc >>= 8; msb += 8; }
|
||||
if (xc >= 0x10) { xc >>= 4; msb += 4; }
|
||||
if (xc >= 0x4) { xc >>= 2; msb += 2; }
|
||||
if (xc >= 0x2) msb += 1; // No need to shift xc anymore
|
||||
|
||||
int256 xe = msb - 127;
|
||||
if (xe > 0) x >>= uint256 (xe);
|
||||
else x <<= uint256 (-xe);
|
||||
|
||||
uint256 result = 0x80000000000000000000000000000000;
|
||||
int256 re = 0;
|
||||
|
||||
while (y > 0) {
|
||||
if (y & 1 > 0) {
|
||||
result = result * x;
|
||||
y -= 1;
|
||||
re += xe;
|
||||
if (result >=
|
||||
0x8000000000000000000000000000000000000000000000000000000000000000) {
|
||||
result >>= 128;
|
||||
re += 1;
|
||||
} else result >>= 127;
|
||||
if (re < -127) return 0; // Underflow
|
||||
require (re < 128); // Overflow
|
||||
} else {
|
||||
x = x * x;
|
||||
y >>= 1;
|
||||
xe <<= 1;
|
||||
if (x >=
|
||||
0x8000000000000000000000000000000000000000000000000000000000000000) {
|
||||
x >>= 128;
|
||||
xe += 1;
|
||||
} else x >>= 127;
|
||||
if (xe < -127) return 0; // Underflow
|
||||
require (xe < 128); // Overflow
|
||||
}
|
||||
}
|
||||
|
||||
if (re > 0) result <<= uint256 (re);
|
||||
else if (re < 0) result >>= uint256 (-re);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate sqrt (x) rounding down, where x is unsigned 256-bit integer
|
||||
* number.
|
||||
*
|
||||
* @param x unsigned 256-bit integer number
|
||||
* @return unsigned 128-bit integer number
|
||||
*/
|
||||
function sqrtu (uint256 x) private pure returns (uint128) {
|
||||
if (x == 0) return 0;
|
||||
else {
|
||||
uint256 xx = x;
|
||||
uint256 r = 1;
|
||||
if (xx >= 0x100000000000000000000000000000000) { xx >>= 128; r <<= 64; }
|
||||
if (xx >= 0x10000000000000000) { xx >>= 64; r <<= 32; }
|
||||
if (xx >= 0x100000000) { xx >>= 32; r <<= 16; }
|
||||
if (xx >= 0x10000) { xx >>= 16; r <<= 8; }
|
||||
if (xx >= 0x100) { xx >>= 8; r <<= 4; }
|
||||
if (xx >= 0x10) { xx >>= 4; r <<= 2; }
|
||||
if (xx >= 0x8) { r <<= 1; }
|
||||
r = (r + x / r) >> 1;
|
||||
r = (r + x / r) >> 1;
|
||||
r = (r + x / r) >> 1;
|
||||
r = (r + x / r) >> 1;
|
||||
r = (r + x / r) >> 1;
|
||||
r = (r + x / r) >> 1;
|
||||
r = (r + x / r) >> 1; // Seven iterations should be enough
|
||||
uint256 r1 = x / r;
|
||||
return uint128 (r < r1 ? r : r1);
|
||||
}
|
||||
}
|
||||
}
|
136
contracts/utils/MerkleTreeWithHistory.sol
Normal file
136
contracts/utils/MerkleTreeWithHistory.sol
Normal file
@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
import "../interfaces/IHasher.sol";
|
||||
|
||||
contract MerkleTreeWithHistory {
|
||||
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||
uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
|
||||
|
||||
uint32 public immutable levels;
|
||||
IHasher public hasher; // todo immutable
|
||||
|
||||
bytes32[] public filledSubtrees;
|
||||
bytes32[] public zeros;
|
||||
uint32 public currentRootIndex = 0;
|
||||
uint32 public nextIndex = 0;
|
||||
uint32 public constant ROOT_HISTORY_SIZE = 10;
|
||||
bytes32[ROOT_HISTORY_SIZE] public roots;
|
||||
|
||||
constructor(uint32 _treeLevels, IHasher _hasher) public {
|
||||
require(_treeLevels > 0, "_treeLevels should be greater than zero");
|
||||
require(_treeLevels < 32, "_treeLevels should be less than 32");
|
||||
levels = _treeLevels;
|
||||
hasher = _hasher;
|
||||
|
||||
bytes32 currentZero = bytes32(ZERO_VALUE);
|
||||
zeros.push(currentZero);
|
||||
filledSubtrees.push(currentZero);
|
||||
|
||||
for (uint32 i = 1; i < _treeLevels; i++) {
|
||||
currentZero = hashLeftRight(currentZero, currentZero);
|
||||
zeros.push(currentZero);
|
||||
filledSubtrees.push(currentZero);
|
||||
}
|
||||
|
||||
filledSubtrees.push(hashLeftRight(currentZero, currentZero));
|
||||
roots[0] = filledSubtrees[_treeLevels];
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Hash 2 tree leaves, returns poseidon(_left, _right)
|
||||
*/
|
||||
function hashLeftRight(bytes32 _left, bytes32 _right) public view returns (bytes32) {
|
||||
return hasher.poseidon([_left, _right]);
|
||||
}
|
||||
|
||||
function _insert(bytes32 _leaf) internal returns (uint32 index) {
|
||||
uint32 currentIndex = nextIndex;
|
||||
require(currentIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added");
|
||||
nextIndex = currentIndex + 1;
|
||||
bytes32 currentLevelHash = _leaf;
|
||||
bytes32 left;
|
||||
bytes32 right;
|
||||
|
||||
for (uint32 i = 0; i < levels; i++) {
|
||||
if (currentIndex % 2 == 0) {
|
||||
left = currentLevelHash;
|
||||
right = zeros[i];
|
||||
filledSubtrees[i] = currentLevelHash;
|
||||
} else {
|
||||
left = filledSubtrees[i];
|
||||
right = currentLevelHash;
|
||||
}
|
||||
|
||||
currentLevelHash = hashLeftRight(left, right);
|
||||
currentIndex /= 2;
|
||||
}
|
||||
|
||||
currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
|
||||
roots[currentRootIndex] = currentLevelHash;
|
||||
return nextIndex - 1;
|
||||
}
|
||||
|
||||
function _bulkInsert(bytes32[] memory _leaves) internal {
|
||||
uint32 insertIndex = nextIndex;
|
||||
require(insertIndex + _leaves.length <= uint32(2)**levels, "Merkle doesn't have enough capacity to add specified leaves");
|
||||
|
||||
bytes32[] memory subtrees = new bytes32[](levels);
|
||||
bool[] memory modifiedSubtrees = new bool[](levels);
|
||||
for (uint32 j = 0; j < _leaves.length - 1; j++) {
|
||||
uint256 index = insertIndex + j;
|
||||
bytes32 currentLevelHash = _leaves[j];
|
||||
|
||||
for (uint32 i = 0; ; i++) {
|
||||
if (index % 2 == 0) {
|
||||
modifiedSubtrees[i] = true;
|
||||
subtrees[i] = currentLevelHash;
|
||||
break;
|
||||
}
|
||||
|
||||
if(subtrees[i] == bytes32(0)) {
|
||||
subtrees[i] = filledSubtrees[i];
|
||||
}
|
||||
currentLevelHash = hashLeftRight(subtrees[i], currentLevelHash);
|
||||
index /= 2;
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32 i = 0; i < levels; i++) {
|
||||
// using local map to save on gas on writes if elements were not modified
|
||||
if (modifiedSubtrees[i]) {
|
||||
filledSubtrees[i] = subtrees[i];
|
||||
}
|
||||
}
|
||||
|
||||
nextIndex = uint32(insertIndex + _leaves.length - 1);
|
||||
_insert(_leaves[_leaves.length - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Whether the root is present in the root history
|
||||
*/
|
||||
function isKnownRoot(bytes32 _root) public view returns (bool) {
|
||||
if (_root == 0) {
|
||||
return false;
|
||||
}
|
||||
uint32 i = currentRootIndex;
|
||||
do {
|
||||
if (_root == roots[i]) {
|
||||
return true;
|
||||
}
|
||||
if (i == 0) {
|
||||
i = ROOT_HISTORY_SIZE;
|
||||
}
|
||||
i--;
|
||||
} while (i != currentRootIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev Returns the last root
|
||||
*/
|
||||
function getLastRoot() public view returns (bytes32) {
|
||||
return roots[currentRootIndex];
|
||||
}
|
||||
}
|
17
contracts/utils/OwnableMerkleTree.sol
Normal file
17
contracts/utils/OwnableMerkleTree.sol
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "./MerkleTreeWithHistory.sol";
|
||||
|
||||
contract OwnableMerkleTree is Ownable, MerkleTreeWithHistory {
|
||||
constructor(uint32 _treeLevels, IHasher _hasher) public MerkleTreeWithHistory(_treeLevels, _hasher) {}
|
||||
|
||||
function insert(bytes32 _leaf) external onlyOwner returns (uint32 index) {
|
||||
return _insert(_leaf);
|
||||
}
|
||||
|
||||
function bulkInsert(bytes32[] calldata _leaves) external onlyOwner {
|
||||
_bulkInsert(_leaves);
|
||||
}
|
||||
}
|
1
contracts/verifiers/RewardVerifier.sol
Symbolic link
1
contracts/verifiers/RewardVerifier.sol
Symbolic link
@ -0,0 +1 @@
|
||||
../../build/circuits/RewardVerifier.sol
|
1
contracts/verifiers/TreeUpdateVerifier.sol
Symbolic link
1
contracts/verifiers/TreeUpdateVerifier.sol
Symbolic link
@ -0,0 +1 @@
|
||||
../../build/circuits/TreeUpdateVerifier.sol
|
1
contracts/verifiers/WithdrawVerifier.sol
Symbolic link
1
contracts/verifiers/WithdrawVerifier.sol
Symbolic link
@ -0,0 +1 @@
|
||||
../../build/circuits/WithdrawVerifier.sol
|
6
index.js
Normal file
6
index.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
Note: require('./src/note'),
|
||||
Account: require('./src/account'),
|
||||
Controller: require('./src/controller'),
|
||||
Utils: require('./src/utils'),
|
||||
}
|
62
package.json
Normal file
62
package.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "tornado-cash-anonymity-mining",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/tornadocash/tornado-anonymity-mining.git",
|
||||
"author": "Tornadocash team <hello@tornado.cash>",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"index.js",
|
||||
"src/*",
|
||||
"contracts/*"
|
||||
],
|
||||
"scripts": {
|
||||
"compile": "truffle compile",
|
||||
"coverage": "yarn compile && truffle run coverage",
|
||||
"test": "truffle test",
|
||||
"test:stacktrace": "yarn test --stacktrace",
|
||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||
"prettier:check": "prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||
"lint": "yarn eslint && yarn prettier:check",
|
||||
"deploy:mainnet": "truffle migrate --network mainnet",
|
||||
"deploy:kovan": "truffle migrate --network kovan",
|
||||
"deploy:dev": "truffle migrate --skip-dry-run --network development",
|
||||
"verify": "truffle run verify --network $NETWORK",
|
||||
"circuit:reward": "scripts/buildCircuit.sh Reward",
|
||||
"circuit:withdraw": "scripts/buildCircuit.sh Withdraw",
|
||||
"circuit:treeUpdate": "scripts/buildCircuit.sh TreeUpdate",
|
||||
"circuit": "mkdir -p build/circuits && yarn circuit:reward && yarn circuit:withdraw && yarn circuit:treeUpdate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openzeppelin/contracts": "^3.1.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bn-chai": "^1.0.1",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"circom": "0.0.35",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"prettier": "^2.1.2",
|
||||
"prettier-plugin-solidity": "^1.0.0-alpha.59",
|
||||
"rlp": "^2.2.6",
|
||||
"solhint-plugin-prettier": "^0.0.4",
|
||||
"solidity-coverage": "^0.7.7",
|
||||
"torn-token": "git+https://github.com/tornadocash/torn-token.git#87743ba239c191baf8d351498d4007dfcd16d3d2",
|
||||
"truffle": "^5.1.29",
|
||||
"truffle-flattener": "^1.4.4",
|
||||
"truffle-hdwallet-provider": "^1.0.17",
|
||||
"truffle-plugin-verify": "^0.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"circomlib": "git+https://github.com/tornadocash/circomlib.git#3b492f9801573eebcfe1b6c584afe8a3beecf2b4",
|
||||
"decimal.js": "^10.2.0",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"fixed-merkle-tree": "^0.3.4",
|
||||
"snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
|
||||
"web3": "^1.2.11",
|
||||
"websnark": "git+https://github.com/tornadocash/websnark.git#86a526718cd6f6f5d31bdb1fe26a9ec8819f633e"
|
||||
}
|
||||
}
|
8
scripts/buildCircuit.sh
Executable file
8
scripts/buildCircuit.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
npx circom circuits/$1.circom -o build/circuits/$1.json
|
||||
npx snarkjs info -c build/circuits/$1.json
|
||||
zkutil setup -c build/circuits/$1.json -p build/circuits/$1.params
|
||||
zkutil export-keys -c build/circuits/$1.json -p build/circuits/$1.params --pk build/circuits/$1_proving_key.json --vk build/circuits/$1_verification_key.json
|
||||
node node_modules/websnark/tools/buildpkey.js -i build/circuits/$1_proving_key.json -o build/circuits/$1_proving_key.bin
|
||||
zkutil generate-verifier -p build/circuits/$1.params -v build/circuits/${1}Verifier.sol
|
||||
sed -i.bak "s/contract Verifier/contract ${1}Verifier/g" build/circuits/${1}Verifier.sol
|
55
scripts/ganacheHelper.js
Normal file
55
scripts/ganacheHelper.js
Normal file
@ -0,0 +1,55 @@
|
||||
// This module is used only for tests
|
||||
function send(method, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
web3.currentProvider.send(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method,
|
||||
params,
|
||||
},
|
||||
(err, res) => {
|
||||
return err ? reject(err) : resolve(res)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const takeSnapshot = async () => {
|
||||
return await send('evm_snapshot')
|
||||
}
|
||||
|
||||
const traceTransaction = async (tx) => {
|
||||
return await send('debug_traceTransaction', [tx, {}])
|
||||
}
|
||||
|
||||
const revertSnapshot = async (id) => {
|
||||
await send('evm_revert', [id])
|
||||
}
|
||||
|
||||
const mineBlock = async (timestamp) => {
|
||||
await send('evm_mine', [timestamp])
|
||||
}
|
||||
|
||||
const increaseTime = async (seconds) => {
|
||||
await send('evm_increaseTime', [seconds])
|
||||
}
|
||||
|
||||
const minerStop = async () => {
|
||||
await send('miner_stop', [])
|
||||
}
|
||||
|
||||
const minerStart = async () => {
|
||||
await send('miner_start', [])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
takeSnapshot,
|
||||
revertSnapshot,
|
||||
mineBlock,
|
||||
minerStop,
|
||||
minerStart,
|
||||
increaseTime,
|
||||
traceTransaction,
|
||||
}
|
39
src/account.js
Normal file
39
src/account.js
Normal file
@ -0,0 +1,39 @@
|
||||
const { toBN } = require('web3-utils')
|
||||
const { encrypt, decrypt } = require('eth-sig-util')
|
||||
const { randomBN, poseidonHash } = require('./utils')
|
||||
|
||||
class Account {
|
||||
constructor({ amount, secret, nullifier } = {}) {
|
||||
this.amount = amount ? toBN(amount) : toBN('0')
|
||||
this.secret = secret ? toBN(secret) : randomBN(31)
|
||||
this.nullifier = nullifier ? toBN(nullifier) : randomBN(31)
|
||||
|
||||
this.commitment = poseidonHash([this.amount, this.secret, this.nullifier])
|
||||
this.nullifierHash = poseidonHash([this.nullifier])
|
||||
|
||||
if (this.amount.lt(toBN(0))) {
|
||||
throw new Error('Cannot create an account with negative amount')
|
||||
}
|
||||
}
|
||||
|
||||
encrypt(pubkey) {
|
||||
const bytes = Buffer.concat([
|
||||
this.amount.toBuffer('be', 31),
|
||||
this.secret.toBuffer('be', 31),
|
||||
this.nullifier.toBuffer('be', 31),
|
||||
])
|
||||
return encrypt(pubkey, { data: bytes.toString('base64') }, 'x25519-xsalsa20-poly1305')
|
||||
}
|
||||
|
||||
static decrypt(privkey, data) {
|
||||
const decryptedMessage = decrypt(data, privkey)
|
||||
const buf = Buffer.from(decryptedMessage, 'base64')
|
||||
return new Account({
|
||||
amount: toBN('0x' + buf.slice(0, 31).toString('hex')),
|
||||
secret: toBN('0x' + buf.slice(31, 62).toString('hex')),
|
||||
nullifier: toBN('0x' + buf.slice(62, 93).toString('hex')),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Account
|
329
src/controller.js
Normal file
329
src/controller.js
Normal file
@ -0,0 +1,329 @@
|
||||
const { toBN } = require('web3-utils')
|
||||
const Web3 = require('web3')
|
||||
const {
|
||||
bitsToNumber,
|
||||
toFixedHex,
|
||||
poseidonHash,
|
||||
poseidonHash2,
|
||||
getExtRewardArgsHash,
|
||||
getExtWithdrawArgsHash,
|
||||
packEncryptedMessage,
|
||||
RewardArgs,
|
||||
} = require('./utils')
|
||||
const Account = require('./account')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
const websnarkUtils = require('websnark/src/utils')
|
||||
const buildGroth16 = require('websnark/src/groth16')
|
||||
|
||||
const web3 = new Web3()
|
||||
|
||||
class Controller {
|
||||
constructor({ contract, tornadoTreesContract, merkleTreeHeight, provingKeys, groth16 }) {
|
||||
this.merkleTreeHeight = Number(merkleTreeHeight)
|
||||
this.provingKeys = provingKeys
|
||||
this.contract = contract
|
||||
this.tornadoTreesContract = tornadoTreesContract
|
||||
this.groth16 = groth16
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.groth16 = await buildGroth16()
|
||||
}
|
||||
|
||||
async _fetchAccountCommitments() {
|
||||
const events = await this.contract.getPastEvents('NewAccount', {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
})
|
||||
return events
|
||||
.sort((a, b) => a.returnValues.index - b.returnValues.index)
|
||||
.map((e) => toBN(e.returnValues.commitment))
|
||||
}
|
||||
|
||||
_fetchDepositDataEvents() {
|
||||
return this._fetchEvents('DepositData')
|
||||
}
|
||||
|
||||
_fetchWithdrawalDataEvents() {
|
||||
return this._fetchEvents('WithdrawalData')
|
||||
}
|
||||
|
||||
async _fetchEvents(eventName) {
|
||||
const events = await this.tornadoTreesContract.getPastEvents(eventName, {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
})
|
||||
return events
|
||||
.sort((a, b) => a.returnValues.index - b.returnValues.index)
|
||||
.map((e) => ({
|
||||
instance: toFixedHex(e.returnValues.instance, 20),
|
||||
hash: toFixedHex(e.returnValues.hash),
|
||||
block: Number(e.returnValues.block),
|
||||
index: Number(e.returnValues.index),
|
||||
}))
|
||||
}
|
||||
|
||||
_updateTree(tree, element) {
|
||||
const oldRoot = tree.root()
|
||||
tree.insert(element)
|
||||
const newRoot = tree.root()
|
||||
const { pathElements, pathIndices } = tree.path(tree.elements().length - 1)
|
||||
return {
|
||||
oldRoot,
|
||||
newRoot,
|
||||
pathElements,
|
||||
pathIndices: bitsToNumber(pathIndices),
|
||||
}
|
||||
}
|
||||
|
||||
async batchReward({ account, notes, publicKey, fee = 0, relayer = 0 }) {
|
||||
const accountCommitments = await this._fetchAccountCommitments()
|
||||
let lastAccount = account
|
||||
const proofs = []
|
||||
for (const note of notes) {
|
||||
const proof = await this.reward({
|
||||
account: lastAccount,
|
||||
note,
|
||||
publicKey,
|
||||
fee,
|
||||
relayer,
|
||||
accountCommitments: accountCommitments.slice(),
|
||||
})
|
||||
proofs.push(proof)
|
||||
lastAccount = proof.account
|
||||
accountCommitments.push(lastAccount.commitment)
|
||||
}
|
||||
const args = proofs.map((x) => web3.eth.abi.encodeParameters(['bytes', RewardArgs], [x.proof, x.args]))
|
||||
return { proofs, args }
|
||||
}
|
||||
|
||||
async reward({ account, note, publicKey, fee = 0, relayer = 0, accountCommitments = null }) {
|
||||
const rate = await this.contract.methods.rates(note.instance).call()
|
||||
|
||||
const newAmount = account.amount.add(
|
||||
toBN(rate)
|
||||
.mul(toBN(note.withdrawalBlock).sub(toBN(note.depositBlock)))
|
||||
.sub(toBN(fee)),
|
||||
)
|
||||
const newAccount = new Account({ amount: newAmount })
|
||||
|
||||
const depositDataEvents = await this._fetchDepositDataEvents()
|
||||
const depositLeaves = depositDataEvents.map((x) => poseidonHash([x.instance, x.hash, x.block]))
|
||||
const depositTree = new MerkleTree(this.merkleTreeHeight, depositLeaves, { hashFunction: poseidonHash2 })
|
||||
const depositItem = depositDataEvents.filter((x) => x.hash === toFixedHex(note.commitment))
|
||||
if (depositItem.length === 0) {
|
||||
throw new Error('The deposits tree does not contain such note commitment')
|
||||
}
|
||||
const depositPath = depositTree.path(depositItem[0].index)
|
||||
|
||||
const withdrawalDataEvents = await this._fetchWithdrawalDataEvents()
|
||||
const withdrawalLeaves = withdrawalDataEvents.map((x) => poseidonHash([x.instance, x.hash, x.block]))
|
||||
const withdrawalTree = new MerkleTree(this.merkleTreeHeight, withdrawalLeaves, {
|
||||
hashFunction: poseidonHash2,
|
||||
})
|
||||
const withdrawalItem = withdrawalDataEvents.filter((x) => x.hash === toFixedHex(note.nullifierHash))
|
||||
if (withdrawalItem.length === 0) {
|
||||
throw new Error('The withdrawals tree does not contain such note nullifier')
|
||||
}
|
||||
const withdrawalPath = withdrawalTree.path(withdrawalItem[0].index)
|
||||
|
||||
accountCommitments = accountCommitments || (await this._fetchAccountCommitments())
|
||||
const accountTree = new MerkleTree(this.merkleTreeHeight, accountCommitments, {
|
||||
hashFunction: poseidonHash2,
|
||||
})
|
||||
const zeroAccount = {
|
||||
pathElements: new Array(this.merkleTreeHeight).fill(0),
|
||||
pathIndices: new Array(this.merkleTreeHeight).fill(0),
|
||||
}
|
||||
const accountIndex = accountTree.indexOf(account.commitment, (a, b) => a.eq(b))
|
||||
const accountPath = accountIndex !== -1 ? accountTree.path(accountIndex) : zeroAccount
|
||||
const accountTreeUpdate = this._updateTree(accountTree, newAccount.commitment)
|
||||
|
||||
const encryptedAccount = packEncryptedMessage(newAccount.encrypt(publicKey))
|
||||
const extDataHash = getExtRewardArgsHash({ relayer, encryptedAccount })
|
||||
|
||||
const input = {
|
||||
rate,
|
||||
fee,
|
||||
instance: note.instance,
|
||||
rewardNullifier: note.rewardNullifier,
|
||||
extDataHash,
|
||||
|
||||
noteSecret: note.secret,
|
||||
noteNullifier: note.nullifier,
|
||||
|
||||
inputAmount: account.amount,
|
||||
inputSecret: account.secret,
|
||||
inputNullifier: account.nullifier,
|
||||
inputRoot: accountTreeUpdate.oldRoot,
|
||||
inputPathElements: accountPath.pathElements,
|
||||
inputPathIndices: bitsToNumber(accountPath.pathIndices),
|
||||
inputNullifierHash: account.nullifierHash,
|
||||
|
||||
outputAmount: newAccount.amount,
|
||||
outputSecret: newAccount.secret,
|
||||
outputNullifier: newAccount.nullifier,
|
||||
outputRoot: accountTreeUpdate.newRoot,
|
||||
outputPathIndices: accountTreeUpdate.pathIndices,
|
||||
outputPathElements: accountTreeUpdate.pathElements,
|
||||
outputCommitment: newAccount.commitment,
|
||||
|
||||
depositBlock: note.depositBlock,
|
||||
depositRoot: depositTree.root(),
|
||||
depositPathIndices: bitsToNumber(depositPath.pathIndices),
|
||||
depositPathElements: depositPath.pathElements,
|
||||
|
||||
withdrawalBlock: note.withdrawalBlock,
|
||||
withdrawalRoot: withdrawalTree.root(),
|
||||
withdrawalPathIndices: bitsToNumber(withdrawalPath.pathIndices),
|
||||
withdrawalPathElements: withdrawalPath.pathElements,
|
||||
}
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(
|
||||
this.groth16,
|
||||
input,
|
||||
this.provingKeys.rewardCircuit,
|
||||
this.provingKeys.rewardProvingKey,
|
||||
)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = {
|
||||
rate: toFixedHex(input.rate),
|
||||
fee: toFixedHex(input.fee),
|
||||
instance: toFixedHex(input.instance, 20),
|
||||
rewardNullifier: toFixedHex(input.rewardNullifier),
|
||||
extDataHash: toFixedHex(input.extDataHash),
|
||||
depositRoot: toFixedHex(input.depositRoot),
|
||||
withdrawalRoot: toFixedHex(input.withdrawalRoot),
|
||||
extData: {
|
||||
relayer: toFixedHex(relayer, 20),
|
||||
encryptedAccount,
|
||||
},
|
||||
account: {
|
||||
inputRoot: toFixedHex(input.inputRoot),
|
||||
inputNullifierHash: toFixedHex(input.inputNullifierHash),
|
||||
outputRoot: toFixedHex(input.outputRoot),
|
||||
outputPathIndices: toFixedHex(input.outputPathIndices),
|
||||
outputCommitment: toFixedHex(input.outputCommitment),
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
proof,
|
||||
args,
|
||||
account: newAccount,
|
||||
}
|
||||
}
|
||||
|
||||
async withdraw({ account, amount, recipient, publicKey, fee = 0, relayer = 0 }) {
|
||||
const newAmount = account.amount.sub(toBN(amount)).sub(toBN(fee))
|
||||
const newAccount = new Account({ amount: newAmount })
|
||||
|
||||
const accountCommitments = await this._fetchAccountCommitments()
|
||||
const accountTree = new MerkleTree(this.merkleTreeHeight, accountCommitments, {
|
||||
hashFunction: poseidonHash2,
|
||||
})
|
||||
const accountIndex = accountTree.indexOf(account.commitment, (a, b) => a.eq(b))
|
||||
if (accountIndex === -1) {
|
||||
throw new Error('The accounts tree does not contain such account commitment')
|
||||
}
|
||||
const accountPath = accountTree.path(accountIndex)
|
||||
const accountTreeUpdate = this._updateTree(accountTree, newAccount.commitment)
|
||||
|
||||
const encryptedAccount = packEncryptedMessage(newAccount.encrypt(publicKey))
|
||||
const extDataHash = getExtWithdrawArgsHash({ fee, recipient, relayer, encryptedAccount })
|
||||
|
||||
const input = {
|
||||
amount: toBN(amount).add(toBN(fee)),
|
||||
extDataHash,
|
||||
|
||||
inputAmount: account.amount,
|
||||
inputSecret: account.secret,
|
||||
inputNullifier: account.nullifier,
|
||||
inputNullifierHash: account.nullifierHash,
|
||||
inputRoot: accountTreeUpdate.oldRoot,
|
||||
inputPathIndices: bitsToNumber(accountPath.pathIndices),
|
||||
inputPathElements: accountPath.pathElements,
|
||||
|
||||
outputAmount: newAccount.amount,
|
||||
outputSecret: newAccount.secret,
|
||||
outputNullifier: newAccount.nullifier,
|
||||
outputRoot: accountTreeUpdate.newRoot,
|
||||
outputPathIndices: accountTreeUpdate.pathIndices,
|
||||
outputPathElements: accountTreeUpdate.pathElements,
|
||||
outputCommitment: newAccount.commitment,
|
||||
}
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(
|
||||
this.groth16,
|
||||
input,
|
||||
this.provingKeys.withdrawCircuit,
|
||||
this.provingKeys.withdrawProvingKey,
|
||||
)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = {
|
||||
amount: toFixedHex(input.amount),
|
||||
extDataHash: toFixedHex(input.extDataHash),
|
||||
extData: {
|
||||
fee: toFixedHex(fee),
|
||||
recipient: toFixedHex(recipient, 20),
|
||||
relayer: toFixedHex(relayer, 20),
|
||||
encryptedAccount,
|
||||
},
|
||||
account: {
|
||||
inputRoot: toFixedHex(input.inputRoot),
|
||||
inputNullifierHash: toFixedHex(input.inputNullifierHash),
|
||||
outputRoot: toFixedHex(input.outputRoot),
|
||||
outputPathIndices: toFixedHex(input.outputPathIndices),
|
||||
outputCommitment: toFixedHex(input.outputCommitment),
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
proof,
|
||||
args,
|
||||
account: newAccount,
|
||||
}
|
||||
}
|
||||
|
||||
async treeUpdate(commitment, accountTree = null) {
|
||||
if (!accountTree) {
|
||||
const accountCommitments = await this._fetchAccountCommitments()
|
||||
accountTree = new MerkleTree(this.merkleTreeHeight, accountCommitments, {
|
||||
hashFunction: poseidonHash2,
|
||||
})
|
||||
}
|
||||
const accountTreeUpdate = this._updateTree(accountTree, commitment)
|
||||
|
||||
const input = {
|
||||
oldRoot: accountTreeUpdate.oldRoot,
|
||||
newRoot: accountTreeUpdate.newRoot,
|
||||
leaf: commitment,
|
||||
pathIndices: accountTreeUpdate.pathIndices,
|
||||
pathElements: accountTreeUpdate.pathElements,
|
||||
}
|
||||
|
||||
const proofData = await websnarkUtils.genWitnessAndProve(
|
||||
this.groth16,
|
||||
input,
|
||||
this.provingKeys.treeUpdateCircuit,
|
||||
this.provingKeys.treeUpdateProvingKey,
|
||||
)
|
||||
const { proof } = websnarkUtils.toSolidityInput(proofData)
|
||||
|
||||
const args = {
|
||||
oldRoot: toFixedHex(input.oldRoot),
|
||||
newRoot: toFixedHex(input.newRoot),
|
||||
leaf: toFixedHex(input.leaf),
|
||||
pathIndices: toFixedHex(input.pathIndices),
|
||||
}
|
||||
|
||||
return {
|
||||
proof,
|
||||
args,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Controller
|
46
src/note.js
Normal file
46
src/note.js
Normal file
@ -0,0 +1,46 @@
|
||||
const { toBN, BN } = require('web3-utils')
|
||||
const { randomBN, pedersenHashBuffer, poseidonHash } = require('./utils')
|
||||
|
||||
class Note {
|
||||
constructor({ secret, nullifier, netId, amount, currency, depositBlock, withdrawalBlock, instance } = {}) {
|
||||
this.secret = secret ? toBN(secret) : randomBN(31)
|
||||
this.nullifier = nullifier ? toBN(nullifier) : randomBN(31)
|
||||
|
||||
this.commitment = pedersenHashBuffer(
|
||||
Buffer.concat([this.nullifier.toBuffer('le', 31), this.secret.toBuffer('le', 31)]),
|
||||
)
|
||||
this.nullifierHash = pedersenHashBuffer(this.nullifier.toBuffer('le', 31))
|
||||
this.rewardNullifier = poseidonHash([this.nullifier])
|
||||
|
||||
this.netId = netId
|
||||
this.amount = amount
|
||||
this.currency = currency
|
||||
this.depositBlock = toBN(depositBlock)
|
||||
this.withdrawalBlock = toBN(withdrawalBlock)
|
||||
this.instance = instance || Note.getInstance(currency, amount)
|
||||
}
|
||||
|
||||
static getInstance(/* currency, amount */) {
|
||||
// todo
|
||||
}
|
||||
|
||||
static fromString(note, instance, depositBlock, withdrawalBlock) {
|
||||
note = note.split('-')
|
||||
const [, currency, amount, netId] = note
|
||||
const hexNote = note[4].slice(2)
|
||||
const nullifier = new BN(hexNote.slice(0, 62), 16, 'le')
|
||||
const secret = new BN(hexNote.slice(62), 16, 'le')
|
||||
return new Note({
|
||||
secret,
|
||||
nullifier,
|
||||
netId,
|
||||
amount,
|
||||
currency,
|
||||
depositBlock,
|
||||
withdrawalBlock,
|
||||
instance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Note
|
165
src/utils.js
Normal file
165
src/utils.js
Normal file
@ -0,0 +1,165 @@
|
||||
const crypto = require('crypto')
|
||||
const Decimal = require('decimal.js')
|
||||
const { bigInt } = require('snarkjs')
|
||||
const { toBN, soliditySha3 } = require('web3-utils')
|
||||
const Web3 = require('web3')
|
||||
const web3 = new Web3()
|
||||
const { babyJub, pedersenHash, mimcsponge, poseidon } = require('circomlib')
|
||||
|
||||
const RewardExtData = {
|
||||
RewardExtData: {
|
||||
relayer: 'address',
|
||||
encryptedAccount: 'bytes',
|
||||
},
|
||||
}
|
||||
const AccountUpdate = {
|
||||
AccountUpdate: {
|
||||
inputRoot: 'bytes32',
|
||||
inputNullifierHash: 'bytes32',
|
||||
outputRoot: 'bytes32',
|
||||
outputPathIndices: 'uint256',
|
||||
outputCommitment: 'bytes32',
|
||||
},
|
||||
}
|
||||
const RewardArgs = {
|
||||
RewardArgs: {
|
||||
rate: 'uint256',
|
||||
fee: 'uint256',
|
||||
instance: 'address',
|
||||
rewardNullifier: 'bytes32',
|
||||
extDataHash: 'bytes32',
|
||||
depositRoot: 'bytes32',
|
||||
withdrawalRoot: 'bytes32',
|
||||
extData: RewardExtData.RewardExtData,
|
||||
account: AccountUpdate.AccountUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
const WithdrawExtData = {
|
||||
WithdrawExtData: {
|
||||
fee: 'uint256',
|
||||
recipient: 'address',
|
||||
relayer: 'address',
|
||||
encryptedAccount: 'bytes',
|
||||
},
|
||||
}
|
||||
|
||||
const pedersenHashBuffer = (buffer) => toBN(babyJub.unpackPoint(pedersenHash.hash(buffer))[0].toString())
|
||||
|
||||
const mimcHash = (items) => toBN(mimcsponge.multiHash(items.map((item) => bigInt(item))).toString())
|
||||
|
||||
const poseidonHash = (items) => toBN(poseidon(items).toString())
|
||||
|
||||
const poseidonHash2 = (a, b) => poseidonHash([a, b])
|
||||
|
||||
/** Generate random number of specified byte length */
|
||||
const randomBN = (nbytes = 31) => toBN(bigInt.leBuff2int(crypto.randomBytes(nbytes)).toString())
|
||||
|
||||
/** BigNumber to hex string of specified length */
|
||||
const toFixedHex = (number, length = 32) =>
|
||||
'0x' +
|
||||
(number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)).padStart(length * 2, '0')
|
||||
|
||||
function getExtRewardArgsHash({ relayer, encryptedAccount }) {
|
||||
const encodedData = web3.eth.abi.encodeParameters(
|
||||
[RewardExtData],
|
||||
[{ relayer: toFixedHex(relayer, 20), encryptedAccount }],
|
||||
)
|
||||
const hash = soliditySha3({ t: 'bytes', v: encodedData })
|
||||
return '0x00' + hash.slice(4) // cut last byte to make it 31 byte long to fit the snark field
|
||||
}
|
||||
|
||||
function getExtWithdrawArgsHash({ fee, recipient, relayer, encryptedAccount }) {
|
||||
const encodedData = web3.eth.abi.encodeParameters(
|
||||
[WithdrawExtData],
|
||||
[
|
||||
{
|
||||
fee: toFixedHex(fee, 32),
|
||||
recipient: toFixedHex(recipient, 20),
|
||||
relayer: toFixedHex(relayer, 20),
|
||||
encryptedAccount,
|
||||
},
|
||||
],
|
||||
)
|
||||
const hash = soliditySha3({ t: 'bytes', v: encodedData })
|
||||
return '0x00' + hash.slice(4) // cut first byte to make it 31 byte long to fit the snark field
|
||||
}
|
||||
|
||||
function packEncryptedMessage(encryptedMessage) {
|
||||
const nonceBuf = Buffer.from(encryptedMessage.nonce, 'base64')
|
||||
const ephemPublicKeyBuf = Buffer.from(encryptedMessage.ephemPublicKey, 'base64')
|
||||
const ciphertextBuf = Buffer.from(encryptedMessage.ciphertext, 'base64')
|
||||
const messageBuff = Buffer.concat([
|
||||
Buffer.alloc(24 - nonceBuf.length),
|
||||
nonceBuf,
|
||||
Buffer.alloc(32 - ephemPublicKeyBuf.length),
|
||||
ephemPublicKeyBuf,
|
||||
ciphertextBuf,
|
||||
])
|
||||
return '0x' + messageBuff.toString('hex')
|
||||
}
|
||||
|
||||
function unpackEncryptedMessage(encryptedMessage) {
|
||||
if (encryptedMessage.slice(0, 2) === '0x') {
|
||||
encryptedMessage = encryptedMessage.slice(2)
|
||||
}
|
||||
const messageBuff = Buffer.from(encryptedMessage, 'hex')
|
||||
const nonceBuf = messageBuff.slice(0, 24)
|
||||
const ephemPublicKeyBuf = messageBuff.slice(24, 56)
|
||||
const ciphertextBuf = messageBuff.slice(56)
|
||||
return {
|
||||
version: 'x25519-xsalsa20-poly1305',
|
||||
nonce: nonceBuf.toString('base64'),
|
||||
ephemPublicKey: ephemPublicKeyBuf.toString('base64'),
|
||||
ciphertext: ciphertextBuf.toString('base64'),
|
||||
}
|
||||
}
|
||||
|
||||
function bitsToNumber(bits) {
|
||||
let result = 0
|
||||
for (const item of bits.slice().reverse()) {
|
||||
result = (result << 1) + item
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// a = floor(10**18 * e^(-0.0000000001 * amount))
|
||||
// yield = BalBefore - (BalBefore * a)/10**18
|
||||
function tornadoFormula({ balance, amount, poolWeight = 1e10 }) {
|
||||
const decimals = new Decimal(10 ** 18)
|
||||
balance = new Decimal(balance.toString())
|
||||
amount = new Decimal(amount.toString())
|
||||
poolWeight = new Decimal(poolWeight.toString())
|
||||
|
||||
const power = amount.div(poolWeight).negated()
|
||||
const exponent = Decimal.exp(power).mul(decimals)
|
||||
const newBalance = balance.mul(exponent).div(decimals)
|
||||
return toBN(balance.sub(newBalance).toFixed(0))
|
||||
}
|
||||
|
||||
function reverseTornadoFormula({ balance, tokens, poolWeight = 1e10 }) {
|
||||
balance = new Decimal(balance.toString())
|
||||
tokens = new Decimal(tokens.toString())
|
||||
poolWeight = new Decimal(poolWeight.toString())
|
||||
|
||||
return toBN(poolWeight.times(Decimal.ln(balance.div(balance.sub(tokens)))).toFixed(0))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
randomBN,
|
||||
pedersenHashBuffer,
|
||||
bitsToNumber,
|
||||
getExtRewardArgsHash,
|
||||
getExtWithdrawArgsHash,
|
||||
packEncryptedMessage,
|
||||
unpackEncryptedMessage,
|
||||
toFixedHex,
|
||||
mimcHash,
|
||||
poseidonHash,
|
||||
poseidonHash2,
|
||||
tornadoFormula,
|
||||
reverseTornadoFormula,
|
||||
RewardArgs,
|
||||
RewardExtData,
|
||||
AccountUpdate,
|
||||
}
|
107
test/merkleTree.test.js
Normal file
107
test/merkleTree.test.js
Normal file
@ -0,0 +1,107 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const { toFixedHex, randomBN } = require('../src/utils')
|
||||
const MerkleTree = artifacts.require('MerkleTreeWithHistoryMock')
|
||||
const Hasher = artifacts.require('Hasher2')
|
||||
|
||||
const levels = 16
|
||||
|
||||
contract('MerkleTree', () => {
|
||||
let tree1
|
||||
let tree2
|
||||
let snapshotId
|
||||
let hasher
|
||||
|
||||
before(async () => {
|
||||
hasher = await Hasher.new()
|
||||
tree1 = await MerkleTree.new(levels, hasher.address)
|
||||
tree2 = await MerkleTree.new(levels, hasher.address)
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
describe('#tree', () => {
|
||||
it('should bulk insert', async () => {
|
||||
const elements = ['123', '456', '789'].map((e) => toFixedHex(e))
|
||||
|
||||
await tree1.bulkInsert(elements)
|
||||
for (const e of elements) {
|
||||
await tree2.insert(e)
|
||||
}
|
||||
|
||||
const root1 = await tree1.getLastRoot()
|
||||
const root2 = await tree2.getLastRoot()
|
||||
|
||||
root1.should.be.equal(root2)
|
||||
})
|
||||
|
||||
it('almost full tree', async () => {
|
||||
let tree = await MerkleTree.new(3, hasher.address)
|
||||
let elements = ['1', '2', '3', '4', '5', '6', '7'].map((e) => toFixedHex(e))
|
||||
await tree.bulkInsert(elements)
|
||||
|
||||
tree = await MerkleTree.new(3, hasher.address)
|
||||
elements = ['1', '2', '3', '4', '5', '6', '7', '8'].map((e) => toFixedHex(e))
|
||||
await tree.bulkInsert(elements)
|
||||
|
||||
tree = await MerkleTree.new(3, hasher.address)
|
||||
elements = ['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((e) => toFixedHex(e))
|
||||
// prettier-ignore
|
||||
await tree
|
||||
.bulkInsert(elements)
|
||||
.should.be.rejectedWith('Merkle doesn\'t have enough capacity to add specified leaves')
|
||||
})
|
||||
|
||||
// it('estimate gas hasher', async () => {
|
||||
// const gas = await tree1.test() // hasher.contract.methods.poseidon([1, 2]).estimateGas()
|
||||
// console.log('gas', gas.toString())
|
||||
// })
|
||||
|
||||
it('should bulk insert with initial state', async () => {
|
||||
const initElements = [123, 456, 789].map((e) => toFixedHex(e))
|
||||
const elements = [12, 34, 56, 78, 90].map((e) => toFixedHex(e))
|
||||
|
||||
for (const e of initElements) {
|
||||
await tree1.insert(e)
|
||||
await tree2.insert(e)
|
||||
}
|
||||
|
||||
await tree1.bulkInsert(elements)
|
||||
for (const e of elements) {
|
||||
await tree2.insert(e)
|
||||
}
|
||||
|
||||
const root1 = await tree1.getLastRoot()
|
||||
const root2 = await tree2.getLastRoot()
|
||||
|
||||
root1.should.be.equal(root2)
|
||||
})
|
||||
|
||||
it.skip('should pass the stress test', async () => {
|
||||
const rounds = 40
|
||||
const elementCount = 10
|
||||
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const length = 1 + Math.floor(Math.random() * elementCount)
|
||||
const elements = Array.from({ length }, () => randomBN()).map((e) => toFixedHex(e))
|
||||
|
||||
await tree1.bulkInsert(elements)
|
||||
for (const e of elements) {
|
||||
await tree2.insert(e)
|
||||
}
|
||||
|
||||
const root1 = await tree1.getLastRoot()
|
||||
const root2 = await tree2.getLastRoot()
|
||||
|
||||
root1.should.be.equal(root2)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
863
test/miner.test.js
Normal file
863
test/miner.test.js
Normal file
@ -0,0 +1,863 @@
|
||||
/* 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,
|
||||
poseidonHash2,
|
||||
packEncryptedMessage,
|
||||
unpackEncryptedMessage,
|
||||
getExtWithdrawArgsHash,
|
||||
} = require('../src/utils')
|
||||
const { getEncryptionPublicKey } = require('eth-sig-util')
|
||||
const Miner = artifacts.require('MinerMock')
|
||||
const TornadoTrees = artifacts.require('TornadoTreesMock')
|
||||
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('fixed-merkle-tree')
|
||||
const Hasher2 = artifacts.require('Hasher2')
|
||||
const Hasher3 = artifacts.require('Hasher3')
|
||||
|
||||
const { MERKLE_TREE_HEIGHT } = process.env
|
||||
|
||||
// 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.setBlockNumber(note.depositBlock)
|
||||
await tornadoTrees.registerDeposit(note.instance, toFixedHex(note.commitment))
|
||||
|
||||
await tornadoTrees.setBlockNumber(note.withdrawalBlock)
|
||||
await tornadoTrees.registerWithdrawal(note.instance, toFixedHex(note.nullifierHash))
|
||||
|
||||
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 = MERKLE_TREE_HEIGHT || 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 thirtyDays = 30 * 24 * 3600
|
||||
const poolWeight = 1e11
|
||||
const governance = accounts[9]
|
||||
|
||||
before(async () => {
|
||||
const rewardVerifier = await RewardVerifier.new()
|
||||
const withdrawVerifier = await WithdrawVerifier.new()
|
||||
const treeUpdateVerifier = await TreeUpdateVerifier.new()
|
||||
const hasher2 = await Hasher2.new()
|
||||
const hasher3 = await Hasher3.new()
|
||||
tornadoTrees = await TornadoTrees.new(operator, hasher2.address, hasher3.address, levels)
|
||||
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() }],
|
||||
)
|
||||
|
||||
const depositData = []
|
||||
const withdrawalData = []
|
||||
for (const note of notes) {
|
||||
const { depositLeaf, withdrawalLeaf } = await registerNote(note, tornadoTrees)
|
||||
depositData.push(depositLeaf)
|
||||
withdrawalData.push(withdrawalLeaf)
|
||||
}
|
||||
|
||||
await tornadoTrees.updateRoots(depositData, withdrawalData)
|
||||
|
||||
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'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
it('should work with outdated deposit or withdrawal merkle root', async () => {
|
||||
const note0 = new Note({
|
||||
instance: tornado,
|
||||
depositBlock: 10,
|
||||
withdrawalBlock: 55,
|
||||
})
|
||||
const note4 = new Note({
|
||||
instance: tornado,
|
||||
depositBlock: 10,
|
||||
withdrawalBlock: 55,
|
||||
})
|
||||
const note5 = new Note({
|
||||
instance: tornado,
|
||||
depositBlock: 10,
|
||||
withdrawalBlock: 65,
|
||||
})
|
||||
|
||||
const claim1 = await controller.reward({ account: new Account(), note: note3, publicKey })
|
||||
|
||||
const note4Leaves = await registerNote(note4, tornadoTrees)
|
||||
await tornadoTrees.updateRoots([note4Leaves.depositLeaf], [note4Leaves.withdrawalLeaf])
|
||||
|
||||
const claim2 = await controller.reward({ account: new Account(), note: note4, publicKey })
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const note0Leaves = await registerNote(note0, tornadoTrees)
|
||||
await tornadoTrees.updateRoots([note0Leaves.depositLeaf], [note0Leaves.withdrawalLeaf])
|
||||
}
|
||||
|
||||
await miner.reward(claim1.proof, claim1.args).should.be.rejectedWith('Incorrect deposit tree root')
|
||||
await miner.reward(claim2.proof, claim2.args).should.be.fulfilled
|
||||
|
||||
const note5Leaves = await registerNote(note5, tornadoTrees)
|
||||
await tornadoTrees.updateRoots([note5Leaves.depositLeaf], [note5Leaves.withdrawalLeaf])
|
||||
|
||||
const claim3 = await controller.reward({ account: new Account(), note: note5, publicKey })
|
||||
await miner.reward(claim3.proof, claim3.args).should.be.fulfilled
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
190
test/rewardSwap.test.js
Normal file
190
test/rewardSwap.test.js
Normal file
@ -0,0 +1,190 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
|
||||
const { toBN } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot, mineBlock } = require('../scripts/ganacheHelper')
|
||||
const { tornadoFormula, reverseTornadoFormula } = require('../src/utils')
|
||||
const Torn = artifacts.require('TORNMock')
|
||||
const RewardSwap = artifacts.require('RewardSwapMock')
|
||||
const tornConfig = require('torn-token')
|
||||
const RLP = require('rlp')
|
||||
|
||||
const MONTH = toBN(60 * 60 * 24 * 30)
|
||||
const DURATION = toBN(60 * 60 * 24 * 365)
|
||||
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
||||
contract('RewardSwap', (accounts) => {
|
||||
let torn
|
||||
let rewardSwap
|
||||
let amount
|
||||
const tornCap = toBN(tornConfig.torn.cap)
|
||||
const miningCap = toBN(tornConfig.torn.distribution.miningV2.amount)
|
||||
const initialTornBalance = toBN(tornConfig.miningV2.initialBalance)
|
||||
let yearLiquidity
|
||||
let delta = toBN(100000) // 0.0000000000001 torn error
|
||||
// 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]
|
||||
let snapshotId
|
||||
const thirtyDays = 30 * 24 * 3600
|
||||
const poolWeight = 1e11
|
||||
|
||||
before(async () => {
|
||||
const swapExpectedAddr = await getNextAddr(accounts[0], 1)
|
||||
torn = await Torn.new(sender, thirtyDays, [
|
||||
{ to: swapExpectedAddr, amount: miningCap.toString() },
|
||||
{ to: sender, amount: tornCap.sub(miningCap).toString() },
|
||||
])
|
||||
rewardSwap = await RewardSwap.new(
|
||||
torn.address,
|
||||
sender,
|
||||
miningCap.toString(),
|
||||
initialTornBalance.toString(),
|
||||
poolWeight,
|
||||
)
|
||||
yearLiquidity = miningCap.sub(initialTornBalance)
|
||||
amount = toBN(await rewardSwap.poolWeight()).mul(toBN(7)) // 10**10
|
||||
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await timeReset()
|
||||
})
|
||||
|
||||
describe('#formula test', () => {
|
||||
it('should work', async () => {
|
||||
let formulaReturn
|
||||
let expectedReturn
|
||||
amount = amount.mul(toBN(5))
|
||||
for (let i = 1; i < 11; i++) {
|
||||
amount = amount.div(toBN(i))
|
||||
formulaReturn = tornadoFormula({ balance: initialTornBalance, amount })
|
||||
expectedReturn = await rewardSwap.getExpectedReturn(amount)
|
||||
expectedReturn.sub(formulaReturn).should.be.lte.BN(delta)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should initialize', async () => {
|
||||
const tokenFromContract = await rewardSwap.torn()
|
||||
tokenFromContract.should.be.equal(torn.address)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return as expected', async () => {
|
||||
const balanceBefore = await torn.balanceOf(recipient)
|
||||
const expectedReturn = await rewardSwap.getExpectedReturn(amount)
|
||||
await rewardSwap.swap(recipient, amount, { from: sender })
|
||||
const balanceAfter = await torn.balanceOf(recipient)
|
||||
balanceAfter.sub(balanceBefore).should.be.eq.BN(expectedReturn)
|
||||
})
|
||||
|
||||
it('reverse rate', async () => {
|
||||
const tokens = await rewardSwap.getExpectedReturn(amount)
|
||||
const balance = await rewardSwap.tornVirtualBalance()
|
||||
const points = reverseTornadoFormula({ balance, tokens })
|
||||
points.sub(amount).should.be.lt.BN(toBN(1))
|
||||
})
|
||||
|
||||
it('should be approximately additive', async () => {
|
||||
const amount = toBN(10).pow(toBN(10)).mul(toBN(2))
|
||||
const delta = toBN('1000') // max floating point error
|
||||
|
||||
const balanceBefore1 = await torn.balanceOf(recipient)
|
||||
await rewardSwap.swap(recipient, amount, { from: sender })
|
||||
const balanceAfter1 = await torn.balanceOf(recipient)
|
||||
|
||||
await revertSnapshot(snapshotId.result)
|
||||
snapshotId = await takeSnapshot()
|
||||
|
||||
const balanceBefore2 = await torn.balanceOf(recipient)
|
||||
await rewardSwap.swap(recipient, amount.div(toBN(2)), { from: sender })
|
||||
await rewardSwap.swap(recipient, amount.div(toBN(2)), { from: sender })
|
||||
const balanceAfter2 = await torn.balanceOf(recipient)
|
||||
|
||||
balanceBefore1.sub(balanceBefore2).should.be.lt.BN(delta)
|
||||
balanceAfter1.sub(balanceAfter2).should.be.lt.BN(delta)
|
||||
})
|
||||
|
||||
describe('#swap', () => {
|
||||
it('should work as uniswap without vested tokens', async () => {
|
||||
const startTimestamp = await rewardSwap.startTimestamp()
|
||||
await rewardSwap.setTimestamp(startTimestamp)
|
||||
|
||||
const expectedTokens = await rewardSwap.getExpectedReturn(amount)
|
||||
const formulaReturn = tornadoFormula({ balance: initialTornBalance, amount })
|
||||
expectedTokens.sub(formulaReturn).should.be.lte.BN(delta)
|
||||
const tornVirtualBalance = await rewardSwap.tornVirtualBalance()
|
||||
tornVirtualBalance.should.be.eq.BN(initialTornBalance)
|
||||
|
||||
const balanceBefore = await torn.balanceOf(recipient)
|
||||
await rewardSwap.swap(recipient, amount, { from: sender })
|
||||
const balanceAfter = await torn.balanceOf(recipient)
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedTokens))
|
||||
})
|
||||
|
||||
it('should work with vested tokens (a half of year passed)', async () => {
|
||||
let startTimestamp = await rewardSwap.startTimestamp()
|
||||
const currentTimestamp = startTimestamp.add(DURATION.div(toBN(2)))
|
||||
await rewardSwap.setTimestamp(currentTimestamp)
|
||||
|
||||
const tornVirtualBalance = await rewardSwap.tornVirtualBalance()
|
||||
tornVirtualBalance.should.be.eq.BN(yearLiquidity.div(toBN(2)).add(initialTornBalance))
|
||||
|
||||
const formulaReturn = tornadoFormula({ balance: tornVirtualBalance, amount })
|
||||
const expectedTokens = await rewardSwap.getExpectedReturn(amount)
|
||||
|
||||
expectedTokens.sub(formulaReturn).should.be.lte.BN(delta)
|
||||
const balanceBefore = await torn.balanceOf(recipient)
|
||||
await rewardSwap.swap(recipient, amount, { from: sender })
|
||||
const balanceAfter = await torn.balanceOf(recipient)
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(expectedTokens))
|
||||
})
|
||||
|
||||
it('should not add any tokens after one year', async () => {
|
||||
let startTimestamp = await rewardSwap.startTimestamp()
|
||||
let currentTimestamp = startTimestamp.add(DURATION)
|
||||
await rewardSwap.setTimestamp(currentTimestamp)
|
||||
|
||||
let tornVirtualBalance = await rewardSwap.tornVirtualBalance()
|
||||
tornVirtualBalance.should.be.eq.BN(miningCap)
|
||||
|
||||
const formulaReturn = tornadoFormula({ balance: tornVirtualBalance, amount })
|
||||
const expectedTokens = await rewardSwap.getExpectedReturn(amount)
|
||||
expectedTokens.sub(formulaReturn).should.be.lte.BN(delta)
|
||||
|
||||
currentTimestamp = currentTimestamp.add(MONTH)
|
||||
await rewardSwap.setTimestamp(currentTimestamp)
|
||||
|
||||
tornVirtualBalance = await rewardSwap.tornVirtualBalance()
|
||||
tornVirtualBalance.should.be.eq.BN(miningCap)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
126
test/rewardSwap.tuning.test.js
Normal file
126
test/rewardSwap.tuning.test.js
Normal file
@ -0,0 +1,126 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
|
||||
const { toBN, fromWei } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot, increaseTime, mineBlock } = require('../scripts/ganacheHelper')
|
||||
const Torn = artifacts.require('TORNMock')
|
||||
const RewardSwap = artifacts.require('RewardSwapMock')
|
||||
const tornConfig = require('torn-token')
|
||||
const RLP = require('rlp')
|
||||
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
||||
// todo mock's fixed timestamp interferes with simulation
|
||||
contract.skip('RewardSwap simulation', (accounts) => {
|
||||
let torn
|
||||
let rewardSwap
|
||||
const sender = accounts[0]
|
||||
const recipient = accounts[1]
|
||||
const tornCap = toBN(tornConfig.torn.cap)
|
||||
const miningCap = toBN(tornConfig.torn.distribution.miningV2.amount)
|
||||
const initialTornBalance = toBN(tornConfig.miningV2.initialBalance)
|
||||
const poolWeight = 1e11
|
||||
let snapshotId
|
||||
|
||||
async function increaseTimeDays(days, verbose = true) {
|
||||
if (verbose) {
|
||||
console.log(`Skipping ${days} days`)
|
||||
}
|
||||
|
||||
const timestamp = await rewardSwap.getTimestamp()
|
||||
await rewardSwap.setTimestamp(Number(timestamp) + 60 * 60 * 24 * Number(days))
|
||||
}
|
||||
|
||||
async function exchange(points, verbose = true) {
|
||||
const balanceBefore = await torn.balanceOf(recipient)
|
||||
await rewardSwap.swap(recipient, points, { from: sender })
|
||||
const balanceAfter = await torn.balanceOf(recipient)
|
||||
const poolSize = await rewardSwap.tornVirtualBalance()
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Exchanged ${points} points for ${fromWei(
|
||||
balanceAfter.sub(balanceBefore),
|
||||
)} TORN. Remaining in pool ${fromWei(poolSize)} TORN`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
const swapExpectedAddr = await getNextAddr(accounts[0], 1)
|
||||
torn = await Torn.new(sender, 0, [
|
||||
{ to: swapExpectedAddr, amount: miningCap.toString() },
|
||||
{ to: sender, amount: tornCap.sub(miningCap).toString() },
|
||||
])
|
||||
rewardSwap = await RewardSwap.new(
|
||||
torn.address,
|
||||
sender,
|
||||
miningCap.toString(),
|
||||
initialTornBalance.toString(),
|
||||
poolWeight,
|
||||
)
|
||||
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await timeReset()
|
||||
})
|
||||
|
||||
describe('Swap Simulations', () => {
|
||||
it('init', async () => {
|
||||
console.log('Cap', fromWei(miningCap))
|
||||
console.log('Virtual balance', fromWei(await rewardSwap.tornVirtualBalance()))
|
||||
})
|
||||
|
||||
it('rates', async () => {
|
||||
let k = toBN(1)
|
||||
for (let i = 0; i < 18; i++) {
|
||||
console.log(
|
||||
`Expected return for 10^${i} points: ${fromWei(await rewardSwap.getExpectedReturn(k))} TORN`,
|
||||
)
|
||||
k = k.mul(toBN(10))
|
||||
}
|
||||
})
|
||||
|
||||
it.skip('sim1', async () => {
|
||||
await exchange(1e8)
|
||||
await exchange(1e8)
|
||||
await increaseTimeDays(1)
|
||||
await exchange(1e8)
|
||||
})
|
||||
|
||||
it('equilibrium sim', async () => {
|
||||
const amountPerDay = 7.2e9
|
||||
for (let i = 0; i <= 450; i++) {
|
||||
const verbose = i < 15 || i % 30 === 0 || (i > 360 && i < 370)
|
||||
if (verbose) {
|
||||
console.log(`Day ${i}`)
|
||||
}
|
||||
await exchange(amountPerDay, verbose)
|
||||
await increaseTimeDays(1, false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
142
test/tornadoTrees.test.js
Normal file
142
test/tornadoTrees.test.js
Normal file
@ -0,0 +1,142 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const Note = require('../src/note')
|
||||
const TornadoTrees = artifacts.require('TornadoTreesMock')
|
||||
const OwnableMerkleTree = artifacts.require('OwnableMerkleTree')
|
||||
const Hasher2 = artifacts.require('Hasher2')
|
||||
const Hasher3 = artifacts.require('Hasher3')
|
||||
const { toFixedHex, poseidonHash2, poseidonHash } = require('../src/utils')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
|
||||
async function registerDeposit(note, tornadoTrees) {
|
||||
await tornadoTrees.setBlockNumber(note.depositBlock)
|
||||
await tornadoTrees.registerDeposit(note.instance, toFixedHex(note.commitment))
|
||||
return {
|
||||
instance: note.instance,
|
||||
hash: toFixedHex(note.commitment),
|
||||
block: toFixedHex(note.depositBlock),
|
||||
}
|
||||
}
|
||||
|
||||
async function registerWithdrawal(note, tornadoTrees) {
|
||||
await tornadoTrees.setBlockNumber(note.withdrawalBlock)
|
||||
await tornadoTrees.registerWithdrawal(note.instance, toFixedHex(note.nullifierHash))
|
||||
return {
|
||||
instance: note.instance,
|
||||
hash: toFixedHex(note.nullifierHash),
|
||||
block: toFixedHex(note.withdrawalBlock),
|
||||
}
|
||||
}
|
||||
|
||||
const levels = 16
|
||||
contract('TornadoTrees', (accounts) => {
|
||||
let tornadoTrees
|
||||
let snapshotId
|
||||
let hasher2
|
||||
let hasher3
|
||||
let operator = accounts[0]
|
||||
let depositTree
|
||||
let withdrawalTree
|
||||
const instances = {
|
||||
one: '0x0000000000000000000000000000000000000001',
|
||||
two: '0x0000000000000000000000000000000000000002',
|
||||
three: '0x0000000000000000000000000000000000000003',
|
||||
four: '0x0000000000000000000000000000000000000004',
|
||||
}
|
||||
const note1 = new Note({
|
||||
instance: instances.one,
|
||||
depositBlock: 10,
|
||||
withdrawalBlock: 10 + 4 * 60 * 24,
|
||||
})
|
||||
const note2 = new Note({
|
||||
instance: instances.two,
|
||||
depositBlock: 10,
|
||||
withdrawalBlock: 10 + 2 * 4 * 60 * 24,
|
||||
})
|
||||
const note3 = new Note({
|
||||
instance: instances.three,
|
||||
depositBlock: 10,
|
||||
withdrawalBlock: 10 + 3 * 4 * 60 * 24,
|
||||
})
|
||||
|
||||
before(async () => {
|
||||
hasher2 = await Hasher2.new()
|
||||
hasher3 = await Hasher3.new()
|
||||
tornadoTrees = await TornadoTrees.new(operator, hasher2.address, hasher3.address, levels)
|
||||
depositTree = await OwnableMerkleTree.at(await tornadoTrees.depositTree())
|
||||
withdrawalTree = await OwnableMerkleTree.at(await tornadoTrees.withdrawalTree())
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should be initialized', async () => {
|
||||
const owner = await tornadoTrees.tornadoProxy()
|
||||
owner.should.be.equal(operator)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#updateRoots', () => {
|
||||
it('should work for many instances', async () => {
|
||||
const note1DepositLeaf = await registerDeposit(note1, tornadoTrees)
|
||||
const note2DepositLeaf = await registerDeposit(note2, tornadoTrees)
|
||||
|
||||
const note2WithdrawalLeaf = await registerWithdrawal(note2, tornadoTrees)
|
||||
|
||||
const note3DepositLeaf = await registerDeposit(note3, tornadoTrees)
|
||||
const note3WithdrawalLeaf = await registerWithdrawal(note3, tornadoTrees)
|
||||
|
||||
await tornadoTrees.updateRoots(
|
||||
[note1DepositLeaf, note2DepositLeaf, note3DepositLeaf],
|
||||
[note2WithdrawalLeaf, note3WithdrawalLeaf],
|
||||
)
|
||||
|
||||
const localDepositTree = new MerkleTree(levels, [], {
|
||||
hashFunction: poseidonHash2,
|
||||
})
|
||||
|
||||
localDepositTree.insert(poseidonHash([note1.instance, note1.commitment, note1.depositBlock]))
|
||||
localDepositTree.insert(poseidonHash([note2.instance, note2.commitment, note2.depositBlock]))
|
||||
localDepositTree.insert(poseidonHash([note3.instance, note3.commitment, note3.depositBlock]))
|
||||
|
||||
const lastDepositRoot = await depositTree.getLastRoot()
|
||||
toFixedHex(localDepositTree.root()).should.be.equal(lastDepositRoot.toString())
|
||||
|
||||
const localWithdrawalTree = new MerkleTree(levels, [], {
|
||||
hashFunction: poseidonHash2,
|
||||
})
|
||||
localWithdrawalTree.insert(poseidonHash([note2.instance, note2.nullifierHash, note2.withdrawalBlock]))
|
||||
localWithdrawalTree.insert(poseidonHash([note3.instance, note3.nullifierHash, note3.withdrawalBlock]))
|
||||
|
||||
const lastWithdrawalRoot = await withdrawalTree.getLastRoot()
|
||||
toFixedHex(localWithdrawalTree.root()).should.be.equal(lastWithdrawalRoot.toString())
|
||||
})
|
||||
it('should work for empty arrays', async () => {
|
||||
await tornadoTrees.updateRoots([], [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRegisteredDeposits', () => {
|
||||
it('should work', async () => {
|
||||
const note1DepositLeaf = await registerDeposit(note1, tornadoTrees)
|
||||
let res = await tornadoTrees.getRegisteredDeposits()
|
||||
res.length.should.be.equal(1)
|
||||
// res[0].should.be.true
|
||||
await tornadoTrees.updateRoots([note1DepositLeaf], [])
|
||||
|
||||
res = await tornadoTrees.getRegisteredDeposits()
|
||||
res.length.should.be.equal(0)
|
||||
|
||||
await registerDeposit(note2, tornadoTrees)
|
||||
res = await tornadoTrees.getRegisteredDeposits()
|
||||
// res[0].should.be.true
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
73
truffle.js
Normal file
73
truffle.js
Normal file
@ -0,0 +1,73 @@
|
||||
require('dotenv').config()
|
||||
const HDWalletProvider = require('truffle-hdwallet-provider')
|
||||
const utils = require('web3-utils')
|
||||
const { PRIVATE_KEY, INFURA_TOKEN } = process.env
|
||||
|
||||
module.exports = {
|
||||
// Uncommenting the defaults below
|
||||
// provides for an easier quick-start with Ganache.
|
||||
// You can also follow this format for other networks;
|
||||
// see <http://truffleframework.com/docs/advanced/configuration>
|
||||
// for more details on how to specify configuration options!
|
||||
//
|
||||
networks: {
|
||||
// development: {
|
||||
// host: '127.0.0.1',
|
||||
// port: 8545,
|
||||
// network_id: '*',
|
||||
// },
|
||||
// test: {
|
||||
// host: "127.0.0.1",
|
||||
// port: 7545,
|
||||
// network_id: "*"
|
||||
// }
|
||||
mainnet: {
|
||||
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://mainnet.infura.io/v3/${INFURA_TOKEN}`),
|
||||
network_id: 1,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('100', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
kovan: {
|
||||
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://kovan.infura.io/v3/${INFURA_TOKEN}`),
|
||||
network_id: 42,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('1', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
coverage: {
|
||||
host: 'localhost',
|
||||
network_id: '*',
|
||||
port: 8554, // <-- If you change this, also set the port option in .solcover.js.
|
||||
gas: 0xfffffffffff, // <-- Use this high gas value
|
||||
gasPrice: 0x01, // <-- Use this low gas price
|
||||
},
|
||||
},
|
||||
compilers: {
|
||||
solc: {
|
||||
version: '0.6.12',
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
external: {
|
||||
command: 'node ./compileHasher.js',
|
||||
targets: [
|
||||
{
|
||||
path: './build/contracts/Hasher2.json',
|
||||
},
|
||||
{
|
||||
path: './build/contracts/Hasher3.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: ['truffle-plugin-verify', 'solidity-coverage'],
|
||||
}
|
Loading…
Reference in New Issue
Block a user