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