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/cli.js b/cli.js
index e2e9c2b..e77e5fa 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
@@ -29,6 +31,7 @@ function createDeposit(nullifier, secret) {
let deposit = { nullifier, secret }
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
deposit.commitment = pedersenHash(deposit.preimage)
+ deposit.nullifierHash = pedersenHash(nullifier.leInt2Buff(31))
return deposit
}
@@ -38,9 +41,10 @@ function createDeposit(nullifier, secret) {
*/
async function deposit() {
const deposit = createDeposit(rbigint(31), rbigint(31))
+ const fromAccount = (await web3.eth.getAccounts())[0]
console.log('Submitting deposit transaction')
- await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: (await web3.eth.getAccounts())[0], gas:1e6 })
+ await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: fromAccount, gas:1e6 })
const note = '0x' + deposit.preimage.toString('hex')
console.log('Your note:', note)
@@ -48,19 +52,21 @@ async function deposit() {
}
async function depositErc20() {
- const account = (await web3.eth.getAccounts())[0]
+ const fromAccount = (await web3.eth.getAccounts())[0]
const tokenAmount = process.env.TOKEN_AMOUNT
- await erc20.methods.mint(account, tokenAmount).send({ from: account, gas:1e6 })
- await erc20.methods.approve(erc20mixer.address, tokenAmount).send({ from: account, gas:1e6 })
- const allowance = await erc20.methods.allowance(account, erc20mixer.address).call()
+ await erc20.methods.mint(fromAccount, tokenAmount).send({ from: fromAccount, gas:1e6 })
+
+ await erc20.methods.approve(erc20mixer.address, tokenAmount).send({ from: fromAccount, gas:1e6 })
+ const allowance = await erc20.methods.allowance(fromAccount, erc20mixer.address).call()
console.log('erc20mixer allowance', allowance.toString(10))
const deposit = createDeposit(rbigint(31), rbigint(31))
- await erc20mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: account, gas:1e6 })
+ await erc20mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ from: fromAccount, gas:1e6 })
const balance = await erc20.methods.balanceOf(erc20mixer.address).call()
console.log('erc20mixer balance', balance.toString(10))
+
const note = '0x' + deposit.preimage.toString('hex')
console.log('Your note:', note)
return note
@@ -196,6 +202,92 @@ async function withdraw(note, receiver) {
console.log('Done')
}
+async function buildDepositTree() {
+ // 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)
+}
+
+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),
+ relayer: bigInt(0),
+ fee: bigInt(web3.utils.toWei('0.01')),
+ refund: bigInt(0),
+
+ // 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 proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
+ const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
+ 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()
+ console.log('netId', netId)
+ // 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(proof, publicSignals).send({ from: account.address, gas: 2e6 })
+ console.log('tx', tx)
+ console.log('Done')
+}
/**
* Init web3, contracts, and snark
*/
@@ -275,6 +367,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)
@@ -322,6 +419,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 'test':
if (args.length === 1) {
(async () => {
diff --git a/contracts/GSNProxy.sol b/contracts/GSNProxy.sol
new file mode 100644
index 0000000..26c62ec
--- /dev/null
+++ b/contracts/GSNProxy.sol
@@ -0,0 +1,157 @@
+pragma solidity ^0.5.8;
+// contract we {}
+
+import "./IUniswapExchange.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";
+import "@openzeppelin/contracts-ethereum-package/contracts/GSN/IRelayHub.sol";
+import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";
+
+contract IMixer {
+ function withdraw(uint256[8] calldata proof, uint256[6] calldata input) external payable;
+ function checkWithdrawalValidity(uint256[8] calldata proof, uint256[6] calldata input) external view;
+ function denomination() external view returns(uint256);
+ function token() external view returns(address); // only for ERC20 version
+}
+
+contract GSNProxy is GSNRecipient, Ownable {
+ IMixer public mixer;
+ IUniswapExchange public uniswap;
+ IERC20 public token;
+
+ constructor(address _mixer, address _uniswap) public {
+ mixer = IMixer(_mixer);
+ if (_uniswap != address(0)) {
+ uniswap = IUniswapExchange(_uniswap);
+ require(mixer.token() == uniswap.tokenAddress(), "mixer and uniswap have different tokens");
+ token = IERC20(uniswap.tokenAddress());
+ } else {
+ // todo: require that mixer is ETH version?
+ }
+ }
+
+ // Allow to refill mixer balance
+ function () external payable {}
+
+ modifier onlyHub() {
+ require(msg.sender == getHubAddr(), "only relay hub");
+ _;
+ }
+
+ /**
+ @dev Checks fee and calls mixer withdraw
+ */
+ function withdraw(uint256[8] calldata proof, uint256[6] calldata input) external {
+ mixer.withdraw.value(refund)(proof, input);
+ // todo: check that we received expected fee?
+ }
+
+ // gsn related stuff
+ // this func is called by a Relayer via the RelayerHub before sending a tx
+ function acceptRelayedCall(
+ address /*relay*/,
+ address /*from*/,
+ bytes memory encodedFunction,
+ uint256 /*transactionFee*/,
+ uint256 /*gasPrice*/,
+ uint256 /*gasLimit*/,
+ uint256 /*nonce*/,
+ bytes memory /*approvalData*/,
+ uint256 maxPossibleCharge
+ ) public view returns (uint256, bytes memory) {
+ // think of a withdraw dry-run
+ if (!compareBytesWithSelector(encodedFunction, this.withdraw.selector)) {
+ return (1, "Only withdrawViaRelayer can be called");
+ }
+
+ bytes memory proof;
+ bytes memory root;
+ uint256 fee;
+ uint256 refund;
+ assembly {
+ let dataPointer := add(encodedFunction, 32)
+ let nullifierPointer := mload(add(dataPointer, 4)) // 4 + (8 * 32) + (32) == selector + proof + root
+ let recipientPointer := mload(add(dataPointer, 324)) // 4 + (8 * 32) + (32) + (32) == selector + proof + root + nullifier
+ mstore(recipient, 64) // save array length
+ mstore(add(recipient, 32), recipientPointer) // save recipient address
+ mstore(add(recipient, 64), nullifierPointer) // save nullifier address
+ }
+ //mixer.checkWithdrawalValidity(proof, inputs)
+ // todo: duplicate withdraw checks?
+
+ if (token != IERC20(0)) {
+ // todo maybe static exchange rate?
+ if (uniswap.getTokenToEthInputPrice(fee) < maxPossibleCharge + refund) {
+ return (11, "Fee is too low");
+ }
+ } else {
+ // refund is expected to be 0, checked by mixer contract
+ if (fee < maxPossibleCharge + refund) {
+ return (11, "Fee is too low");
+ }
+ }
+
+ if (mixer.checkWithdrawalValidity()) {
+
+ }
+
+ return _approveRelayedCall();
+ }
+
+ // this func is called by RelayerHub right before calling a target func
+ function preRelayedCall(bytes calldata /*context*/) onlyHub external returns (bytes32) {}
+ function postRelayedCall(bytes memory /*context*/, bool /*success*/, uint actualCharge, bytes32 /*preRetVal*/) onlyHub public {
+ IRelayHub(getHubAddr()).depositFor.value(actualCharge)(address(this));
+ }
+
+ 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];
+ }
+
+ // Admin functions
+
+ function withdrawFundsFromHub(uint256 amount, address payable dest) onlyOwner external {
+ IRelayHub(getHubAddr()).withdraw(amount, dest);
+ }
+
+ function upgradeRelayHub(address newRelayHub) onlyOwner external {
+ _upgradeRelayHub(newRelayHub);
+ }
+
+ function withdrawEther(uint256 amount) onlyOwner external {
+ msg.sender.transfer(amount);
+ }
+
+ function withdrawTokens(uint256 amount) onlyOwner external {
+ safeErc20Transfer(msg.sender, amount);
+ }
+
+ function sellTokens(uint256 amount, uint256 min_eth) onlyOwner external {
+ token.approve(address(uniswap), amount);
+ uniswap.tokenToEthSwapInput(amount, min_eth, now);
+ }
+
+ function safeErc20Transfer(address to, uint256 amount) internal {
+ bool success;
+ bytes memory data;
+ bytes4 transferSelector = 0xa9059cbb;
+ (success, data) = address(token).call(
+ abi.encodeWithSelector(
+ transferSelector,
+ to, amount
+ )
+ );
+ require(success, "not enough tokens");
+
+ // if contract returns some data let's make sure that is `true` according to standard
+ if (data.length > 0) {
+ assembly {
+ success := mload(add(data, 0x20))
+ }
+ require(success, "not enough tokens. Token returns false.");
+ }
+ }
+}
diff --git a/contracts/IUniswapExchange.sol b/contracts/IUniswapExchange.sol
new file mode 100644
index 0000000..f47b913
--- /dev/null
+++ b/contracts/IUniswapExchange.sol
@@ -0,0 +1,70 @@
+pragma solidity ^0.5.0;
+
+contract IUniswapExchange {
+ // Address of ERC20 token sold on this exchange
+ function tokenAddress() external view returns (address token) {}
+ // Address of Uniswap Factory
+ function factoryAddress() external view returns (address factory) {}
+ // Provide Liquidity
+ function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256) {}
+
+ function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) external returns (uint256, uint256) {}
+ // Get Prices
+ function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought) {}
+
+ function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold) {}
+
+ function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought) {}
+
+ function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold) {}
+ // Trade ETH to ERC20
+ function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought) {}
+
+ function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address recipient) external payable returns (uint256 tokens_bought) {}
+
+ function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold) {}
+
+ function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address recipient) external payable returns (uint256 eth_sold) {}
+ // Trade ERC20 to ETH
+ function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought) {}
+
+ function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline, address recipient) external returns (uint256 eth_bought) {}
+
+ function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold) {}
+
+ function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold) {}
+ // Trade ERC20 to ERC20
+ function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external returns (uint256 tokens_bought) {}
+
+ function tokenToTokenTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_bought) {}
+
+ function tokenToTokenSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address token_addr) external returns (uint256 tokens_sold) {}
+
+ function tokenToTokenTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_sold) {}
+ // Trade ERC20 to Custom Pool
+ function tokenToExchangeSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address exchange_addr) external returns (uint256 tokens_bought) {}
+
+ function tokenToExchangeTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address exchange_addr) external returns (uint256 tokens_bought) {}
+
+ function tokenToExchangeSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address exchange_addr) external returns (uint256 tokens_sold) {}
+
+ function tokenToExchangeTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address exchange_addr) external returns (uint256 tokens_sold) {}
+ // ERC20 comaptibility for liquidity tokens
+ bytes32 public name;
+ bytes32 public symbol;
+ uint256 public decimals;
+
+ function transfer(address _to, uint256 _value) external returns (bool) {}
+
+ function transferFrom(address _from, address _to, uint256 value) external returns (bool) {}
+
+ function approve(address _spender, uint256 _value) external returns (bool) {}
+
+ function allowance(address _owner, address _spender) external view returns (uint256) {}
+
+ function balanceOf(address _owner) external view returns (uint256) {}
+
+ function totalSupply() external view returns (uint256) {}
+ // Never use
+ function setup(address token_addr) external {}
+}
diff --git a/contracts/Mixer.sol b/contracts/Mixer.sol
index 1845dcc..14c0919 100644
--- a/contracts/Mixer.sol
+++ b/contracts/Mixer.sol
@@ -90,16 +90,34 @@ contract Mixer is MerkleTreeWithHistory {
address payable relayer = address(input[3]);
uint256 fee = input[4];
uint256 refund = input[5];
+
require(fee < denomination, "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(proof, input), "Invalid withdraw proof");
+
nullifierHashes[nullifierHash] = true;
_processWithdraw(receiver, relayer, fee, refund);
emit Withdraw(receiver, nullifierHash, relayer, fee);
}
+ // todo: use this function in withdraw?
+ /**
+ @dev same checks as `withdraw` implemented as a view function. Used for relayers.
+ */
+ function checkWithdrawalValidity(uint256[8] calldata proof, uint256[6] calldata input) external view {
+ uint256 root = input[0];
+ uint256 nullifierHash = input[1];
+ //address payable receiver = address(input[2]);
+ //address payable relayer = address(input[3]);
+ uint256 fee = input[4];
+ uint256 refund = input[5];
+ require(fee < denomination, "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(proof, input), "Invalid withdraw proof");
+ }
+
/** @dev this function is defined in a child contract */
function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee, uint256 _refund) internal {}
diff --git a/contracts/Mocks/UniswapMock.sol b/contracts/Mocks/UniswapMock.sol
new file mode 100644
index 0000000..f2670aa
--- /dev/null
+++ b/contracts/Mocks/UniswapMock.sol
@@ -0,0 +1,67 @@
+pragma solidity ^0.5.0;
+
+import "./ERC20Mock.sol";
+import "../IUniswapExchange.sol";
+
+contract UniswapMock is IUniswapExchange {
+
+ ERC20Mock public tokenAddress;
+ uint256 public price;
+
+ // EthPurchase: event({buyer: indexed(address), tokens_sold: indexed(uint256), eth_bought: indexed(uint256(wei))})
+ event EthPurchase(address buyer, uint256 tokens_sold, uint256 eth_bought);
+
+ constructor(ERC20Mock _token, uint256 _price) public payable {
+ tokenAddress = _token;
+ price = _price; // in wei
+ }
+
+
+ /*
+ * @notice Convert Tokens to ETH.
+ * @dev User specifies maximum input and exact output.
+ * @param eth_bought Amount of ETH purchased.
+ * @param max_tokens Maximum Tokens sold.
+ * @param deadline Time after which this transaction can no longer be executed.
+ * @return Amount of Tokens sold.
+ * @public
+ * def tokenToEthSwapOutput(eth_bought: uint256(wei), max_tokens: uint256, deadline: timestamp) -> uint256:
+ */
+ function tokenToEthSwapOutput(uint256 eth_bought, uint256 /*max_tokens*/, uint256 /*deadline*/) public returns(uint256 tokens_sold) {
+ tokens_sold = getTokenToEthOutputPrice(eth_bought);
+ tokenAddress.transferFrom(msg.sender, address(this), tokens_sold);
+ msg.sender.transfer(eth_bought);
+ emit EthPurchase(msg.sender, tokens_sold, eth_bought);
+ return eth_bought;
+ }
+
+ function getTokenToEthOutputPrice(uint256 eth_bought) public view returns (uint256) {
+ return eth_bought * price / 10**18;
+ }
+
+ /*
+ * @notice Convert Tokens to ETH.
+ * @dev User specifies exact input and minimum output.
+ * @param tokens_sold Amount of Tokens sold.
+ * @param min_eth Minimum ETH purchased.
+ * @param deadline Time after which this transaction can no longer be executed.
+ * @return Amount of ETH bought.
+ * def tokenToEthSwapInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp) -> uint256(wei):
+ */
+ function tokenToEthSwapInput(uint256 tokens_sold, uint256 /* min_eth */, uint256 /* deadline */) public returns(uint256) {
+ tokenAddress.transferFrom(msg.sender, address(this), tokens_sold);
+ uint256 eth_bought = getTokenToEthInputPrice(tokens_sold);
+ msg.sender.transfer(eth_bought);
+ return eth_bought;
+ }
+
+ function getTokenToEthInputPrice(uint256 tokens_sold /* in wei */) public view returns (uint256 eth_bought) {
+ return tokens_sold * price / 10**18;
+ }
+
+ function setPrice(uint256 _price) external {
+ price = _price;
+ }
+
+ function() external payable {}
+}
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