611 lines
20 KiB
Solidity
611 lines
20 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import { IHasher } from '../Classic/interfaces/IHasher.sol';
|
|
import { IVerifier } from '../Classic/interfaces/IVerifier.sol';
|
|
import { IWETH } from '../Classic/interfaces/IWETH.sol';
|
|
import { IERC20, SafeERC20 } from "../Classic/libraries/SafeERC20.sol";
|
|
import { ReentrancyGuard } from '../Classic/libraries/ReentrancyGuard.sol';
|
|
|
|
import { IVault } from './interfaces/IVault.sol';
|
|
import { ISignatureTransfer } from './interfaces/ISignatureTransfer.sol';
|
|
import { ParseSignature } from './libraries/ParseSignature.sol';
|
|
|
|
/**
|
|
* @notice (WIP) Singleton Tornado Cash contract
|
|
* @author tornadocontrib.eth
|
|
*/
|
|
contract TornadoV2 is ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
enum SignatureType {
|
|
PERMIT,
|
|
PERMIT2
|
|
}
|
|
|
|
struct PermitCommitments {
|
|
bytes32 instancesHash;
|
|
bytes32 commitmentsHash;
|
|
}
|
|
|
|
bytes public constant COMMITMENT_TYPE = "PermitCommitments(bytes32 instancesHash,bytes32 commitmentsHash)";
|
|
|
|
bytes32 public constant COMMITMENT_TYPEHASH = keccak256(bytes(COMMITMENT_TYPE));
|
|
|
|
string public constant WITNESS_TYPE_STRING = string(abi.encodePacked("PermitCommitments witness)", COMMITMENT_TYPE, "TokenPermissions(address token,uint256 amount)"));
|
|
|
|
// https://docs.uniswap.org/contracts/v3/reference/deployments/ethereum-deployments
|
|
ISignatureTransfer public constant permit2 = ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
|
|
|
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
|
uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
|
|
uint32 public constant ROOT_HISTORY_SIZE = 30;
|
|
|
|
uint16 public constant REVISION = 0;
|
|
|
|
IWETH public weth;
|
|
IHasher public hasher;
|
|
IVerifier public verifier;
|
|
uint32 public levels;
|
|
|
|
address public feeTo;
|
|
uint16 public constant feeRate = 30;
|
|
|
|
event Deposit(
|
|
uint256 indexed id,
|
|
address from,
|
|
bytes32 indexed commitment,
|
|
uint32 leafIndex,
|
|
uint256 timestamp
|
|
);
|
|
|
|
event Withdrawal(
|
|
uint256 indexed id,
|
|
address to,
|
|
bytes32 nullifierHash,
|
|
address indexed relayer,
|
|
uint256 fee,
|
|
uint256 timestamp
|
|
);
|
|
|
|
struct InstanceStorage {
|
|
mapping(uint256 => bytes32) filledSubtrees;
|
|
mapping(uint256 => bytes32) zeros;
|
|
mapping(uint256 => bytes32) roots;
|
|
|
|
uint32 currentRootIndex;
|
|
uint32 nextIndex;
|
|
|
|
IERC20 token;
|
|
uint256 denomination;
|
|
|
|
// total deposits - total withdrawals
|
|
uint32 delta;
|
|
uint256 rewards;
|
|
|
|
IVault vault;
|
|
}
|
|
|
|
struct InstanceView {
|
|
uint32 currentRootIndex;
|
|
uint32 nextIndex;
|
|
|
|
IERC20 token;
|
|
bool native;
|
|
uint256 denomination;
|
|
|
|
// total deposits - total withdrawals
|
|
uint32 delta;
|
|
uint256 rewards;
|
|
|
|
IVault vault;
|
|
}
|
|
|
|
mapping(uint256 => InstanceStorage) internal instances;
|
|
mapping(bytes32 => bool) public hasInstance;
|
|
|
|
mapping(bytes32 => bool) public nullifierHashes;
|
|
mapping(bytes32 => bool) public commitments;
|
|
|
|
uint256 public instanceIndex;
|
|
|
|
error Initialized();
|
|
|
|
error DuplicatedInstance();
|
|
|
|
error InvalidSignatureType();
|
|
|
|
receive() external payable {
|
|
require(msg.sender == address(weth), 'Not from WETH');
|
|
}
|
|
|
|
function initialize(
|
|
IWETH _weth,
|
|
IVerifier _verifier,
|
|
IHasher _hasher,
|
|
uint32 _levels,
|
|
address _feeTo,
|
|
CreateInstance[] memory _instances
|
|
) external virtual {
|
|
if (address(hasher) != address(0)) {
|
|
revert Initialized();
|
|
}
|
|
|
|
require(_levels > 0, "_levels should be greater than zero");
|
|
require(_levels < 32, "_levels should be less than 32");
|
|
|
|
weth = _weth;
|
|
levels = _levels;
|
|
hasher = _hasher;
|
|
verifier = _verifier;
|
|
|
|
feeTo = _feeTo;
|
|
|
|
for (uint256 i; i < _instances.length; ++i) {
|
|
CreateInstance memory _instance = _instances[i];
|
|
createInstance(_instance.token, _instance.denomination, _instance.vault);
|
|
}
|
|
}
|
|
|
|
struct CreateInstance {
|
|
IERC20 token;
|
|
uint256 denomination;
|
|
IVault vault;
|
|
}
|
|
|
|
function createInstance(IERC20 _token, uint256 _denomination, IVault _vault) public virtual nonReentrant returns (uint256 id) {
|
|
id = instanceIndex;
|
|
|
|
bytes32 instanceHash = keccak256(abi.encode(address(_token), _denomination, address(_vault)));
|
|
|
|
if (hasInstance[instanceHash]) {
|
|
revert DuplicatedInstance();
|
|
}
|
|
|
|
hasInstance[instanceHash] = true;
|
|
|
|
instances[id].token = _token;
|
|
instances[id].denomination = _denomination;
|
|
instances[id].vault = _vault;
|
|
|
|
bytes32 currentZero = bytes32(ZERO_VALUE);
|
|
for (uint32 i = 0; i < levels; i++) {
|
|
instances[id].zeros[i] = currentZero;
|
|
instances[id].filledSubtrees[i] = currentZero;
|
|
currentZero = hashLeftRight(currentZero, currentZero);
|
|
}
|
|
|
|
instances[id].roots[0] = currentZero;
|
|
|
|
instanceIndex++;
|
|
}
|
|
|
|
function witness(PermitCommitments memory permitData) public pure returns (bytes32) {
|
|
return keccak256(
|
|
abi.encode(COMMITMENT_TYPEHASH, permitData.instancesHash, permitData.commitmentsHash)
|
|
);
|
|
}
|
|
|
|
function getSignatureType(bytes memory permitData) public pure returns (SignatureType) {
|
|
(bytes1 sigType) = abi.decode(permitData, (bytes1));
|
|
|
|
if (uint8(sigType) == uint8(0)) {
|
|
return SignatureType.PERMIT;
|
|
} else if (uint8(sigType) == uint8(1)) {
|
|
return SignatureType.PERMIT2;
|
|
} else {
|
|
revert InvalidSignatureType();
|
|
}
|
|
}
|
|
|
|
function _parseDeposit(
|
|
uint256[] memory _ids,
|
|
bytes32[] memory _commitments
|
|
) internal view returns (IERC20 _token, uint256 _amount, IVault _vault) {
|
|
require(_ids.length == _commitments.length, "Incorrect input");
|
|
|
|
_token = instances[_ids[0]].token;
|
|
_vault = instances[_ids[0]].vault;
|
|
|
|
for (uint i; i < _ids.length; ++i) {
|
|
require(_token == instances[_ids[i]].token, "Incorrect Token");
|
|
require(_vault == instances[_ids[i]].vault, "Incorrect Vault");
|
|
|
|
_amount += instances[_ids[i]].denomination;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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 _commitments the note commitment, which is PedersenHash(nullifier + secret)
|
|
* @dev Does not support fee-on-transfer tokens
|
|
*/
|
|
function deposit(
|
|
uint256[] memory _ids,
|
|
bytes32[] memory _commitments,
|
|
bytes memory permitData
|
|
) external virtual payable nonReentrant {
|
|
(IERC20 _token, uint256 _amount, IVault _vault) = _parseDeposit(_ids, _commitments);
|
|
bool native = address(_token) == address(weth);
|
|
|
|
require(msg.value == 0 || (native && msg.value == _amount), "Please send correct ETH");
|
|
|
|
// Let Vault handle deposits
|
|
if (address(_vault) != address(0)) {
|
|
|
|
_vault.deposit{ value: msg.value }(
|
|
_ids,
|
|
address(_token),
|
|
msg.sender,
|
|
_amount,
|
|
permitData
|
|
);
|
|
|
|
// Handle native ETH deposits
|
|
} else if (native && msg.value == _amount) {
|
|
|
|
weth.deposit{ value: _amount }();
|
|
|
|
// Handle approved token deposits
|
|
} else if (permitData.length == 0) {
|
|
|
|
_token.safeTransferFrom(msg.sender, address(this), _amount);
|
|
|
|
// Handle signature deposits (PERMIT, PERMIT2)
|
|
} else {
|
|
|
|
SignatureType sigType = getSignatureType(permitData);
|
|
|
|
if (sigType == SignatureType.PERMIT) {
|
|
|
|
(, address owner, bytes memory signature) = abi.decode(permitData, (bytes1, address, bytes));
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = ParseSignature.parse(signature);
|
|
|
|
bytes32 commitmentsHash = keccak256(abi.encodePacked(_commitments));
|
|
|
|
_token.permit(owner, address(this), _amount, uint256(commitmentsHash), v, r, s);
|
|
_token.safeTransferFrom(owner, address(this), _amount);
|
|
|
|
} else {
|
|
|
|
(, address owner, uint256 nonce, uint256 deadline, bytes memory signature) = abi.decode(
|
|
permitData, (bytes1, address, uint256, uint256, bytes)
|
|
);
|
|
|
|
bytes32 witnessHash;
|
|
|
|
{
|
|
witnessHash = witness(PermitCommitments({
|
|
instancesHash: keccak256(abi.encodePacked(_ids)),
|
|
commitmentsHash: keccak256(abi.encodePacked(_commitments))
|
|
}));
|
|
}
|
|
|
|
permit2.permitWitnessTransferFrom(
|
|
ISignatureTransfer.PermitTransferFrom({
|
|
permitted: ISignatureTransfer.TokenPermissions({
|
|
token: address(_token),
|
|
amount: _amount
|
|
}),
|
|
nonce: nonce,
|
|
deadline: deadline
|
|
}),
|
|
ISignatureTransfer.SignatureTransferDetails({
|
|
to: address(this),
|
|
requestedAmount: _amount
|
|
}),
|
|
owner,
|
|
witnessHash,
|
|
WITNESS_TYPE_STRING,
|
|
signature
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
for (uint256 i; i < _commitments.length; ++i) {
|
|
_processDeposit(_ids[i], _commitments[i]);
|
|
instances[_ids[i]].delta++;
|
|
}
|
|
}
|
|
|
|
function _processDeposit(uint256 id, bytes32 _commitment) internal {
|
|
require(!commitments[_commitment], "The commitment has been submitted");
|
|
|
|
uint32 insertedIndex = _insert(id, _commitment);
|
|
commitments[_commitment] = true;
|
|
|
|
emit Deposit(id, tx.origin, _commitment, insertedIndex, block.timestamp);
|
|
}
|
|
|
|
/**
|
|
* @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(
|
|
uint256 id,
|
|
bytes calldata _proof,
|
|
bytes32 _root,
|
|
bytes32 _nullifierHash,
|
|
address payable _recipient,
|
|
address payable _relayer,
|
|
uint256 _relayerFee,
|
|
uint256 _refund,
|
|
bytes memory _data
|
|
) external virtual payable nonReentrant {
|
|
_processWithdraw(id, _proof, _root, _nullifierHash, _recipient, _relayer, _relayerFee, _refund, _data);
|
|
_processSend(id, _recipient, _relayer, _relayerFee, _refund, _data);
|
|
}
|
|
|
|
function _processSend(
|
|
uint256 id,
|
|
address payable _recipient,
|
|
address payable _relayer,
|
|
uint256 _relayerFee,
|
|
uint256 _refund,
|
|
bytes memory _data
|
|
) internal {
|
|
IERC20 _token = instances[id].token;
|
|
bool native = address(_token) == address(weth);
|
|
uint256 _denomination = instances[id].denomination;
|
|
|
|
if (address(instances[id].vault) != address(0)) {
|
|
|
|
instances[id].vault.withdraw{ value: msg.value }(
|
|
id,
|
|
address(_token),
|
|
_recipient,
|
|
_denomination,
|
|
_relayer,
|
|
_relayerFee,
|
|
_refund,
|
|
_data
|
|
);
|
|
|
|
instances[id].delta--;
|
|
|
|
return;
|
|
}
|
|
|
|
if (native) {
|
|
weth.withdraw(_denomination);
|
|
}
|
|
|
|
uint256 toSend;
|
|
{
|
|
(uint256 rewards, uint256 fees) = _processFees(id);
|
|
toSend = _denomination + rewards - fees - _relayerFee;
|
|
}
|
|
|
|
instances[id].delta--;
|
|
|
|
if (native) {
|
|
|
|
// 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: toSend }(_data);
|
|
require(success, "payment to _recipient did not go thru");
|
|
|
|
if (_relayerFee > 0) {
|
|
(success, ) = _relayer.call{ value: _relayerFee }("");
|
|
require(success, "payment to _relayer did not go thru");
|
|
}
|
|
|
|
} else {
|
|
|
|
require(msg.value == _refund, "Incorrect refund amount received by the contract");
|
|
|
|
_token.safeTransfer(_recipient, toSend);
|
|
|
|
if (_relayerFee > 0) {
|
|
_token.safeTransfer(_relayer, _relayerFee);
|
|
}
|
|
|
|
if (_refund > 0 || _data.length != 0) {
|
|
(bool success, ) = _recipient.call{ value: _refund }(_data);
|
|
require(success, "refund to _recipient did not go thru");
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
function _processFees(uint256 id) internal returns (uint256, uint256) {
|
|
IERC20 _token = instances[id].token;
|
|
bool native = address(_token) == address(weth);
|
|
uint256 _denomination = instances[id].denomination;
|
|
|
|
// reward to pay to
|
|
uint256 rewards = instances[id].rewards / (instances[id].delta * _denomination);
|
|
|
|
instances[id].rewards -= rewards;
|
|
|
|
// withdrawal fees to collect (doesn't collect when this is the last withdrawal)
|
|
uint256 fees = (instances[id].delta > 1) ? (_denomination * feeRate / 10000) : 0;
|
|
uint256 devFee = (feeTo != address(0)) ? (fees / 2) : 0;
|
|
uint256 dividend = fees - devFee;
|
|
|
|
if (devFee != 0) {
|
|
if (native) {
|
|
(bool success, ) = feeTo.call{ value: devFee }("");
|
|
require(success, "payment to feeTo did not go thru");
|
|
} else {
|
|
_token.safeTransfer(feeTo, devFee);
|
|
}
|
|
}
|
|
|
|
instances[id].rewards += dividend;
|
|
|
|
return (rewards, fees);
|
|
}
|
|
|
|
function _processWithdraw(
|
|
uint256 id,
|
|
bytes calldata _proof,
|
|
bytes32 _root,
|
|
bytes32 _nullifierHash,
|
|
address payable _recipient,
|
|
address payable _relayer,
|
|
uint256 _fee,
|
|
uint256 _refund,
|
|
bytes memory _data
|
|
) internal {
|
|
require(_fee <= instances[id].denomination, "Fee exceeds transfer value");
|
|
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
|
|
require(isKnownRoot(id, _root), "Cannot find your merkle root"); // Make sure to use a recent one
|
|
|
|
uint256 recipientInt = uint256(uint160(address(_recipient)));
|
|
|
|
if (_data.length != 0) {
|
|
// Make sure the hashed value is under PRIME_Q value
|
|
recipientInt = uint256(uint248(bytes31(keccak256(abi.encode(_recipient, _data)))));
|
|
}
|
|
|
|
require(
|
|
verifier.verifyProof(
|
|
_proof,
|
|
[uint256(_root), uint256(_nullifierHash), recipientInt, uint256(uint160(address(_relayer))), _fee, _refund]
|
|
),
|
|
"Invalid withdraw proof"
|
|
);
|
|
|
|
nullifierHashes[_nullifierHash] = true;
|
|
|
|
emit Withdrawal(id, _recipient, _nullifierHash, _relayer, _fee, block.timestamp);
|
|
}
|
|
|
|
/**
|
|
* @dev Hash 2 tree leaves, returns MiMC(_left, _right)
|
|
*/
|
|
function hashLeftRight(
|
|
bytes32 _left,
|
|
bytes32 _right
|
|
) public view 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(uint256 id, bytes32 _leaf) internal returns (uint32 index) {
|
|
uint32 _nextIndex = instances[id].nextIndex;
|
|
require(_nextIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added");
|
|
uint32 currentIndex = _nextIndex;
|
|
bytes32 currentLevelHash = _leaf;
|
|
bytes32 left;
|
|
bytes32 right;
|
|
|
|
for (uint32 i = 0; i < levels; i++) {
|
|
if (currentIndex % 2 == 0) {
|
|
left = currentLevelHash;
|
|
right = instances[id].zeros[i];
|
|
instances[id].filledSubtrees[i] = currentLevelHash;
|
|
} else {
|
|
left = instances[id].filledSubtrees[i];
|
|
right = currentLevelHash;
|
|
}
|
|
currentLevelHash = hashLeftRight(left, right);
|
|
currentIndex /= 2;
|
|
}
|
|
|
|
uint32 newRootIndex = (instances[id].currentRootIndex + 1) % ROOT_HISTORY_SIZE;
|
|
instances[id].currentRootIndex = newRootIndex;
|
|
instances[id].roots[newRootIndex] = currentLevelHash;
|
|
instances[id].nextIndex = _nextIndex + 1;
|
|
return _nextIndex;
|
|
}
|
|
|
|
/**
|
|
* @dev Whether the root is present in the root history
|
|
*/
|
|
function isKnownRoot(uint256 id, bytes32 _root) public view returns (bool) {
|
|
if (_root == 0) {
|
|
return false;
|
|
}
|
|
uint32 _currentRootIndex = instances[id].currentRootIndex;
|
|
uint32 i = _currentRootIndex;
|
|
do {
|
|
if (_root == instances[id].roots[i]) {
|
|
return true;
|
|
}
|
|
if (i == 0) {
|
|
i = ROOT_HISTORY_SIZE;
|
|
}
|
|
i--;
|
|
} while (i != _currentRootIndex);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @dev Returns the last root
|
|
*/
|
|
function nextIndex(uint256 id) public view returns (uint32) {
|
|
return instances[id].nextIndex;
|
|
}
|
|
|
|
function getLastRoot(uint256 id) public view returns (bytes32) {
|
|
return instances[id].roots[instances[id].currentRootIndex];
|
|
}
|
|
|
|
function token(uint256 id) external view returns (address) {
|
|
return address(instances[id].token);
|
|
}
|
|
|
|
function denomination(uint256 id) external view returns (uint256) {
|
|
return instances[id].denomination;
|
|
}
|
|
|
|
function instance(uint256 id) external view returns (InstanceView memory) {
|
|
return InstanceView({
|
|
currentRootIndex: instances[id].currentRootIndex,
|
|
nextIndex: instances[id].nextIndex,
|
|
token: instances[id].token,
|
|
native: address(instances[id].token) == address(weth),
|
|
denomination: instances[id].denomination,
|
|
delta: instances[id].delta,
|
|
rewards: instances[id].rewards,
|
|
vault: instances[id].vault
|
|
});
|
|
}
|
|
|
|
function filledSubtrees(uint256 id, uint256 index) external view returns (bytes32) {
|
|
return instances[id].filledSubtrees[index];
|
|
}
|
|
|
|
function zeros(uint256 id, uint256 index) external view returns (bytes32) {
|
|
return instances[id].zeros[index];
|
|
}
|
|
|
|
function roots(uint256 id, uint256 index) external view returns (bytes32) {
|
|
return instances[id].roots[index];
|
|
}
|
|
|
|
/**
|
|
* @dev whether a note is already spent
|
|
* It is discouraged to use function on remote RPC environment
|
|
* You should always sync the whole withdrawal events and find from them to ensure privacy
|
|
*/
|
|
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 (uint256 i = 0; i < _nullifierHashes.length; i++) {
|
|
if (isSpent(_nullifierHashes[i])) {
|
|
spent[i] = true;
|
|
}
|
|
}
|
|
}
|
|
} |