315 lines
9.9 KiB
Solidity
315 lines
9.9 KiB
Solidity
|
// SPDX-License-Identifier: MIT
|
||
|
|
||
|
pragma solidity ^0.6.0;
|
||
|
pragma experimental ABIEncoderV2;
|
||
|
|
||
|
import "./interfaces/IVerifier.sol";
|
||
|
import "./interfaces/IRewardSwap.sol";
|
||
|
import "./TornadoTrees.sol";
|
||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||
|
import "@openzeppelin/contracts/math/SafeMath.sol";
|
||
|
import "torn-token/contracts/ENS.sol";
|
||
|
|
||
|
contract Miner is EnsResolve {
|
||
|
using SafeMath for uint256;
|
||
|
|
||
|
IVerifier public rewardVerifier;
|
||
|
IVerifier public withdrawVerifier;
|
||
|
IVerifier public treeUpdateVerifier;
|
||
|
IRewardSwap public immutable rewardSwap;
|
||
|
address public immutable governance;
|
||
|
TornadoTrees public tornadoTrees;
|
||
|
|
||
|
mapping(bytes32 => bool) public accountNullifiers;
|
||
|
mapping(bytes32 => bool) public rewardNullifiers;
|
||
|
mapping(address => uint256) public rates;
|
||
|
|
||
|
uint256 public accountCount;
|
||
|
uint256 public constant ACCOUNT_ROOT_HISTORY_SIZE = 100;
|
||
|
bytes32[ACCOUNT_ROOT_HISTORY_SIZE] public accountRoots;
|
||
|
|
||
|
event NewAccount(bytes32 commitment, bytes32 nullifier, bytes encryptedAccount, uint256 index);
|
||
|
event RateChanged(address instance, uint256 value);
|
||
|
event VerifiersUpdated(address reward, address withdraw, address treeUpdate);
|
||
|
|
||
|
struct TreeUpdateArgs {
|
||
|
bytes32 oldRoot;
|
||
|
bytes32 newRoot;
|
||
|
bytes32 leaf;
|
||
|
uint256 pathIndices;
|
||
|
}
|
||
|
|
||
|
struct AccountUpdate {
|
||
|
bytes32 inputRoot;
|
||
|
bytes32 inputNullifierHash;
|
||
|
bytes32 outputRoot;
|
||
|
uint256 outputPathIndices;
|
||
|
bytes32 outputCommitment;
|
||
|
}
|
||
|
|
||
|
struct RewardExtData {
|
||
|
address relayer;
|
||
|
bytes encryptedAccount;
|
||
|
}
|
||
|
|
||
|
struct RewardArgs {
|
||
|
uint256 rate;
|
||
|
uint256 fee;
|
||
|
address instance;
|
||
|
bytes32 rewardNullifier;
|
||
|
bytes32 extDataHash;
|
||
|
bytes32 depositRoot;
|
||
|
bytes32 withdrawalRoot;
|
||
|
RewardExtData extData;
|
||
|
AccountUpdate account;
|
||
|
}
|
||
|
|
||
|
struct WithdrawExtData {
|
||
|
uint256 fee;
|
||
|
address recipient;
|
||
|
address relayer;
|
||
|
bytes encryptedAccount;
|
||
|
}
|
||
|
|
||
|
struct WithdrawArgs {
|
||
|
uint256 amount;
|
||
|
bytes32 extDataHash;
|
||
|
WithdrawExtData extData;
|
||
|
AccountUpdate account;
|
||
|
}
|
||
|
|
||
|
struct Rate {
|
||
|
bytes32 instance;
|
||
|
uint256 value;
|
||
|
}
|
||
|
|
||
|
modifier onlyGovernance() {
|
||
|
require(msg.sender == governance, "Only governance can perform this action");
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
constructor(
|
||
|
bytes32 _rewardSwap,
|
||
|
bytes32 _governance,
|
||
|
bytes32 _tornadoTrees,
|
||
|
bytes32[3] memory _verifiers,
|
||
|
bytes32 _accountRoot,
|
||
|
Rate[] memory _rates
|
||
|
) public {
|
||
|
rewardSwap = IRewardSwap(resolve(_rewardSwap));
|
||
|
governance = resolve(_governance);
|
||
|
tornadoTrees = TornadoTrees(resolve(_tornadoTrees));
|
||
|
|
||
|
// insert empty tree root without incrementing accountCount counter
|
||
|
accountRoots[0] = _accountRoot;
|
||
|
|
||
|
_setRates(_rates);
|
||
|
// prettier-ignore
|
||
|
_setVerifiers([
|
||
|
IVerifier(resolve(_verifiers[0])),
|
||
|
IVerifier(resolve(_verifiers[1])),
|
||
|
IVerifier(resolve(_verifiers[2]))
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
function reward(bytes memory _proof, RewardArgs memory _args) public {
|
||
|
reward(_proof, _args, new bytes(0), TreeUpdateArgs(0, 0, 0, 0));
|
||
|
}
|
||
|
|
||
|
function batchReward(bytes[] calldata _rewardArgs) external {
|
||
|
for (uint256 i = 0; i < _rewardArgs.length; i++) {
|
||
|
(bytes memory proof, RewardArgs memory args) = abi.decode(_rewardArgs[i], (bytes, RewardArgs));
|
||
|
reward(proof, args);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function reward(
|
||
|
bytes memory _proof,
|
||
|
RewardArgs memory _args,
|
||
|
bytes memory _treeUpdateProof,
|
||
|
TreeUpdateArgs memory _treeUpdateArgs
|
||
|
) public {
|
||
|
validateAccountUpdate(_args.account, _treeUpdateProof, _treeUpdateArgs);
|
||
|
tornadoTrees.validateRoots(_args.depositRoot, _args.withdrawalRoot);
|
||
|
require(_args.extDataHash == keccak248(abi.encode(_args.extData)), "Incorrect external data hash");
|
||
|
require(_args.fee < 2**248, "Fee value out of range");
|
||
|
require(_args.rate == rates[_args.instance] && _args.rate > 0, "Invalid reward rate");
|
||
|
require(!rewardNullifiers[_args.rewardNullifier], "Reward has been already spent");
|
||
|
require(
|
||
|
rewardVerifier.verifyProof(
|
||
|
_proof,
|
||
|
[
|
||
|
uint256(_args.rate),
|
||
|
uint256(_args.fee),
|
||
|
uint256(_args.instance),
|
||
|
uint256(_args.rewardNullifier),
|
||
|
uint256(_args.extDataHash),
|
||
|
uint256(_args.account.inputRoot),
|
||
|
uint256(_args.account.inputNullifierHash),
|
||
|
uint256(_args.account.outputRoot),
|
||
|
uint256(_args.account.outputPathIndices),
|
||
|
uint256(_args.account.outputCommitment),
|
||
|
uint256(_args.depositRoot),
|
||
|
uint256(_args.withdrawalRoot)
|
||
|
]
|
||
|
),
|
||
|
"Invalid reward proof"
|
||
|
);
|
||
|
|
||
|
accountNullifiers[_args.account.inputNullifierHash] = true;
|
||
|
rewardNullifiers[_args.rewardNullifier] = true;
|
||
|
insertAccountRoot(_args.account.inputRoot == getLastAccountRoot() ? _args.account.outputRoot : _treeUpdateArgs.newRoot);
|
||
|
if (_args.fee > 0) {
|
||
|
rewardSwap.swap(_args.extData.relayer, _args.fee);
|
||
|
}
|
||
|
|
||
|
emit NewAccount(
|
||
|
_args.account.outputCommitment,
|
||
|
_args.account.inputNullifierHash,
|
||
|
_args.extData.encryptedAccount,
|
||
|
accountCount - 1
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function withdraw(bytes memory _proof, WithdrawArgs memory _args) public {
|
||
|
withdraw(_proof, _args, new bytes(0), TreeUpdateArgs(0, 0, 0, 0));
|
||
|
}
|
||
|
|
||
|
function withdraw(
|
||
|
bytes memory _proof,
|
||
|
WithdrawArgs memory _args,
|
||
|
bytes memory _treeUpdateProof,
|
||
|
TreeUpdateArgs memory _treeUpdateArgs
|
||
|
) public {
|
||
|
validateAccountUpdate(_args.account, _treeUpdateProof, _treeUpdateArgs);
|
||
|
require(_args.extDataHash == keccak248(abi.encode(_args.extData)), "Incorrect external data hash");
|
||
|
require(_args.amount < 2**248, "Amount value out of range");
|
||
|
require(
|
||
|
withdrawVerifier.verifyProof(
|
||
|
_proof,
|
||
|
[
|
||
|
uint256(_args.amount),
|
||
|
uint256(_args.extDataHash),
|
||
|
uint256(_args.account.inputRoot),
|
||
|
uint256(_args.account.inputNullifierHash),
|
||
|
uint256(_args.account.outputRoot),
|
||
|
uint256(_args.account.outputPathIndices),
|
||
|
uint256(_args.account.outputCommitment)
|
||
|
]
|
||
|
),
|
||
|
"Invalid withdrawal proof"
|
||
|
);
|
||
|
|
||
|
insertAccountRoot(_args.account.inputRoot == getLastAccountRoot() ? _args.account.outputRoot : _treeUpdateArgs.newRoot);
|
||
|
accountNullifiers[_args.account.inputNullifierHash] = true;
|
||
|
// allow submitting noop withdrawals (amount == 0)
|
||
|
uint256 amount = _args.amount.sub(_args.extData.fee, "Amount should be greater than fee");
|
||
|
if (amount > 0) {
|
||
|
rewardSwap.swap(_args.extData.recipient, amount);
|
||
|
}
|
||
|
// Note. The relayer swap rate always will be worse than estimated
|
||
|
if (_args.extData.fee > 0) {
|
||
|
rewardSwap.swap(_args.extData.relayer, _args.extData.fee);
|
||
|
}
|
||
|
|
||
|
emit NewAccount(
|
||
|
_args.account.outputCommitment,
|
||
|
_args.account.inputNullifierHash,
|
||
|
_args.extData.encryptedAccount,
|
||
|
accountCount - 1
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function setRates(Rate[] memory _rates) external onlyGovernance {
|
||
|
_setRates(_rates);
|
||
|
}
|
||
|
|
||
|
function setVerifiers(IVerifier[3] calldata _verifiers) external onlyGovernance {
|
||
|
_setVerifiers(_verifiers);
|
||
|
}
|
||
|
|
||
|
function setTornadoTreesContract(TornadoTrees _tornadoTrees) external onlyGovernance {
|
||
|
tornadoTrees = _tornadoTrees;
|
||
|
}
|
||
|
|
||
|
function setPoolWeight(uint256 _newWeight) external onlyGovernance {
|
||
|
rewardSwap.setPoolWeight(_newWeight);
|
||
|
}
|
||
|
|
||
|
// ------VIEW-------
|
||
|
|
||
|
/**
|
||
|
@dev Whether the root is present in the root history
|
||
|
*/
|
||
|
function isKnownAccountRoot(bytes32 _root, uint256 _index) public view returns (bool) {
|
||
|
return _root != 0 && accountRoots[_index % ACCOUNT_ROOT_HISTORY_SIZE] == _root;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
@dev Returns the last root
|
||
|
*/
|
||
|
function getLastAccountRoot() public view returns (bytes32) {
|
||
|
return accountRoots[accountCount % ACCOUNT_ROOT_HISTORY_SIZE];
|
||
|
}
|
||
|
|
||
|
// -----INTERNAL-------
|
||
|
|
||
|
function keccak248(bytes memory _data) internal pure returns (bytes32) {
|
||
|
return keccak256(_data) & 0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
||
|
}
|
||
|
|
||
|
function validateTreeUpdate(
|
||
|
bytes memory _proof,
|
||
|
TreeUpdateArgs memory _args,
|
||
|
bytes32 _commitment
|
||
|
) internal view {
|
||
|
require(_proof.length > 0, "Outdated account merkle root");
|
||
|
require(_args.oldRoot == getLastAccountRoot(), "Outdated tree update merkle root");
|
||
|
require(_args.leaf == _commitment, "Incorrect commitment inserted");
|
||
|
require(_args.pathIndices == accountCount, "Incorrect account insert index");
|
||
|
require(
|
||
|
treeUpdateVerifier.verifyProof(
|
||
|
_proof,
|
||
|
[uint256(_args.oldRoot), uint256(_args.newRoot), uint256(_args.leaf), uint256(_args.pathIndices)]
|
||
|
),
|
||
|
"Invalid tree update proof"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function validateAccountUpdate(
|
||
|
AccountUpdate memory _account,
|
||
|
bytes memory _treeUpdateProof,
|
||
|
TreeUpdateArgs memory _treeUpdateArgs
|
||
|
) internal view {
|
||
|
require(!accountNullifiers[_account.inputNullifierHash], "Outdated account state");
|
||
|
if (_account.inputRoot != getLastAccountRoot()) {
|
||
|
// _account.outputPathIndices (= last tree leaf index) is always equal to root index in the history mapping
|
||
|
// because we always generate a new root for each new leaf
|
||
|
require(isKnownAccountRoot(_account.inputRoot, _account.outputPathIndices), "Invalid account root");
|
||
|
validateTreeUpdate(_treeUpdateProof, _treeUpdateArgs, _account.outputCommitment);
|
||
|
} else {
|
||
|
require(_account.outputPathIndices == accountCount, "Incorrect account insert index");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function insertAccountRoot(bytes32 _root) internal {
|
||
|
accountRoots[++accountCount % ACCOUNT_ROOT_HISTORY_SIZE] = _root;
|
||
|
}
|
||
|
|
||
|
function _setRates(Rate[] memory _rates) internal {
|
||
|
for (uint256 i = 0; i < _rates.length; i++) {
|
||
|
require(_rates[i].value < 2**128, "Incorrect rate");
|
||
|
address instance = resolve(_rates[i].instance);
|
||
|
rates[instance] = _rates[i].value;
|
||
|
emit RateChanged(instance, _rates[i].value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function _setVerifiers(IVerifier[3] memory _verifiers) internal {
|
||
|
rewardVerifier = _verifiers[0];
|
||
|
withdrawVerifier = _verifiers[1];
|
||
|
treeUpdateVerifier = _verifiers[2];
|
||
|
emit VerifiersUpdated(address(_verifiers[0]), address(_verifiers[1]), address(_verifiers[2]));
|
||
|
}
|
||
|
}
|