diff --git a/.gitignore b/.gitignore
index 135f8a8..81c6bfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,3 +96,6 @@ typings/
ERC20Mixer_flat.sol
ETHMixer_flat.sol
+
+.openzeppelin/.session
+.openzeppelin/dev-*.json
diff --git a/.openzeppelin/project.json b/.openzeppelin/project.json
new file mode 100644
index 0000000..849f7bc
--- /dev/null
+++ b/.openzeppelin/project.json
@@ -0,0 +1,15 @@
+{
+ "manifestVersion": "2.2",
+ "contracts": {
+ "ETHMixer": "ETHMixer"
+ },
+ "dependencies": {},
+ "name": "tornado",
+ "version": "1.0.0",
+ "compiler": {
+ "manager": "truffle",
+ "compilerSettings": {
+ "optimizer": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/circuits/withdraw.circom b/circuits/withdraw.circom
index 27612d8..611a074 100644
--- a/circuits/withdraw.circom
+++ b/circuits/withdraw.circom
@@ -31,8 +31,7 @@ template Withdraw(levels, rounds) {
signal input root;
signal input nullifierHash;
signal input receiver; // not taking part in any computations
- signal input relayer; // not taking part in any computations
- signal input fee; // not taking part in any computations
+
signal private input nullifier;
signal private input secret;
signal private input pathElements[levels];
@@ -56,11 +55,7 @@ template Withdraw(levels, rounds) {
// 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 receiverSquare;
- signal feeSquare;
- signal relayerSquare;
receiverSquare <== receiver * receiver;
- feeSquare <== fee * fee;
- relayerSquare <== relayer * relayer;
}
component main = Withdraw(16, 220);
diff --git a/cli.js b/cli.js
index dd4ee1f..a6e4bbc 100755
--- a/cli.js
+++ b/cli.js
@@ -11,6 +11,8 @@ const merkleTree = require('./lib/MerkleTree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
+const { GSNProvider, GSNDevProvider } = require('@openzeppelin/gsn-provider')
+const { ephemeral } = require('@openzeppelin/network')
let web3, mixer, erc20mixer, circuit, proving_key, groth16, erc20
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN
@@ -194,11 +196,83 @@ async function withdraw(note, receiver) {
console.log('Done')
}
+async function withdrawViaRelayer(note, receiver) {
+ // Decode hex string and restore the deposit object
+ let buf = Buffer.from(note.slice(2), 'hex')
+ let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
+ const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
+ const paddedNullifierHash = nullifierHash.toString(16).padStart('66', '0x000000')
+ const paddedCommitment = deposit.commitment.toString(16).padStart('66', '0x000000')
+
+ // Get all deposit events from smart contract and assemble merkle tree from them
+ console.log('Getting current state from mixer contract')
+ const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
+ const leaves = events
+ .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order
+ .map(e => e.returnValues.commitment)
+ const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
+
+ // Find current commitment in the tree
+ let depositEvent = events.find(e => e.returnValues.commitment.eq(paddedCommitment))
+ let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex.toNumber() : -1
+
+ // Validate that our data is correct
+ const isValidRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
+ const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call()
+ assert(isValidRoot === true, 'Merkle tree assembled incorrectly') // Merkle tree assembled correctly
+ assert(isSpent === false, 'The note is spent') // The note is not spent
+ assert(leafIndex >= 0, 'Our deposit is not present in the tree') // Our deposit is present in the tree
+
+ // Compute merkle proof of our commitment
+ const { root, path_elements, path_index } = await tree.path(leafIndex)
+
+ // Prepare circuit input
+ const input = {
+ // Public snark inputs
+ root: root,
+ nullifierHash,
+ receiver: bigInt(receiver),
+
+ // Private snark inputs
+ nullifier: deposit.nullifier,
+ secret: deposit.secret,
+ pathElements: path_elements,
+ pathIndex: path_index,
+ }
+
+ console.log('Generating SNARK proof')
+ console.time('Proof time')
+ const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
+ const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
+ console.timeEnd('Proof time')
+
+ console.log('Submitting withdraw transaction via relayer')
+
+ const account = ephemeral()
+ const HARDCODED_RELAYER_OPTS = {
+ txFee: 90,
+ fixedGasPrice: 22000000001,
+ gasPrice: 22000000001,
+ fixedGasLimit: 5000000,
+ gasLimit: 5000000,
+ verbose: true,
+ }
+ // const provider = new GSNProvider('https://rinkeby.infura.io/v3/c7463beadf2144e68646ff049917b716', { signKey: account })
+ const provider = new GSNDevProvider('http://localhost:8545', { signKey: account, ...HARDCODED_RELAYER_OPTS })
+ web3 = new Web3(provider)
+ const netId = await web3.eth.net.getId()
+ // eslint-disable-next-line require-atomic-updates
+ mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
+ console.log('mixer address', contractJson.networks[netId].address)
+ const tx = await mixer.methods.withdrawViaRelayer(pi_a, pi_b, pi_c, publicSignals).send({ from: account.address, gas: '5000000' })
+ console.log('tx', tx)
+ console.log('Done')
+}
/**
* Init web3, contracts, and snark
*/
+let contractJson, erc20ContractJson, erc20mixerJson
async function init() {
- let contractJson, erc20ContractJson, erc20mixerJson
if (inBrowser) {
// Initialize using injected web3 (Metamask)
// To assemble web version run `npm run browserify`
@@ -207,7 +281,7 @@ async function init() {
circuit = await (await fetch('build/circuits/withdraw.json')).json()
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
MERKLE_TREE_HEIGHT = 16
- ETH_AMOUNT = 1e18
+ ETH_AMOUNT = '30000000000000000'
EMPTY_ELEMENT = 1
} else {
// Initialize from local node
@@ -231,9 +305,11 @@ async function init() {
mixer.deployedBlock = tx.blockNumber
}
- const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash)
- erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address)
- erc20mixer.deployedBlock = tx3.blockNumber
+ if (erc20mixerJson) {
+ const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash)
+ erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address)
+ erc20mixer.deployedBlock = tx3.blockNumber
+ }
if(ERC20_TOKEN === '') {
erc20 = new web3.eth.Contract(erc20ContractJson.abi, erc20ContractJson.networks[netId].address)
@@ -273,6 +349,11 @@ if (inBrowser) {
const receiver = (await web3.eth.getAccounts())[0]
await withdraw(note, receiver)
}
+ window.withdrawViaRelayer = async () => {
+ const note = prompt('Enter the note to withdrawViaRelayer')
+ const receiver = (await web3.eth.getAccounts())[0]
+ await withdrawViaRelayer(note, receiver)
+ }
init()
} else {
const args = process.argv.slice(2)
@@ -320,6 +401,13 @@ if (inBrowser) {
else
printHelp(1)
break
+ case 'withdrawViaRelayer':
+ if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
+ init().then(() => withdrawViaRelayer(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
+ }
+ else
+ printHelp(1)
+ break
case 'auto':
if (args.length === 1) {
(async () => {
diff --git a/contracts/ETHMixer.sol b/contracts/ETHMixer.sol
index 107fe6c..fdb0aca 100644
--- a/contracts/ETHMixer.sol
+++ b/contracts/ETHMixer.sol
@@ -12,8 +12,10 @@
pragma solidity ^0.5.8;
import "./Mixer.sol";
+import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";
+import "@openzeppelin/contracts-ethereum-package/contracts/GSN/IRelayHub.sol";
-contract ETHMixer is Mixer {
+contract ETHMixer is Mixer, GSNRecipient {
constructor(
address _verifier,
uint256 _mixDenomination,
@@ -23,14 +25,77 @@ contract ETHMixer is Mixer {
) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public {
}
- function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal {
- _receiver.transfer(mixDenomination - _fee);
- if (_fee > 0) {
- _relayer.transfer(_fee);
- }
+ function _processWithdraw(address payable _receiver) internal {
+ _receiver.transfer(mixDenomination);
}
function _processDeposit() internal {
require(msg.value == mixDenomination, "Please send `mixDenomination` ETH along with transaction");
}
+
+ function withdrawViaRelayer(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public {
+ uint256 root = input[0];
+ uint256 nullifierHash = input[1];
+ require(!nullifierHashes[nullifierHash], "The note has been already spent");
+
+ require(isKnownRoot(root), "Cannot find your merkle root"); // Make sure to use a recent one
+ require(verifier.verifyProof(a, b, c, input), "Invalid withdraw proof");
+ nullifierHashes[nullifierHash] = true;
+ // we will process withdraw in postRelayedCall func
+ }
+
+ // gsn related stuff
+ // this func is called by a Relayer via the RelayerHub before sending a tx
+ function acceptRelayedCall(
+ address relay,
+ address from,
+ bytes calldata encodedFunction,
+ uint256 transactionFee,
+ uint256 gasPrice,
+ uint256 gasLimit,
+ uint256 nonce,
+ bytes calldata approvalData,
+ uint256 maxPossibleCharge
+ ) external view returns (uint256, bytes memory) {
+ // think of a withdraw dry-run
+ if (_computeCharge(gasLimit, gasPrice, transactionFee) * 2 > mixDenomination) {
+ return (1, "Fee exceeds 50% of transfer value");
+ }
+
+ if (!compareBytesWithSelector(encodedFunction, this.withdrawViaRelayer.selector)) {
+ return (2, "Only withdrawViaRelayer can be called");
+ }
+
+ return _approveRelayedCall();
+ }
+
+ // this func is called by RelayerHub right before calling a target func
+ function preRelayedCall(bytes calldata /*context*/) external returns (bytes32) {}
+
+ event Debug(uint actualCharge, bytes context, address recipient);
+ // this func is called by RelayerHub right after calling a target func
+ function postRelayedCall(bytes memory context, bool /*success*/, uint actualCharge, bytes32 /*preRetVal*/) public {
+ IRelayHub relayHub = IRelayHub(getHubAddr());
+ address payable recipient;
+ assembly {
+ recipient := sload(add(context, 324)) // 4 + (8 * 32) + (32) + (32) == selector + proof + root + nullifier
+ }
+ emit Debug(actualCharge, context, recipient);
+
+ recipient.transfer(mixDenomination - actualCharge);
+ relayHub.depositFor.value(actualCharge)(address(this));
+ // or we can send actualCharge somewhere else...
+ }
+
+ function compareBytesWithSelector(bytes memory data, bytes4 sel) internal pure returns (bool) {
+ return data[0] == sel[0]
+ && data[1] == sel[1]
+ && data[2] == sel[2]
+ && data[3] == sel[3];
+ }
+
+ function withdrawFundsFromHub(uint256 amount, address payable dest) external {
+ require(msg.sender == operator, "unauthorized");
+ IRelayHub(getHubAddr()).withdraw(amount, dest);
+ }
}
diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol
index 262967c..8a27ac2 100644
--- a/contracts/Mixer.sol
+++ b/contracts/Mixer.sol
@@ -14,7 +14,7 @@ pragma solidity ^0.5.8;
import "./MerkleTreeWithHistory.sol";
contract IVerifier {
- function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[5] memory input) public returns(bool);
+ function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public returns(bool);
}
contract Mixer is MerkleTreeWithHistory {
@@ -29,7 +29,7 @@ contract Mixer is MerkleTreeWithHistory {
uint256 public mixDenomination;
event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp);
- event Withdraw(address to, uint256 nullifierHash, address indexed relayer, uint256 fee);
+ event Withdraw(address to, uint256 nullifierHash, address indexed relayer);
/**
@dev The constructor
@@ -75,20 +75,17 @@ contract Mixer is MerkleTreeWithHistory {
- the receiver of funds
- optional fee that goes to the transaction sender (usually a relay)
*/
- function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[5] memory input) public {
+ function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public {
uint256 root = input[0];
uint256 nullifierHash = input[1];
address payable receiver = address(input[2]);
- address payable relayer = address(input[3]);
- uint256 fee = input[4];
- require(fee < mixDenomination, "Fee exceeds transfer value");
require(!nullifierHashes[nullifierHash], "The note has been already spent");
require(isKnownRoot(root), "Cannot find your merkle root"); // Make sure to use a recent one
require(verifier.verifyProof(a, b, c, input), "Invalid withdraw proof");
nullifierHashes[nullifierHash] = true;
- _processWithdraw(receiver, relayer, fee);
- emit Withdraw(receiver, nullifierHash, relayer, fee);
+ _processWithdraw(receiver);
+ emit Withdraw(receiver, nullifierHash, receiver);
}
function toggleDeposits() external {
@@ -106,6 +103,5 @@ contract Mixer is MerkleTreeWithHistory {
}
function _processDeposit() internal {}
- function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal {}
-
+ function _processWithdraw(address payable _receiver) internal {}
}
diff --git a/index.html b/index.html
index 359c20b..1d78b83 100644
--- a/index.html
+++ b/index.html
@@ -10,6 +10,7 @@
Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your contract to)
Deposit
Withdraw
+ withdrawViaRelayer