instances/comparison/ERC20Tornado.md

957 lines
42 KiB
Markdown
Raw Normal View History

2023-06-15 02:33:27 +03:00
# Comparing ERC20 cloneable instance code with old ERC20 instances
Note that these contracts come directly from the `tornado-core` repository (just had to be inlined), so they are written by the original team.
We are going to be comparing the contracts and explaining their every part.
We are going to start from the `ERC20Tornado.sol` contracts, then work our way up the inheritance tree, and only then analyze the most derived class `ERC20TornadoCloneable.sol`.
## Comparison: `ERC20Tornado` / `TornadoCash_erc20`
Let first first immediately eliminate the functions `_safeErc20Transfer` and `_safeErc20TransferFrom`, these functions are used in the legacy (old) contract for token transfers, and are made obsolete by the use of `SafeERC20` in the new contract.
Now let us start top down and look at the constructor, we will notice the constructor arguments `_hasher` and `_operator` differ. On a high level, this is for the following reason:
First, why has the hasher been added?
Solidity contracts may be linked with libraries deployed at certain addresses, the [original hasher](https://etherscan.io/address/0x83584f83f26aF4eDDA9CBe8C730bc87C364b28fe#code) is a deployed library. Now, the [10 ETH](https://etherscan.io/address/0x910cbd523d972eb0a6f4cae4618ad62622b39dbf/advanced#code) contract, as an example, uses it as a library (scroll to the bottom of the page). The [100,000 DAI](https://etherscan.io/address/0x23773E65ed146A459791799d01336DB287f25334#readContract) contract on the other hand, is using it as an interface.
What is the operator?
The operator was an EOA that could update the SNARK verification key during the trusted setup ceremony. The rights for this have been transferred to the zero address once the ceremony has been finalized. Since the verifier is now set up, there is no need to redo this process.
Besides this, the constructor arguments are the same, the token is only wrapped in `IERC20` immediately but this corresponds to the same ABI encoding which would normally be used. The token assignment is the same in the constructor.
In `_processDeposit`, the logic is the same (or safer), because the `require` check is the same and a `safeTransfer` with the same targets is being used.
If you examine `_processWithdraw`, you will note that similarly the function contains the exact same logic, only a syntactic difference is visible in the line containing `_recipient.call{ value: _refund }("");`. This syntactic change from round brackets to braces is fine and expected.
Besides this there is nothing more to these contracts.
[details="New Contract"]
```
contract ERC20Tornado is Tornado {
using SafeERC20 for IERC20;
IERC20 public token;
constructor(
IVerifier _verifier,
IHasher _hasher,
uint256 _denomination,
uint32 _merkleTreeHeight,
IERC20 _token
) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {
token = _token;
}
function _processDeposit() internal virtual override {
require(msg.value == 0, "ETH value is supposed to be 0 for ERC20 instance");
token.safeTransferFrom(msg.sender, address(this), denomination);
}
function _processWithdraw(
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) internal virtual override {
require(msg.value == _refund, "Incorrect refund amount received by the contract");
token.safeTransfer(_recipient, denomination - _fee);
if (_fee > 0) {
token.safeTransfer(_relayer, _fee);
}
if (_refund > 0) {
(bool success, ) = _recipient.call{ value: _refund }("");
if (!success) {
// let's return _refund back to the relayer
_relayer.transfer(_refund);
}
}
}
}
```
[/details]
[details="Old Contract"]
```
pragma solidity ^0.5.8;
contract TornadoCash_erc20 is Tornado {
address public token;
constructor(
IVerifier _verifier,
uint256 _denomination,
uint32 _merkleTreeHeight,
address _operator,
address _token
) public Tornado(_verifier, _denomination, _merkleTreeHeight, _operator) {
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.");
}
}
}
```
[/details]
## Comparison: `Tornado` (New) / `Tornado` (Old)
The first thing which should be examined is the `IVerifier` contract/interface change. The function signature is the same and all that has been done is transform the contract `IVerifier` into an interface. All public functions are transformed to external in interfaces.
Let us now also immediately note that `operator`, `onlyOperator`, `updateVerifier` and `changeOperator` have been removed. This is in line with what we mentioned earlier.
So now what we are left with is the following:
```
// Old (in different order - same variables & events)
IVerifier public verifier;
mapping(bytes32 => bool) public nullifierHashes;
mapping(bytes32 => bool) public commitments;
uint256 public denomination;
event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
// New
IVerifier public immutable verifier;
mapping(bytes32 => bool) public nullifierHashes;
mapping(bytes32 => bool) public commitments;
uint256 public denomination;
event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
```
The verifier can be immutable since it is assigned at construction time, the variables are otherwise the same as are the events.
So now, in the constructor, the same logic is applied as in the analysis above (for the above contracts constructor), and otherwise, all of the assignments are the same. So that is the constructor eliminated.
The `deposit` function implements the exactly same logic in both contracts, even down to naming changes.
`_processDeposit` has been enabled for inheritability via `virtual`.
The `withdraw` function too implements the exactly same logic in both contracts, as with `deposit`.
`_processWithdraw` has been enabled for inheritability via `virtual`.
The same case with `isSpent`, completely identical logic.
In `isSpentArray`, the index counter has been explicitly declared as `uint256` instead of implicitly with `uint`.
That would be it for the `Tornado` contract, since almost all of the functionality is identical and the unnecessary `operator` logic has been removed, our security only depends on whether internal modifications to functions in contracts up the inheritance hierarchy result in changes to state (or returned values) which is not equivalent to those which would result through the execution of the old contract. This largely depends on the next contract which will be covered.
[details="New Contract"]
interface IVerifier {
function verifyProof(bytes memory \_proof, uint256[6] memory \_input) external returns (bool);
}
abstract contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
IVerifier public immutable verifier;
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;
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 \_hasher the address of MiMC hash contract
@param \_denomination transfer amount for each deposit
@param \_merkleTreeHeight the height of deposits' Merkle Tree
_/
constructor(
IVerifier \_verifier,
IHasher \_hasher,
uint256 \_denomination,
uint32 \_merkleTreeHeight
) MerkleTreeWithHistory(\_merkleTreeHeight, \_hasher) {
require(\_denomination > 0, "denomination should be greater than 0");
verifier = \_verifier;
denomination = \_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 \_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 virtual;
/\*_
@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,
[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 virtual;
/\*_ @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 (uint256 i = 0; i < \_nullifierHashes.length; i++) {
if (isSpent(\_nullifierHashes[i])) {
spent[i] = true;
}
}
}
}
[/details]
[details="Old Contract"]
contract IVerifier {
function verifyProof(bytes memory \_proof, uint256[6] memory \_input) public returns (bool);
}
contract Tornado is MerkleTreeWithHistory, ReentrancyGuard {
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
) public MerkleTreeWithHistory(\_merkleTreeHeight) {
require(\_denomination > 0, "denomination should be greater than 0");
verifier = \_verifier;
operator = \_operator;
denomination = \_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 \_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,
[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;
}
}
[/details]
## Comparison: `MerkleTreeWithHistory` (New) / `MerkleTreeWithHistory` (Old)
Please remind yourself now of the explanation on how the hasher is in the old contract included as a library and in the new contract as an interface.
[Please observe `Action [7]` here](https://etherscan.io/vmtrace?txhash=0x70dfc6bdb304df4d5f3977258347ea5b56705ae4e1b60b6dcca93fc1499f5a70&type=parity). You will note that the hasher library address has been called with the function selector `0xf47d33b5`. If you have chisel installed, or can run hardhat console, ape console, brownie console, or remix, compute the function selector in the following way: `keccak256('MiMCSponge(uint256,uint256)')`
You should receive the same function selector, as I did. Now, if the returns are not good, then the function would revert, and since we are working with unsigned integers, upcasting doesn't lose information, if upcasting were to happen (to `uint256`). So we can say that the function is safe.
So now let us exactly examine how the library call has been replaced with the interface by modifying the logic of `hashLeftRight`.
`hashLeftRight` is called when a commitment is being inserted into the Merkle Deposit tree of the contract and when the entire path must be recalculated from leaf towards root. `hashLeftRight` hashes the value of the paired nodes in the Merkle Tree which is binary. Most of the integration of this logic happens in `_insert`.
The functions has been modified by simply exchanging `Hash.MiMCSponge(R, C)` with `_hasher.MiMCSponge(R, C)`. `_hasher` comes as an added function argument, which is added to the function from the stored immutable. Why is it done like this?
`hashLeftRight` is a public function, it is possible to specify another MiMC Sponge implementation for an external contract and to then use the `hashLeftRight` function for it. Beyond this there is no use, and otherwise the logic in the function is the same.
Let us now examine the required cryptographic constants:
```
// NEW
uint256 public constant FIELD_SIZE =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE =
21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
// OLD
uint256 public constant FIELD_SIZE =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE =
21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
```
Exactly the same. Now let us move towards the constructor. So, the old contract will compute all of the preset zeros of the tree up to the root and then use those to compute the new root in `_insert`.
The new contract does not do this, it already has all of the precomputed values ready in the function `zeros` which replaces the variable by the same name. The last value of `zeros` represents the final result of the recursive invocation of `hashLeftRight` on the zero values and is assigned to the merkle tree root in both contracts.
The `hasher` in the new contract is assigned, we have discussed this. All checks are the same. In short, the constructor contains the same logic like in the old contract. `levels` is equally assigned and used too.
When it comes to the actual `filledSubtrees` and `roots` being transformed into mappings, it is simply done because mappings are cheaper than arrays, and the length value of the former, and iterations, were not used by the contract.
Let us now examine `_insert` in detail. The `nextIndex` increment which happens at line 3 (starting from the signature as line 0) of the function, has been delegated to the bottom. This basically increments the level 0 index for the next commitment.
The `currentLevelHash`, meaning initially the hex encoded commitment in `bytes32` form, as well as `left` and `right` are initialized equally to the old contract.
Now, it is easy to see that inside the iteration the logic is the same. Leaf nodes are only inserted at the bottom of the merkle tree, and then moving upwards the node hashes are computed using `hashLeftRight` meaning MiMC Sponge. Integer division by two always results to a parent node, and parent nodes may be left nodes or right nodes. If the parent nodes are left nodes, meaning even nodes, the right nodes, since we are going left to right, are always empty, thus `currentIndex % 2 == 0` always fills the zero value and also fills `filledSubtrees` (for the next commitment). If we hit a right parent node (an odd node), since we are going left to right, then left node must be non-zero, and as such the formerly stored value is used for computing the hash, of their respective parent node.
The new root index is then calculated, and in the new contract it is cached to preserve gas in the assignment for the root. This contract uses a `100` long merkle root history. In any case, whatever the root history be, this doesn't impact contract safety. The next index is properly returned out. It might seem a little weird that the `nextIndex` is read out again in the old contract (I mean it's really bad gas wise, but otherwise correct), but in both contracts the same logic is executed.
`isKnownRoot` is a similar case, just a cache for gas optimizations, and finally `getLastRoot` is identical.
[details="New Contract"]
```
interface IHasher {
function MiMCSponge(uint256 in_xL, uint256 in_xR) external pure returns (uint256 xL, uint256 xR);
}
contract MerkleTreeWithHistory {
uint256 public constant FIELD_SIZE =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant ZERO_VALUE =
21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
IHasher public immutable hasher;
uint32 public levels;
// the following variables are made public for easier testing and debugging and
// are not supposed to be accessed in regular code
// filledSubtrees and roots could be bytes32[size], but using mappings makes it cheaper because
// it removes index range check on every interaction
mapping(uint256 => bytes32) public filledSubtrees;
mapping(uint256 => bytes32) public roots;
uint32 public constant ROOT_HISTORY_SIZE = 30;
uint32 public currentRootIndex = 0;
uint32 public nextIndex = 0;
constructor(uint32 _levels, IHasher _hasher) {
require(_levels > 0, "_levels should be greater than zero");
require(_levels < 32, "_levels should be less than 32");
levels = _levels;
hasher = _hasher;
for (uint32 i = 0; i < _levels; i++) {
filledSubtrees[i] = zeros(i);
}
roots[0] = zeros(_levels - 1);
}
/**
@dev Hash 2 tree leaves, returns MiMC(_left, _right)
*/
function hashLeftRight(IHasher _hasher, 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 _nextIndex = 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 = zeros(i);
filledSubtrees[i] = currentLevelHash;
} else {
left = filledSubtrees[i];
right = currentLevelHash;
}
currentLevelHash = hashLeftRight(hasher, left, right);
currentIndex /= 2;
}
uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
currentRootIndex = newRootIndex;
roots[newRootIndex] = currentLevelHash;
nextIndex = _nextIndex + 1;
return _nextIndex;
}
/**
@dev Whether the root is present in the root history
*/
function isKnownRoot(bytes32 _root) public view returns (bool) {
if (_root == 0) {
return false;
}
uint32 _currentRootIndex = currentRootIndex;
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];
}
/// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
function zeros(uint256 i) public pure returns (bytes32) {
if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c);
else if (i == 1) return bytes32(0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d);
else if (i == 2) return bytes32(0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200);
else if (i == 3) return bytes32(0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb);
else if (i == 4) return bytes32(0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9);
else if (i == 5) return bytes32(0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959);
else if (i == 6) return bytes32(0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c);
else if (i == 7) return bytes32(0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4);
else if (i == 8) return bytes32(0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80);
else if (i == 9) return bytes32(0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007);
else if (i == 10) return bytes32(0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30);
else if (i == 11) return bytes32(0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5);
else if (i == 12) return bytes32(0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f);
else if (i == 13) return bytes32(0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd);
else if (i == 14) return bytes32(0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108);
else if (i == 15) return bytes32(0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6);
else if (i == 16) return bytes32(0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854);
else if (i == 17) return bytes32(0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea);
else if (i == 18) return bytes32(0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d);
else if (i == 19) return bytes32(0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05);
else if (i == 20) return bytes32(0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4);
else if (i == 21) return bytes32(0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967);
else if (i == 22) return bytes32(0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453);
else if (i == 23) return bytes32(0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48);
else if (i == 24) return bytes32(0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1);
else if (i == 25) return bytes32(0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c);
else if (i == 26) return bytes32(0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99);
else if (i == 27) return bytes32(0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354);
else if (i == 28) return bytes32(0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d);
else if (i == 29) return bytes32(0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427);
else if (i == 30) return bytes32(0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb);
else if (i == 31) return bytes32(0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc);
else revert("Index out of bounds");
}
}
```
[/details]
[details="Old Contract"]
```
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];
}
}
```
[/details]
## `ERC20TornadoCloneable`
This contract introduces a minimum amount of new functionalities for:
1. **Cloneability:** such that ERC20 instances may be deployed via factory. This is done via an `initialize` function, which was implemented by the original team (the entire set of code changes and the factory is what I scraped together from their repositories), except one contract address initialization which works with the below.
#
2. **Downgradeability:** we include (read the entire sentence, it is important) a mechanism to force _withdrawals_ through the router, but only under certain conditions, and only for relayers, and with the _permissionless possibility of downgrading the contract by performing a single function call, given the behaviour of certain contracts._
The code itself is documented, here the explanation.
- **`initialize`**: if you look at the sequence of constructor calls in the above contracts, and then you look at the conditions and state changes in the `initialize` function, beyond extra checks for initalizability, you will notice that the logic exactly follows the constructors. Essentially, the initialize function is only there such that the _immutable minimal proxies_ may be initialized while pointing towards common execution logic. `denomination` and `levels` must be set in the constructor to 1 because otherwise the constructor will revert (otherwise we would have to touch the logic and we don't want to do that), but since these are state variables, we can change them (only!) at initialization time again. Since these are otherwise totally immutable, we use them as a check for the initalization.
Otherwise, the `initialize` function assigns the to-be-deployed new `router` address, which is what brings us to our next topic.
- **`_processWithdraw`**: this function allows us to add extra assertions on top of (respecting the requirements of the parent class contract) the parent classes function of the same name, and the most important one we are adding is the requirement that all calls must come from the `router` _unless_ the `router` has been disabled in this contract (meaning that the `router` is "dead" for this contract and set to `address(0)`). The reasoning for this is because we want relayers part of the sybil resistance system, since otherwise they can flood our frontends, and the things is that we have yet to deploy that infrastructure (the infra is being worked on). In any case, besides this requirement, a trivial requirement is added for a user to not burn themselves by inputting a relayer address as `address(0)` and the burning a fee / refund to it.
We now come to the downgradeability function.
- **`checkInfrastructureIsDead`**: this function is designed to kill the router if either the `RelayerRegistry` proxy contract or the `InstanceRegistry` contract are misbehaving. This means the contracts either revert, give falsy data, the instance has been disabled and the data tells us this, or even if a potential contract hijacker attempts to make the functions waste exactly enough gas for these functions in this call to otherwise pass (would we not explicitly pass gas), but revert in the more gas intensive withdraw call sequence of the router.
If the router is dead, besides the extra check for a user to not burn themselves, the contract reverts to the totally old functionality that you have known before, albeit optimized with the original team's code above.
[details="Code"]
```
// Local imports
import "../core/ERC20Tornado.sol";
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ INTERFACES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
interface ITornadoRouter {
function relayerRegistry() external view returns (address);
function instanceRegistry() external view returns (address);
}
interface IRelayerRegistry {
function burn(address, address, address) external;
}
interface IInstanceRegistry {
function instanceData(address) external view returns (address, uint80, bool, bool);
}
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CONTRACT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
/**
* @title ERC20TornadoCloneable
* @author AlienTornadosaurusHex
* @notice A cloneable ERC20 instances version which must be called (only by relayers) through the router (as
long as registry not dead or we don't disable, what the intention is).
*/
contract ERC20TornadoCloneable is ERC20Tornado {
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
/** @notice The address of the router which may call this contract's withdraw function (unless registry
dead) */
address public router;
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
constructor(
address verifier,
address hasher
) ERC20Tornado(IVerifier(verifier), IHasher(hasher), 1, 1, IERC20(address(0))) {}
/**
* @notice This initialization function follows the logic of the original
instance contructors, meaning ERC20Tornado => Tornado =>
MerkleTreeWithHistory. It includes the appropriate checks which ensure that
initialization may only be done once. It also initializes the address of
the router such that we can, until the relayer registry is not dead, force
transactions through the router (more in the function below on this).
* @param _router The address of the TornadoRouter.
* @param _token The token of the instance.
* @param _denomination The denomination of the instance.
* @param _merkleTreeHeight The merkle tree height of the instance. Usually
20.
*/
function initialize(
address _router,
address _token,
uint256 _denomination,
uint32 _merkleTreeHeight
) public virtual {
// Part of ERC20TornadoCloneable.sol, denomination and levels is obviously
// in all older code set once and never changed again, as such, we can use
// these for `initialize`. The question comes, why is it set to 1 above?
// This only sets in the local implementation address the state of the
// variables to 1, why the original devs chose 1 instead of 0, is because
// internal requires need it to be above 0 for the constructor not to
// revert.
require(denomination == 0 && levels == 0, "already initialized");
// Part of ERC20Tornado.sol constructor
token = IERC20(_token);
// Part of the Tornado.sol constructor,
// Note that the verifier has already been set as an immutable
require(_denomination > 0, "denomination should be greater than 0");
denomination = _denomination;
// Part of the MerkleTreeWithHistory.sol constructor The merkle tree height
// is the same thing as levels, meaning the levels of the merkle... binary
// tree. The variable is properly assigned. Node that just like the
// verifier, the hasher is properly assigned as an immutable.
require(_merkleTreeHeight > 0, "_levels should be greater than zero");
require(_merkleTreeHeight < 32, "_levels should be less than 32");
levels = _merkleTreeHeight;
for (uint32 i = 0; i < _merkleTreeHeight; i++) {
filledSubtrees[i] = zeros(i);
}
roots[0] = zeros(_merkleTreeHeight - 1);
// Part of ERC20TornadoCloneable.sol
router = _router;
}
/**
* @notice This function is a permissionless function which, if the infra is dead, immediately downgrades
the address of the router to address(0), which makes the instance function as any of the older
instances.
* @dev We will disable the current infra and call this function once we deploy a new infrastructure
system which will require the relayers to have a wallet-like smart contract, because then
frontends will be able to build proofs for it, and as such there will be no need for this bullshit.
*/
function checkInfrastructureIsDead() public virtual {
require(router != address(0), "infrastructure already dead");
try
// Amount of gas forwarded specified so a potential hijacker can't break the system
// by allowing this to not revert, but making it enough gas so router reverts
IRelayerRegistry(ITornadoRouter(router).relayerRegistry()).burn{ gas: 100_000 }(
msg.sender, // Such that it can't be hardcoded for which person
address(0),
address(this) // This will get passed in the withdraw function
)
{
/* Do nothing since registry is ok */
} catch {
router = address(0);
return;
}
try
// Amount of gas forwarded specified so a potential hijacker can't break the system
// by allowing this to not revert, but making it enough gas so router reverts
IInstanceRegistry(ITornadoRouter(router).instanceRegistry()).instanceData{ gas: 3_000 }(address(this))
returns (address _token, uint80, bool _isERC20, bool _isEnabled) {
if (IERC20(_token) != token || !_isERC20 || !_isEnabled) {
router = address(0);
}
} catch {
router = address(0);
}
}
/**
* @notice Alright so first of all, the contract is still permissionless with
this function. If the `_relayer` field is set to address(0), transactions
will still work (because of the router). This means, that what this blocks
is relayers which are not registered from processing transactions, while
manual user deposits and transactions still work. This is done because, the
entire intention of the system was to make it economically (similarly to
proof of stake) sybil resistant, but the system has the issue that it can
be avoided. So, this is a temporary fix for this until we don't make a full
system upgrade after which will we disable this trash by disabling the infra
and calling `checkInfrastructureIsDead()`.
* @param _recipient The recipient address of the withdraw.
* @param _relayer The relayer address of the withdraw. Must be a registered
relayer otherwise router reverts.
* @param _fee The fee in bips.
* @param _refund The refund in amount * decimals.
*/
function _processWithdraw(
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) internal virtual override {
// This is the part which we don't check if infra is down
if (router != address(0)) {
require(msg.sender == router, "if infrastructure not dead, router must be caller");
}
// This check should make sure that a user doesn't doom his refund by chance
if (_relayer == address(0)) {
require(_fee == 0 && _refund == 0, "no fees and refunds if no relayer");
}
// Call the regular super version of the function
super._processWithdraw(_recipient, _relayer, _fee, _refund);
}
}
```
[/details]
Thank you for reading this.