Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EVM webb anchor contracts w/ CHAIN_ID #123

Merged
merged 1 commit into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ runtime/target
**/*.rs.bk
/*/**/target
.vscode
node_modules
116 changes: 116 additions & 0 deletions contracts/evm/webb-anchor/contracts/Anchor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
pragma solidity 0.5.17;

import "./MerkleTreeWithHistory.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract IVerifier {
function verifyProof(bytes memory _proof, uint256[6] memory _input) public returns(bool);
}

contract Anchor is MerkleTreeWithHistory, ReentrancyGuard {
uint256 public CHAIN_ID;
uint256 public denomination;
mapping(bytes32 => bool) public nullifierHashes;
// we store all commitments just to prevent accidental deposits with the same commitment
mapping(bytes32 => bool) public commitments;
IVerifier public verifier;

// operator can update snark verification key
// after the final trusted setup ceremony operator rights are supposed to be transferred to zero address
address public operator;
modifier onlyOperator {
require(msg.sender == operator, "Only operator can call this function.");
_;
}

event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);

/**
@dev The constructor
@param _verifier the address of SNARK verifier for this contract
@param _denomination transfer amount for each deposit
@param _merkleTreeHeight the height of deposits' Merkle Tree
@param _operator operator address (see operator comment above)
*/
constructor(
IVerifier _verifier,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator,
uint256 _chainId,
) MerkleTreeWithHistory(_merkleTreeHeight) public {
require(_denomination > 0, "denomination should be greater than 0");
verifier = _verifier;
operator = _operator;
denomination = _denomination;j
CHAIN_ID = _chainId;
}

/**
@dev Deposit funds into the contract. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this instance.
@param _commitment the note commitment, which is PedersenHash(nullifier + secret)
*/
function deposit(bytes32 _commitment) external payable nonReentrant {
require(!commitments[_commitment], "The commitment has been submitted");

uint32 insertedIndex = _insert(_commitment);
commitments[_commitment] = true;
_processDeposit();

emit Deposit(_commitment, insertedIndex, block.timestamp);
}

/** @dev this function is defined in a child contract */
function _processDeposit() internal;

/**
@dev Withdraw a deposit from the contract. `proof` is a zkSNARK proof data, and input is an array of circuit public inputs
`input` array consists of:
- merkle root of all deposits in the contract
- hash of unique deposit nullifier to prevent double spends
- the recipient of funds
- optional fee that goes to the transaction sender (usually a relay)
*/
function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant {
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, [CHAIN_ID, uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]), "Invalid withdraw proof");

nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
}

/** @dev this function is defined in a child contract */
function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal;

/** @dev whether a note is already spent */
function isSpent(bytes32 _nullifierHash) public view returns(bool) {
return nullifierHashes[_nullifierHash];
}

/** @dev whether an array of notes is already spent */
function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns(bool[] memory spent) {
spent = new bool[](_nullifierHashes.length);
for(uint i = 0; i < _nullifierHashes.length; i++) {
if (isSpent(_nullifierHashes[i])) {
spent[i] = true;
}
}
}

/**
@dev allow operator to update SNARK verification keys. This is needed to update keys after the final trusted setup ceremony is held.
After that operator rights are supposed to be transferred to zero address
*/
function updateVerifier(address _newVerifier) external onlyOperator {
verifier = IVerifier(_newVerifier);
}

/** @dev operator can change his address */
function changeOperator(address _newOperator) external onlyOperator {
operator = _newOperator;
}
}
64 changes: 64 additions & 0 deletions contracts/evm/webb-anchor/contracts/ERC20Anchor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
pragma solidity 0.5.17;

import "./Anchor.sol";

contract ERC20Anchor is Anchor {
address public token;

constructor(
IVerifier _verifier,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator,
address _token,
uint256 _chainId,
) Anchor(_verifier, _denomination, _merkleTreeHeight, _operator, _chainId) public {
token = _token;
}

function _processDeposit() internal {
require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance");
_safeErc20TransferFrom(msg.sender, address(this), denomination);
}

function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
require(msg.value == _refund, "Incorrect refund amount received by the contract");

_safeErc20Transfer(_recipient, denomination - _fee);
if (_fee > 0) {
_safeErc20Transfer(_relayer, _fee);
}

if (_refund > 0) {
(bool success, ) = _recipient.call.value(_refund)("");
if (!success) {
// let's return _refund back to the relayer
_relayer.transfer(_refund);
}
}
}

function _safeErc20TransferFrom(address _from, address _to, uint256 _amount) internal {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd /* transferFrom */, _from, _to, _amount));
require(success, "not enough allowed tokens");

// if contract returns some data lets make sure that is `true` according to standard
if (data.length > 0) {
require(data.length == 32, "data length should be either 0 or 32 bytes");
success = abi.decode(data, (bool));
require(success, "not enough allowed tokens. Token returns false.");
}
}

function _safeErc20Transfer(address _to, uint256 _amount) internal {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb /* transfer */, _to, _amount));
require(success, "not enough tokens");

// if contract returns some data lets make sure that is `true` according to standard
if (data.length > 0) {
require(data.length == 32, "data length should be either 0 or 32 bytes");
success = abi.decode(data, (bool));
require(success, "not enough tokens. Token returns false.");
}
}
}
109 changes: 109 additions & 0 deletions contracts/evm/webb-anchor/contracts/MerkleTreeWithHistory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
pragma solidity 0.5.17;

library Hasher {
function MiMCSponge(uint256 in_xL, uint256 in_xR) public pure returns (uint256 xL, uint256 xR);
}

contract MerkleTreeWithHistory {
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE

uint32 public levels;

// the following variables are made public for easier testing and debugging and
// are not supposed to be accessed in regular code
bytes32[] public filledSubtrees;
bytes32[] public zeros;
uint32 public currentRootIndex = 0;
uint32 public nextIndex = 0;
uint32 public constant ROOT_HISTORY_SIZE = 100;
bytes32[ROOT_HISTORY_SIZE] public roots;

constructor(uint32 _treeLevels) public {
require(_treeLevels > 0, "_treeLevels should be greater than zero");
require(_treeLevels < 32, "_treeLevels should be less than 32");
levels = _treeLevels;

bytes32 currentZero = bytes32(ZERO_VALUE);
zeros.push(currentZero);
filledSubtrees.push(currentZero);

for (uint32 i = 1; i < levels; i++) {
currentZero = hashLeftRight(currentZero, currentZero);
zeros.push(currentZero);
filledSubtrees.push(currentZero);
}

roots[0] = hashLeftRight(currentZero, currentZero);
}

/**
@dev Hash 2 tree leaves, returns MiMC(_left, _right)
*/
function hashLeftRight(bytes32 _left, bytes32 _right) public pure returns (bytes32) {
require(uint256(_left) < FIELD_SIZE, "_left should be inside the field");
require(uint256(_right) < FIELD_SIZE, "_right should be inside the field");
uint256 R = uint256(_left);
uint256 C = 0;
(R, C) = Hasher.MiMCSponge(R, C);
R = addmod(R, uint256(_right), FIELD_SIZE);
(R, C) = Hasher.MiMCSponge(R, C);
return bytes32(R);
}

function _insert(bytes32 _leaf) internal returns(uint32 index) {
uint32 currentIndex = nextIndex;
require(currentIndex != uint32(2)**levels, "Merkle tree is full. No more leafs can be added");
nextIndex += 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;
}

/**
@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];
}
}
31 changes: 31 additions & 0 deletions contracts/evm/webb-anchor/contracts/NativeAnchor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pragma solidity 0.5.17;

import "./Anchor.sol";

contract NativeAnchor is Anchor {
constructor(
IVerifier _verifier,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator,
uint256 _chainId,
) Anchor(_verifier, _denomination, _merkleTreeHeight, _operator, _chainId) public {
}

function _processDeposit() internal {
require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
}

function _processWithdraw(address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) internal {
// sanity checks
require(msg.value == 0, "Message value is supposed to be zero for ETH instance");
require(_refund == 0, "Refund value is supposed to be zero for ETH instance");

(bool success, ) = _recipient.call.value(denomination - _fee)("");
require(success, "payment to _recipient did not go thru");
if (_fee > 0) {
(success, ) = _relayer.call.value(_fee)("");
require(success, "payment to _relayer did not go thru");
}
}
}
6 changes: 6 additions & 0 deletions contracts/evm/webb-anchor/hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.7.3",
};
10 changes: 10 additions & 0 deletions contracts/evm/webb-anchor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"chai": "^4.3.4",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.0.32",
"hardhat": "^2.1.1"
}
}
Loading