This commit is contained in:
poma 2020-12-15 18:08:37 +03:00
commit 080d0f8366
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
57 changed files with 10996 additions and 0 deletions

2
.codecov.yml Normal file
View File

@ -0,0 +1,2 @@
coverage:
range: '100...100'

9
.editorconfig Normal file
View 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
View File

@ -0,0 +1,5 @@
MERKLE_TREE_HEIGHT=20
PRIVATE_KEY=
INFURA_KEY=97c8bf358b9942a9853fab1ba93dc5b3
TORN=
GOVERNANCE=

27
.eslintrc Normal file
View 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
View File

@ -0,0 +1 @@
*.sol linguist-language=Solidity

85
.github/workflows/build.yml vendored Normal file
View 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
View File

@ -0,0 +1,11 @@
node_modules
types
coverage
coverage.json
.secret
.infura
.DS_Store
build
circuits/*.json
circuits/*.bin
.env

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
12

8
.prettierignore Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
module.exports = {
copyPackages: ['@openzeppelin/contracts'],
testrpcOptions: '-d --accounts 10 --port 8555',
skipFiles: ['Migrations.sol'],
}

13
.solhint.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "solhint:recommended",
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 110
}
],
"quotes": ["error", "double"]
},
"plugins": ["prettier"]
}

22
LICENSE Normal file
View 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
View 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
```

View 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
}

View 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
View 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);

View File

@ -0,0 +1,4 @@
include "./MerkleTreeUpdater.circom";
// zeroLeaf = keccak256("tornado") % FIELD_SIZE
component main = MerkleTreeUpdater(20, 21663839004416932945382355908790599225266501822907911457504978515578255421292);

25
circuits/Utils.circom Normal file
View 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
View 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
View 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
View 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
View 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;
}
}

View 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
View 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;
}
}

View 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);
}

View 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;
}

View 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;
}

View 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;
}

View 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);
}

View 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);
}
}

View 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)));
}
}

View 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;
}
}

View File

@ -0,0 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "torn-token/contracts/mocks/TORNMock.sol";

View 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;
}
}

View 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);
}
}

View 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);
}
}
}

View 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];
}
}

View 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);
}
}

View File

@ -0,0 +1 @@
../../build/circuits/RewardVerifier.sol

View File

@ -0,0 +1 @@
../../build/circuits/TreeUpdateVerifier.sol

View File

@ -0,0 +1 @@
../../build/circuits/WithdrawVerifier.sol

6
index.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
})
})

View 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
View 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
View 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'],
}

6486
yarn.lock Normal file

File diff suppressed because it is too large Load Diff