diff --git a/src/proposals/InfrastructureUpgradeProposal.sol b/src/proposals/InfrastructureUpgradeProposal.sol index aadeff4..734d572 100644 --- a/src/proposals/InfrastructureUpgradeProposal.sol +++ b/src/proposals/InfrastructureUpgradeProposal.sol @@ -14,6 +14,8 @@ import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ // Local V2 imports +import { RelayerRegistry } from "../v1/RelayerRegistry.sol"; + import { FeeOracleManager } from "../v2/FeeOracleManager.sol"; import { InstanceRegistry } from "../v2/InstanceRegistry.sol"; @@ -76,7 +78,11 @@ contract InfrastructureUpgradeProposal { TornadoRouter router = TornadoRouter(deployedTornadoRouterAddress); - router.initialize(instanceRegistryProxyAddress, relayerRegistryProxyAddress); + router.initialize(instanceRegistryProxyAddress, relayerRegistryProxyAddress, feeManagerProxyAddress); + + // Also set the Tornado Router in the registry + + RelayerRegistry(relayerRegistryProxyAddress).setTornadoRouter(address(router)); // We also now need to upgrade the InstanceRegistry proxy and the FeeManager proxy diff --git a/src/reference/RelayerRegistry.sol b/src/reference/RelayerRegistry.sol new file mode 100644 index 0000000..d83edab --- /dev/null +++ b/src/reference/RelayerRegistry.sol @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { EnsResolve } from "torn-token/contracts/ENS.sol"; +import { TORN } from "torn-token/contracts/TORN.sol"; +import { TornadoStakingRewards } from "../v1/staking/TornadoStakingRewards.sol"; + +interface ITornadoInstance { + function token() external view returns (address); + + function denomination() external view returns (uint256); + + function deposit(bytes32 commitment) external payable; + + function withdraw( + bytes calldata proof, + bytes32 root, + bytes32 nullifierHash, + address payable recipient, + address payable relayer, + uint256 fee, + uint256 refund + ) external payable; +} + +interface IENS { + function owner(bytes32 node) external view returns (address); +} + +/* + * @dev Solidity implementation of the ENS namehash algorithm. + * + * Warning! Does not normalize or validate names before hashing. + * Original version can be found here https://github.com/JonahGroendal/ens-namehash/ + */ +library ENSNamehash { + function namehash(bytes memory domain) internal pure returns (bytes32) { + return namehash(domain, 0); + } + + function namehash(bytes memory domain, uint256 i) internal pure returns (bytes32) { + if (domain.length <= i) return 0x0000000000000000000000000000000000000000000000000000000000000000; + + uint256 len = labelLength(domain, i); + + return keccak256(abi.encodePacked(namehash(domain, i + len + 1), keccak(domain, i, len))); + } + + function labelLength(bytes memory domain, uint256 i) private pure returns (uint256) { + uint256 len; + while (i + len != domain.length && domain[i + len] != 0x2e) { + len++; + } + return len; + } + + function keccak(bytes memory data, uint256 offset, uint256 len) private pure returns (bytes32 ret) { + require(offset + len <= data.length); + assembly { + ret := keccak256(add(add(data, 32), offset), len) + } + } +} + +interface IFeeManager { + function instanceFeeWithUpdate(ITornadoInstance _instance) external returns (uint160); +} + +struct RelayerState { + uint256 balance; + bytes32 ensHash; +} + +/** + * @notice Registry contract, one of the main contracts of this protocol upgrade. + * The contract should store relayers' addresses and data attributed to the + * master address of the relayer. This data includes the relayers stake and + * his ensHash. + * A relayers master address has a number of subaddresses called "workers", + * these are all addresses which burn stake in communication with the proxy. + * If a relayer is not registered, he is not displayed on the frontend. + * @dev CONTRACT RISKS: + * - if setter functions are compromised, relayer metadata would be at risk, including the noted amount + * of his balance + * - if burn function is compromised, relayers run the risk of being unable to handle withdrawals + * - the above risk also applies to the nullify balance function + * + */ +contract RelayerRegistry is Initializable, EnsResolve { + using SafeMath for uint256; + using SafeERC20 for TORN; + using ENSNamehash for bytes; + + TORN public immutable torn; + address public immutable governance; + IENS public immutable ens; + TornadoStakingRewards public immutable staking; + IFeeManager public immutable feeManager; + + address public tornadoRouter; + uint256 public minStakeAmount; + + mapping(address => RelayerState) public relayers; + mapping(address => address) public workers; + + event RelayerBalanceNullified(address relayer); + event WorkerRegistered(address relayer, address worker); + event WorkerUnregistered(address relayer, address worker); + event StakeAddedToRelayer(address relayer, uint256 amountStakeAdded); + event StakeBurned(address relayer, uint256 amountBurned); + event MinimumStakeAmount(uint256 minStakeAmount); + event RouterRegistered(address tornadoRouter); + event RelayerRegistered(bytes32 relayer, string ensName, address relayerAddress, uint256 stakedAmount); + + modifier onlyGovernance() { + require(msg.sender == governance, "only governance"); + _; + } + + modifier onlyTornadoRouter() { + require(msg.sender == tornadoRouter, "only proxy"); + _; + } + + modifier onlyRelayer(address sender, address relayer) { + require(workers[sender] == relayer, "only relayer"); + _; + } + + constructor(address _torn, address _governance, address _ens, address _staking, address _feeManager) + public + { + torn = TORN(_torn); + governance = _governance; + ens = IENS(_ens); + staking = TornadoStakingRewards(_staking); + feeManager = IFeeManager(_feeManager); + } + + /** + * @notice initialize function for upgradeability + * @dev this contract will be deployed behind a proxy and should not assign values at logic address, + * params left out because self explainable + * + */ + function initialize(bytes32 _tornadoRouter) external initializer { + tornadoRouter = resolve(_tornadoRouter); + } + + /** + * @notice This function should register a master address and optionally a set of workeres for a relayer + + * metadata + * @dev Relayer can't steal other relayers workers since they are registered, and a wallet (msg.sender + * check) can always unregister itself + * @param ensName ens name of the relayer + * @param stake the initial amount of stake in TORN the relayer is depositing + * + */ + function register(string calldata ensName, uint256 stake, address[] calldata workersToRegister) + external + { + _register(msg.sender, ensName, stake, workersToRegister); + } + + /** + * @dev Register function equivalent with permit-approval instead of regular approve. + * + */ + function registerPermit( + string calldata ensName, + uint256 stake, + address[] calldata workersToRegister, + address relayer, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + torn.permit(relayer, address(this), stake, deadline, v, r, s); + _register(relayer, ensName, stake, workersToRegister); + } + + function _register( + address relayer, + string calldata ensName, + uint256 stake, + address[] calldata workersToRegister + ) internal { + bytes32 ensHash = bytes(ensName).namehash(); + require(relayer == ens.owner(ensHash), "only ens owner"); + require(workers[relayer] == address(0), "cant register again"); + RelayerState storage metadata = relayers[relayer]; + + require(metadata.ensHash == bytes32(0), "registered already"); + require(stake >= minStakeAmount, "!min_stake"); + + torn.safeTransferFrom(relayer, address(staking), stake); + emit StakeAddedToRelayer(relayer, stake); + + metadata.balance = stake; + metadata.ensHash = ensHash; + workers[relayer] = relayer; + + for (uint256 i = 0; i < workersToRegister.length; i++) { + address worker = workersToRegister[i]; + _registerWorker(relayer, worker); + } + + emit RelayerRegistered(ensHash, ensName, relayer, stake); + } + + /** + * @notice This function should allow relayers to register more workeres + * @param relayer Relayer which should send message from any worker which is already registered + * @param worker Address to register + * + */ + function registerWorker(address relayer, address worker) external onlyRelayer(msg.sender, relayer) { + _registerWorker(relayer, worker); + } + + function _registerWorker(address relayer, address worker) internal { + require(workers[worker] == address(0), "can't steal an address"); + workers[worker] = relayer; + emit WorkerRegistered(relayer, worker); + } + + /** + * @notice This function should allow anybody to unregister an address they own + * @dev designed this way as to allow someone to unregister themselves in case a relayer misbehaves + * - this should be followed by an action like burning relayer stake + * - there was an option of allowing the sender to burn relayer stake in case of malicious behaviour, + * this feature was not included in the end + * - reverts if trying to unregister master, otherwise contract would break. in general, there should + * be no reason to unregister master at all + * + */ + function unregisterWorker(address worker) external { + if (worker != msg.sender) require(workers[worker] == msg.sender, "only owner of worker"); + require(workers[worker] != worker, "cant unregister master"); + emit WorkerUnregistered(workers[worker], worker); + workers[worker] = address(0); + } + + /** + * @notice This function should allow anybody to stake to a relayer more TORN + * @param relayer Relayer main address to stake to + * @param stake Stake to be added to relayer + * + */ + function stakeToRelayer(address relayer, uint256 stake) external { + _stakeToRelayer(msg.sender, relayer, stake); + } + + /** + * @dev stakeToRelayer function equivalent with permit-approval instead of regular approve. + * @param staker address from that stake is paid + * + */ + function stakeToRelayerPermit( + address relayer, + uint256 stake, + address staker, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + torn.permit(staker, address(this), stake, deadline, v, r, s); + _stakeToRelayer(staker, relayer, stake); + } + + function _stakeToRelayer(address staker, address relayer, uint256 stake) internal { + require(workers[relayer] == relayer, "!registered"); + torn.safeTransferFrom(staker, address(staking), stake); + relayers[relayer].balance = stake.add(relayers[relayer].balance); + emit StakeAddedToRelayer(relayer, stake); + } + + /** + * @notice This function should burn some relayer stake on withdraw and notify staking of this + * @dev IMPORTANT FUNCTION: + * - This should be only called by the tornado proxy + * - Should revert if relayer does not call proxy from valid worker + * - Should not overflow + * - Should underflow and revert (SafeMath) on not enough stake (balance) + * @param sender worker to check sender == relayer + * @param relayer address of relayer who's stake is being burned + * @param pool instance to get fee for + * + */ + function burn(address sender, address relayer, ITornadoInstance pool) external onlyTornadoRouter { + address masterAddress = workers[sender]; + if (masterAddress == address(0)) { + require(workers[relayer] == address(0), "Only custom relayer"); + return; + } + + require(masterAddress == relayer, "only relayer"); + uint256 toBurn = feeManager.instanceFeeWithUpdate(pool); + relayers[relayer].balance = relayers[relayer].balance.sub(toBurn); + staking.addBurnRewards(toBurn); + emit StakeBurned(relayer, toBurn); + } + + /** + * @notice This function should allow governance to set the minimum stake amount + * @param minAmount new minimum stake amount + * + */ + function setMinStakeAmount(uint256 minAmount) external onlyGovernance { + minStakeAmount = minAmount; + emit MinimumStakeAmount(minAmount); + } + + /** + * @notice This function should allow governance to set a new tornado proxy address + * @param tornadoRouterAddress address of the new proxy + * + */ + function setTornadoRouter(address tornadoRouterAddress) external onlyGovernance { + tornadoRouter = tornadoRouterAddress; + emit RouterRegistered(tornadoRouterAddress); + } + + /** + * @notice This function should allow governance to nullify a relayers balance + * @dev IMPORTANT FUNCTION: + * - Should nullify the balance + * - Adding nullified balance as rewards was refactored to allow for the flexibility of these funds + * (for gov to operate with them) + * @param relayer address of relayer who's balance is to nullify + * + */ + function nullifyBalance(address relayer) external onlyGovernance { + address masterAddress = workers[relayer]; + require(relayer == masterAddress, "must be master"); + relayers[masterAddress].balance = 0; + emit RelayerBalanceNullified(relayer); + } + + /** + * @notice This function should check if a worker is associated with a relayer + * @param toResolve address to check + * @return true if is associated + * + */ + function isRelayer(address toResolve) external view returns (bool) { + return workers[toResolve] != address(0); + } + + /** + * @notice This function should check if a worker is registered to the relayer stated + * @param relayer relayer to check + * @param toResolve address to check + * @return true if registered + * + */ + function isRelayerRegistered(address relayer, address toResolve) external view returns (bool) { + return workers[toResolve] == relayer; + } + + /** + * @notice This function should get a relayers ensHash + * @param relayer address to fetch for + * @return relayer's ensHash + * + */ + function getRelayerEnsHash(address relayer) external view returns (bytes32) { + return relayers[workers[relayer]].ensHash; + } + + /** + * @notice This function should get a relayers balance + * @param relayer relayer who's balance is to fetch + * @return relayer's balance + * + */ + function getRelayerBalance(address relayer) external view returns (uint256) { + return relayers[workers[relayer]].balance; + } +} diff --git a/src/reference/TornadoStakingRewards.sol b/src/reference/TornadoStakingRewards.sol new file mode 100644 index 0000000..85ff1b8 --- /dev/null +++ b/src/reference/TornadoStakingRewards.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; +import { EnsResolve } from "torn-token/contracts/ENS.sol"; + +interface ITornadoVault { + function withdrawTorn(address recipient, uint256 amount) external; +} + +interface ITornadoGovernance { + function lockedBalance(address account) external view returns (uint256); + + function userVault() external view returns (ITornadoVault); +} + +/** + * @notice This is the staking contract of the governance staking upgrade. + * This contract should hold the staked funds which are received upon relayer registration, + * and properly attribute rewards to addresses without security issues. + * @dev CONTRACT RISKS: + * - Relayer staked TORN at risk if contract is compromised. + * + */ +contract TornadoStakingRewards is Initializable, EnsResolve { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /// @notice 1e25 + uint256 public immutable ratioConstant; + ITornadoGovernance public immutable Governance; + IERC20 public immutable torn; + address public immutable relayerRegistry; + + /// @notice the sum torn_burned_i/locked_amount_i*coefficient where i is incremented at each burn + uint256 public accumulatedRewardPerTorn; + /// @notice notes down accumulatedRewardPerTorn for an address on a lock/unlock/claim + mapping(address => uint256) public accumulatedRewardRateOnLastUpdate; + /// @notice notes down how much an account may claim + mapping(address => uint256) public accumulatedRewards; + + event RewardsUpdated(address indexed account, uint256 rewards); + event RewardsClaimed(address indexed account, uint256 rewardsClaimed); + + modifier onlyGovernance() { + require(msg.sender == address(Governance), "only governance"); + _; + } + + // Minor code change here we won't resolve the registry by ENS + constructor(address governanceAddress, address tornAddress, address _relayerRegistry) public { + Governance = ITornadoGovernance(governanceAddress); + torn = IERC20(tornAddress); + relayerRegistry = _relayerRegistry; + ratioConstant = IERC20(tornAddress).totalSupply(); + } + + /** + * @notice This function should safely send a user his rewards. + * @dev IMPORTANT FUNCTION: + * We know that rewards are going to be updated every time someone locks or unlocks + * so we know that this function can't be used to falsely increase the amount of + * lockedTorn by locking in governance and subsequently calling it. + * - set rewards to 0 greedily + */ + function getReward() external { + uint256 rewards = _updateReward(msg.sender, Governance.lockedBalance(msg.sender)); + rewards = rewards.add(accumulatedRewards[msg.sender]); + accumulatedRewards[msg.sender] = 0; + torn.safeTransfer(msg.sender, rewards); + emit RewardsClaimed(msg.sender, rewards); + } + + /** + * @notice This function should increment the proper amount of rewards per torn for the contract + * @dev IMPORTANT FUNCTION: + * - calculation must not overflow with extreme values + * (amount <= 1e25) * 1e25 / (balance of vault <= 1e25) -> (extreme values) + * @param amount amount to add to the rewards + */ + function addBurnRewards(uint256 amount) external { + require(msg.sender == address(Governance) || msg.sender == relayerRegistry, "unauthorized"); + accumulatedRewardPerTorn = accumulatedRewardPerTorn.add( + amount.mul(ratioConstant).div(torn.balanceOf(address(Governance.userVault()))) + ); + } + + /** + * @notice This function should allow governance to properly update the accumulated rewards rate for an + * account + * @param account address of account to update data for + * @param amountLockedBeforehand the balance locked beforehand in the governance contract + * + */ + function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) + external + onlyGovernance + { + uint256 claimed = _updateReward(account, amountLockedBeforehand); + accumulatedRewards[account] = accumulatedRewards[account].add(claimed); + } + + /** + * @notice This function should allow governance rescue tokens from the staking rewards contract + * + */ + function withdrawTorn(uint256 amount) external onlyGovernance { + if (amount == type(uint256).max) amount = torn.balanceOf(address(this)); + torn.safeTransfer(address(Governance), amount); + } + + /** + * @notice This function should calculated the proper amount of rewards attributed to user since the last + * update + * @dev IMPORTANT FUNCTION: + * - calculation must not overflow with extreme values + * (accumulatedReward <= 1e25) * (lockedBeforehand <= 1e25) / 1e25 + * - result may go to 0, since this implies on 1 TORN locked => accumulatedReward <= 1e7, meaning a + * very small reward + * @param account address of account to calculate rewards for + * @param amountLockedBeforehand the balance locked beforehand in the governance contract + * @return claimed the rewards attributed to user since the last update + */ + function _updateReward(address account, uint256 amountLockedBeforehand) + private + returns (uint256 claimed) + { + if (amountLockedBeforehand != 0) { + claimed = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul( + amountLockedBeforehand + ).div(ratioConstant); + } + accumulatedRewardRateOnLastUpdate[account] = accumulatedRewardPerTorn; + emit RewardsUpdated(account, claimed); + } + + /** + * @notice This function should show a user his rewards. + * @param account address of account to calculate rewards for + */ + function checkReward(address account) external view returns (uint256 rewards) { + uint256 amountLocked = Governance.lockedBalance(account); + if (amountLocked != 0) { + rewards = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul( + amountLocked + ).div(ratioConstant); + } + rewards = rewards.add(accumulatedRewards[account]); + } +} diff --git a/src/v2/FeeOracleManager.sol b/src/v2/FeeOracleManager.sol index 7af3ccd..515acfd 100644 --- a/src/v2/FeeOracleManager.sol +++ b/src/v2/FeeOracleManager.sol @@ -122,6 +122,10 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { _; } + function version() public pure virtual returns (string memory) { + return "v2-slashing-and-oracles"; + } + /** * @dev If there will be a need to initialize the proxy again, simply pad storage and inherit again, * making sure to not reference old data anywhere. @@ -203,11 +207,11 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { oracle.update(torn, _feeInstance); // There must a be a fee set otherwise it's just 0 - uint160 newFee = fee.percent != 0 ? oracle.getFee(torn, _feeInstance) : 0; + fee.amount = fee.percent != 0 ? oracle.getFee(torn, _feeInstance) : 0; // Store feesByInstance[_instance] = FeeData({ - amount: newFee, + amount: fee.amount, percent: fee.percent, updateInterval: fee.updateInterval, lastUpdateTime: uint32(now) @@ -290,10 +294,6 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - function version() public pure virtual returns (string memory) { - return "v2-oracle-manager"; - } - function getUpdatedFeeForInstance(ITornadoInstance instance) public view virtual returns (uint160) { return instanceFeeOracles[instance].getFee(torn, populateInstanceWithFeeData(instance)); } diff --git a/src/v2/InstanceRegistry.sol b/src/v2/InstanceRegistry.sol index 0b1ef2f..e22a0d4 100644 --- a/src/v2/InstanceRegistry.sol +++ b/src/v2/InstanceRegistry.sol @@ -129,6 +129,10 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali _; } + function version() public pure virtual returns (string memory) { + return "v2-slashing-and-oracles"; + } + function initialize(ITornadoInstance[] memory _instances, TornadoRouter _router) external onlyGovernance @@ -259,10 +263,6 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - function version() public pure virtual returns (string memory) { - return "v2-oracle-manager"; - } - function getAllInstances() public view virtual returns (ITornadoInstance[] memory allInstances) { return getInstances(0, instances.length - 1); } diff --git a/src/v2/RelayerRegistry.sol b/src/v2/RelayerRegistry.sol new file mode 100644 index 0000000..6888e90 --- /dev/null +++ b/src/v2/RelayerRegistry.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +// OZ imports + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; + +// Tornado imports + +import { TORN } from "torn-token/contracts/TORN.sol"; +import { EnsResolve } from "torn-token/contracts/ENS.sol"; + +// Local imports + +import { IENS } from "./interfaces/IENS.sol"; + +import { ENSNamehash } from "./libraries/ENSNamehash.sol"; + +import { TornadoStakingRewards } from "./TornadoStakingRewards.sol"; + +import { FeeOracleManager } from "./FeeOracleManager.sol"; + +contract RegistryLegacyStorage { + /** + * @dev From Initializable.sol of first contract + */ + bool private _deprecatedInitialized; + + /** + * @dev From Initializable.sol of first contract + */ + bool private _deprecatedInitializing; + + /** + * @dev This one will be moved for visibility + */ + address internal _oldRouterAddress; + + /** + * @dev We are not using this one because we will pull some magic + */ + uint256 internal _oldMinStakeAmount; +} + +interface MinimumStakingAmountOracle { + function calculateMinimumStakingAmount() external view returns (uint256); +} + +struct RelayerMetadata { + uint256 balance; + bytes32 node; +} + +contract RelayerRegistry is RegistryLegacyStorage, EnsResolve, Initializable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LEGACY STORAGE, IN USE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /** + * @notice Relayer metadata: their (non-refundable) TORN balances and ENS node + */ + mapping(address => RelayerMetadata) public metadata; + + /** + * @notice Subaddresses which may work for a given relayer + */ + mapping(address => address) public workers; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ NEW STORAGE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /** + * @notice Basis points are 1/100th of a % + */ + uint256 public constant BIP_DIVISOR = 10_000; + + /** + * @notice The address of the Governance proxy. + */ + address public immutable governanceProxyAddress; + + /** + * @notice The TORN token. + */ + TORN public immutable torn; + + /** + * @notice The ENS Registry. + */ + IENS public ens; + + /** + * @notice The Fee Oracle Manager contract. + */ + FeeOracleManager public feeOracleManager; + + /** + * @notice The Staking Rewards contract. + */ + TornadoStakingRewards public stakingRewards; + + /** + * @notice The address from which slashes may come from. + */ + address public routerAddress; + + /** + * @notice Since registrations are maybe relatively frequent and this boils down to a view getter, we can + * use a bytes20 to either read out a min stake amount as a number or to fetch it from an address. + */ + bytes20 public minStakeAmountOracle; + + /** + * @notice What the kickback to stakers is on a slash + */ + uint256 public stakerKickbackBips; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + event ENSUpdated(address ens); + event RouterAddressUpdated(address routerAddress); + event StakingRewardsUpdated(address stakingRewards); + event FeeOracleManagerUpdated(address feeOracleManagerAddress); + event MinimumStakingAmountOracleUpdated(bytes20 oracle, bool isContract); + + event WorkerRegistered(address relayer, address worker); + event WorkerUnregistered(address relayer, address worker); + + event StakeAddedToRelayer(address relayer, uint256 amountStakeAdded); + event StakeBurned(address relayer, uint256 amountBurned); + + event RelayerRegistered(string ensName, bytes32 relayer, address relayerAddress, uint256 stakedAmount); + event RelayerSlashed( + address indexed relayer, address beneficiary, uint256 slashedAmount, uint256 stakerKickbackBips + ); + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + constructor(address _governanceProxyAddress, address _tornTokenAddress) public { + governanceProxyAddress = _governanceProxyAddress; + torn = TORN(_tornTokenAddress); + } + + modifier onlyGovernance() { + require(msg.sender == governanceProxyAddress, "RelayerRegistry: onlyGovernance"); + _; + } + + modifier onlyRouter() { + require(msg.sender == routerAddress, "RelayerRegistry: onlyRouter"); + _; + } + + modifier onlyRelayer(address _possibleRelayer, address _possibleWorker) { + require(isRegisteredRelayer(_possibleRelayer, _possibleWorker), "RelayerRegistry: onlyRelayer"); + _; + } + + modifier onlyENSOwner(address _relayer, bytes32 _node) { + require(_relayer == ens.owner(_node), "RelayerRegistry: onlyENSOwner"); + _; + } + + function version() public pure virtual returns (string memory) { + return "v2-slashing-and-oracles"; + } + + function initialize( + address _ensAddress, + address _feeOracleManagerAddress, + address _stakingRewardsAddress, + bytes20 _minStakeAmountOracle, + uint256 _stakerKickbackBips + ) public virtual initializer { + feeOracleManager = FeeOracleManager(_feeOracleManagerAddress); + ens = IENS(_ensAddress); + stakingRewards = TornadoStakingRewards(_stakingRewardsAddress); + minStakeAmountOracle = _minStakeAmountOracle; + stakerKickbackBips = _stakerKickbackBips; + } + + function register(string calldata _name, uint256 _staked, address[] calldata _workers) external { + _register(_name, ENSNamehash.namehash(bytes(_name)), msg.sender, _staked, _workers); + } + + function registerPermit( + string calldata _name, + uint256 _staked, + address[] calldata _workers, + address _relayer, + uint256 _deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + torn.permit(_relayer, address(this), _staked, _deadline, v, r, s); + _register(_name, ENSNamehash.namehash(bytes(_name)), _relayer, _staked, _workers); + } + + function _register( + string memory _name, + bytes32 _node, + address _relayer, + uint256 _staked, + address[] calldata _workers + ) internal onlyENSOwner(_relayer, _node) { + bytes32 currentNodeValue = metadata[_relayer].node; + + uint256 minStakeAmount = getMinimumStakingAmount(); + + require(minStakeAmount <= _staked, "RelayerRegistry: stake too low"); + + require( + workers[_relayer] == address(0) && currentNodeValue == bytes32(0), + "RelayerRegistry: already registered" + ); + + IERC20(torn).safeTransferFrom(_relayer, address(stakingRewards), _staked); + + metadata[_relayer] = RelayerMetadata({ balance: _staked, node: _node }); + + workers[_relayer] = _relayer; + + for (uint256 i = 0; i < _workers.length; i++) { + _registerWorker(_relayer, _workers[i]); + } + + emit RelayerRegistered(_name, _node, _relayer, _staked); + } + + function registerWorker(address _relayer, address _worker) external onlyRelayer(_relayer, msg.sender) { + _registerWorker(_relayer, _worker); + } + + function _registerWorker(address _relayer, address _worker) internal { + require(workers[_worker] == address(0), "RelayerRegistry: already registered"); + workers[_worker] = _relayer; + emit WorkerRegistered(_relayer, _worker); + } + + function unregisterWorker(address _worker) external { + // We are doing this to not allow a relayer to unregister a worker and then slash themselves + require(workers[_worker] == msg.sender, "RelayerRegistry: sender must own worker"); + require(workers[_worker] != _worker, "RelayerRegistry: cant remove owner"); + + workers[_worker] = address(0); + + emit WorkerUnregistered(workers[_worker], _worker); + } + + function stakeToRelayer(address _relayer, uint256 _staked) external { + _addStake(msg.sender, _relayer, _staked); + } + + function stakeToRelayerPermit( + address _relayer, + uint256 _staked, + address _staker, + uint256 _deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + torn.permit(_staker, address(this), _staked, _deadline, v, r, s); + _addStake(_staker, _relayer, _staked); + } + + function _addStake(address _staker, address _relayer, uint256 _staked) + internal + onlyRelayer(_relayer, _relayer) + { + IERC20(torn).safeTransferFrom(_staker, address(stakingRewards), _staked); + metadata[_relayer].balance = _staked.add(metadata[_relayer].balance); + emit StakeAddedToRelayer(_relayer, _staked); + } + + function deductBalance(address _sender, address _relayer, uint256 _burned) + public + virtual + onlyRouter + onlyRelayer(_relayer, _sender) + { + metadata[_relayer].balance = metadata[_relayer].balance.sub(_burned); + stakingRewards.addBurnRewards(_burned); + emit StakeBurned(_relayer, _burned); + } + + function slashRelayer(address _beneficiary, address _relayer) public virtual onlyRouter { + // Store to reward + uint256 balToSlash = metadata[_relayer].balance; + + // Slash + metadata[_relayer].balance = 0; + + // We can't give the slasher the full balance because in that case the relayer will just slash himself + stakingRewards.rewardSlasher( + _beneficiary, (balToSlash * (BIP_DIVISOR - stakerKickbackBips)) / BIP_DIVISOR + ); + + // Give rest to Gov stakers + stakingRewards.addBurnRewards(stakerKickbackBips / BIP_DIVISOR); + + emit RelayerSlashed(_relayer, _beneficiary, balToSlash, stakerKickbackBips); + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function setENS(address _newENSAddress) public virtual onlyGovernance { + ens = IENS(_newENSAddress); + emit ENSUpdated(_newENSAddress); + } + + function setRouterAddress(address _newRouterAddress) public virtual onlyGovernance { + routerAddress = _newRouterAddress; + emit RouterAddressUpdated(_newRouterAddress); + } + + function setStakingRewards(address _newStakingRewardsAddress) public virtual onlyGovernance { + stakingRewards = TornadoStakingRewards(_newStakingRewardsAddress); + emit StakingRewardsUpdated(_newStakingRewardsAddress); + } + + function setFeeOracleManager(address _newFeeOracleManagerAddress) public virtual onlyGovernance { + feeOracleManager = FeeOracleManager(_newFeeOracleManagerAddress); + emit FeeOracleManagerUpdated(_newFeeOracleManagerAddress); + } + + function setMinimumStakingAmountOracle(bytes20 _newOracle, bool _isContractOracle) + public + virtual + onlyGovernance + { + bool oracleIsContract = Address.isContract(address(_newOracle)); + if (_isContractOracle) { + require(oracleIsContract, "RelayerRegistry: oracle is not contract"); + } else { + require(!oracleIsContract, "RelayerRegistry: oracle is contract"); + } + minStakeAmountOracle = _newOracle; + emit MinimumStakingAmountOracleUpdated(_newOracle, oracleIsContract); + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function isWorker(address _possibleWorker) public view virtual returns (bool) { + return workers[_possibleWorker] != address(0); + } + + function isRegisteredRelayer(address _possibleRelayer, address _possibleWorker) + public + view + virtual + returns (bool) + { + return _possibleRelayer == workers[_possibleWorker]; + } + + function getRelayerENSNode(address _relayer) public view virtual returns (bytes32) { + return metadata[workers[_relayer]].node; + } + + function getRelayerBalance(address _relayer) public view virtual returns (uint256) { + return metadata[workers[_relayer]].balance; + } + + function getOwningRelayerOfWorker(address _worker) public view virtual returns (address) { + return workers[_worker]; + } + + function getMinimumStakingAmount() public view virtual returns (uint256) { + bytes20 _minStakeAmountOracle = minStakeAmountOracle; + if (Address.isContract(address(_minStakeAmountOracle))) { + return MinimumStakingAmountOracle(address(_minStakeAmountOracle)).calculateMinimumStakingAmount(); + } + return uint160(_minStakeAmountOracle); + } +} diff --git a/src/v2/TornadoRouter.sol b/src/v2/TornadoRouter.sol index dc3a3ed..efd2707 100644 --- a/src/v2/TornadoRouter.sol +++ b/src/v2/TornadoRouter.sol @@ -8,6 +8,7 @@ pragma experimental ABIEncoderV2; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Math } from "@openzeppelin/contracts/math/Math.sol"; // Tornado Imports @@ -16,13 +17,14 @@ import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ // Local imports -import { RelayerRegistry } from "../v1/RelayerRegistry.sol"; +import { RelayerRegistry } from "./RelayerRegistry.sol"; import { InstanceRegistry, InstanceState } from "./InstanceRegistry.sol"; +import { FeeOracleManager } from "./FeeOracleManager.sol"; /** * @title TornadoRouter * @author AlienTornadosaurusHex - * @notice This contracts is a router for all Tornado Cash deposits and withdrawals + * @notice This contract is a router for all Tornado Cash deposits and withdrawals * @dev This is an improved version of the TornadoRouter with a modified design from the original contract. */ contract TornadoRouter is Initializable { @@ -36,22 +38,37 @@ contract TornadoRouter is Initializable { address public immutable governanceProxyAddress; /** - * @notice The instance registry + * @notice The Instance Registry */ InstanceRegistry public instanceRegistry; /** - * @notice The relayer registry + * @notice The Relayer Registry */ RelayerRegistry public relayerRegistry; + /** + * @notice The Fee Oracle Manager + */ + FeeOracleManager public feeOracleManager; + + /** + * @notice Note down proofs for slashing relayers + */ + mapping(bytes32 => bool) public slashProofs; + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ event EncryptedNote(address indexed sender, bytes encryptedNote); event TokenApproved(address indexed spender, uint256 amount); + event WithdrawalWithRelayer( + address sender, address relayer, address instanceAddress, bytes32 nullifierHash + ); + event InstanceRegistryUpdated(address newInstanceRegistryProxyAddress); event RelayerRegistryUpdated(address newRelayerRegistryProxyAddress); + event FeeOracleManagerUpdated(address newFeeOracleManagerProxyAddress); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -69,17 +86,23 @@ contract TornadoRouter is Initializable { _; } - function initialize(address _instanceRegistryAddress, address _relayerRegistryAddress) - external - onlyGovernance - initializer - { + function version() public pure virtual returns (string memory) { + return "v2-slashing-and-oracles"; + } + + function initialize( + address _instanceRegistryAddress, + address _relayerRegistryAddress, + address _feeOracleManager + ) external onlyGovernance initializer { instanceRegistry = InstanceRegistry(_instanceRegistryAddress); relayerRegistry = RelayerRegistry(_relayerRegistryAddress); + feeOracleManager = FeeOracleManager(_feeOracleManager); } /** - * @notice Function to deposit into a Tornado instance. + * @notice Function to deposit into a Tornado instance. We don't really case if an external contract + * breaks for deposit since deposits can go through the instances too if need be. * @param _tornado The instance to deposit into (address). * @param _commitment The commitment which will be added to the Merkle Tree. * @param _encryptedNote An encrypted note tied to the commitment which may be logged. @@ -104,17 +127,83 @@ contract TornadoRouter is Initializable { } /** - * @notice Withdraw from a Tornado Instance. NOTE: Until infrastructure, which will include a - * smart-contract relayer wallet, isn't deployed, this function will be the only gateway for withdrawing - * from the _new_ instances. But fear not! Each instance contains a permissionless function by the name of - * `checkInfrastructureIsDead()`, which can be used in the case that the infra is deactivated or - * malfunctions (this contract can't malfunction to block withdrawals). It makes the instance work just - * like a regular one again! The intention is to call the formerly mentioned function when the new - * infrastructure is deployed, and until then, use the simple fix in the new instances to route - * withdrawals - * through the registry. ALSO NOTE, that manual depositors and withdrawers will still be able to - * permissionlessly use the instances, they anyways do not rely on the `relayer` address field, and as - * such they will be able to claim their deposits no matter whether the check is effective or not! + * @notice Withdrawal function for Governance relayers by which they can safely process a withdrawal + * without getting slashed (which kicks them off the frontend). + * @param _tornado The Tornado instance to withdraw from. + * @param _proof Bytes proof data. + * @param _root A current or historical bytes32 root of the Merkle Tree within the proofs context. + * @param _nullifierHash The bytes32 nullifierHash for the deposit. + * @param _recipient The address of recipient for withdrawn funds. + * @param _relayer The address of the relayer which will be making the withdrawal. + * @param _fee The fee in bips to pay the relayer. + * @param _refund If swapping into ETH on the other side, use this to specify how much should be paid for + */ + function processWithdrawAsGovernanceRelayer( + ITornadoInstance _tornado, + bytes calldata _proof, + bytes32 _root, + bytes32 _nullifierHash, + address payable _recipient, + address payable _relayer, + uint256 _fee, + uint256 _refund + ) public payable virtual { + // Deduct balance immediately + relayerRegistry.deductBalance(msg.sender, _relayer, feeOracleManager.updateFee(_tornado, true)); + // Then call the regular withdraw function which will make the relayer slashable + withdraw(_tornado, _proof, _root, _nullifierHash, _recipient, _relayer, _fee, _refund); + // But then undo this and get gas refund + slashProofs[keccak256(abi.encode(msg.sender, _tornado, _relayer, _nullifierHash))] = false; + } + + /** + * @notice Slash a relayer by proving that there is a nullifier hash for which he has not been deducted, + * get tokens back for it. + * @dev Events are emitted when withdrawals are processed, that is how you can find hashes. + * @param _relayer The relayer to be slashed + * @param _instanceAddress The instance for which nullifier hash is valid for + * @param _nullifierHash The nullifier hash + */ + function slashRelayer(address _sender, address _relayer, address _instanceAddress, bytes32 _nullifierHash) + public + virtual + { + // Construct the proof, msg.sender can't lie about _sender, because proof will be invalid + // And _sender was withdraw msg.sender + bytes32 possibleSlashProof = + keccak256(abi.encode(_sender, _relayer, _instanceAddress, _nullifierHash)); + + // Proof must be valid + require(slashProofs[possibleSlashProof], "TornadoRouter: invalid proof"); + + // Since the proof is valid, we know the _sender is correct + // First step against multicall + require(msg.sender != _sender, "TornadoRouter: slasher is withdrawer"); + + // Also require the sender is not a contract, multicall could proxy over one + require(!Address.isContract(msg.sender), "TornadoRouter: sender is contract"); + + // Further it doesn't make sense then to really assert it's not a worker, + // since relayer could use a different account. + // + // In any case searchers and the balance not being returned in full should prevent relayers from not + // behaving well, if they are listed on the frontend + // + // Otherwise they are on their own + + // Slash relayer first, it also logs the slash event + relayerRegistry.slashRelayer(msg.sender, _relayer); + + // Then set this back false + slashProofs[possibleSlashProof] = false; + } + + /** + * @notice Withdraw from a Tornado Instance. Note that the withdraw function is totally independent of any + * Governance contracts, that means, even if the instances demand that only Router is the caller, and the + * Governance contracts get fucked, depositors won't lose their money AND third-party relayers will still + * have the opportunity to operate. Relayers not affiliated with Governance, sorry for that one extra + * SSTORE and emit! * @param _tornado The Tornado instance to withdraw from. * @param _proof Bytes proof data. * @param _root A current or historical bytes32 root of the Merkle Tree within the proofs context. @@ -135,18 +224,9 @@ contract TornadoRouter is Initializable { uint256 _fee, uint256 _refund ) public payable virtual { - // If an instance is turned off, we still want withdrawals to pass - // so instead of requiring `isEnabled`, we will only use it to - // decide whether relayers should burn or not. This is beneficial - // for every party involved! - (,,, bool isEnabled) = instanceRegistry.instanceData(_tornado); - - // Remove the require and instead just skip if there is no - // relayer being used or the instance is not enabled. Note that, - // even if the registry gets hijacked, you can just do - // manual withdrawals (using the SDK, for example). - if (_relayer != address(0) && isEnabled) { - relayerRegistry.burn(msg.sender, _relayer, _tornado); + if (_relayer != address(0)) { + slashProofs[keccak256(abi.encode(msg.sender, _relayer, address(_tornado), _nullifierHash))] = true; + emit WithdrawalWithRelayer(msg.sender, _relayer, address(_tornado), _nullifierHash); } _tornado.withdraw{ value: msg.value }( @@ -160,6 +240,9 @@ contract TornadoRouter is Initializable { } } + /** + * @notice Note that this contract doesn't leave dust unless someone sends dust to it. + */ function rescueTokens(IERC20 _token, address payable _to, uint256 _amount) public virtual @@ -201,9 +284,8 @@ contract TornadoRouter is Initializable { emit RelayerRegistryUpdated(_newRelayerRegistryProxyAddress); } - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - - function version() public pure virtual returns (string memory) { - return "v2-oracle-manager"; + function setFeeOracleManager(address _newFeeOracleManagerProxyAddress) external onlyGovernance { + feeOracleManager = FeeOracleManager(_newFeeOracleManagerProxyAddress); + emit FeeOracleManagerUpdated(_newFeeOracleManagerProxyAddress); } } diff --git a/src/v2/TornadoStakingRewards.sol b/src/v2/TornadoStakingRewards.sol new file mode 100644 index 0000000..c1bcfbb --- /dev/null +++ b/src/v2/TornadoStakingRewards.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol"; +import { EnsResolve } from "torn-token/contracts/ENS.sol"; + +interface ITornadoVault { + function withdrawTorn(address recipient, uint256 amount) external; +} + +interface ITornadoGovernance { + function lockedBalance(address account) external view returns (uint256); + + function userVault() external view returns (ITornadoVault); +} + +/** + * @notice This is the staking contract of the governance staking upgrade. + * This contract should hold the staked funds which are received upon relayer registration, + * and properly attribute rewards to addresses without security issues. + * @dev CONTRACT RISKS: + * - Relayer staked TORN at risk if contract is compromised. + * + */ +contract TornadoStakingRewards is Initializable, EnsResolve { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /// @notice 1e25 + uint256 public immutable ratioConstant; + ITornadoGovernance public immutable Governance; + IERC20 public immutable torn; + address public immutable relayerRegistry; + + /// @notice the sum torn_burned_i/locked_amount_i*coefficient where i is incremented at each burn + uint256 public accumulatedRewardPerTorn; + /// @notice notes down accumulatedRewardPerTorn for an address on a lock/unlock/claim + mapping(address => uint256) public accumulatedRewardRateOnLastUpdate; + /// @notice notes down how much an account may claim + mapping(address => uint256) public accumulatedRewards; + + event RewardsUpdated(address indexed account, uint256 rewards); + event RewardsClaimed(address indexed account, uint256 rewardsClaimed); + + modifier onlyGovernance() { + require(msg.sender == address(Governance), "only governance"); + _; + } + + modifier onlyRelayerRegistry() { + require(msg.sender == relayerRegistry, "only relayer registry"); + _; + } + + // Minor code change here we won't resolve the registry by ENS + constructor(address governanceAddress, address tornAddress, address _relayerRegistry) public { + Governance = ITornadoGovernance(governanceAddress); + torn = IERC20(tornAddress); + relayerRegistry = _relayerRegistry; + ratioConstant = IERC20(tornAddress).totalSupply(); + } + + /** + * @notice This function should safely send a user his rewards. + * @dev IMPORTANT FUNCTION: + * We know that rewards are going to be updated every time someone locks or unlocks + * so we know that this function can't be used to falsely increase the amount of + * lockedTorn by locking in governance and subsequently calling it. + * - set rewards to 0 greedily + */ + function getReward() external { + uint256 rewards = _updateReward(msg.sender, Governance.lockedBalance(msg.sender)); + rewards = rewards.add(accumulatedRewards[msg.sender]); + accumulatedRewards[msg.sender] = 0; + torn.safeTransfer(msg.sender, rewards); + emit RewardsClaimed(msg.sender, rewards); + } + + /** + * @notice This function should increment the proper amount of rewards per torn for the contract + * @dev IMPORTANT FUNCTION: + * - calculation must not overflow with extreme values + * (amount <= 1e25) * 1e25 / (balance of vault <= 1e25) -> (extreme values) + * @param amount amount to add to the rewards + */ + function addBurnRewards(uint256 amount) external { + require(msg.sender == address(Governance) || msg.sender == relayerRegistry, "unauthorized"); + accumulatedRewardPerTorn = accumulatedRewardPerTorn.add( + amount.mul(ratioConstant).div(torn.balanceOf(address(Governance.userVault()))) + ); + } + + /** + * @notice This function should allow governance to properly update the accumulated rewards rate for an + * account + * @param account address of account to update data for + * @param amountLockedBeforehand the balance locked beforehand in the governance contract + * + */ + function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) + external + onlyGovernance + { + uint256 claimed = _updateReward(account, amountLockedBeforehand); + accumulatedRewards[account] = accumulatedRewards[account].add(claimed); + } + + /** + * @notice This function should allow governance rescue tokens from the staking rewards contract + */ + function withdrawTorn(uint256 amount) external onlyGovernance { + if (amount == type(uint256).max) amount = torn.balanceOf(address(this)); + torn.safeTransfer(address(Governance), amount); + } + + /** + * @notice This function may be used to reward a slasher (since it is registry gated) + */ + function rewardSlasher(address _slasher, uint256 _amount) external onlyRelayerRegistry { + torn.safeTransfer(_slasher, _amount); + } + + /** + * @notice This function should calculated the proper amount of rewards attributed to user since the last + * update + * @dev IMPORTANT FUNCTION: + * - calculation must not overflow with extreme values + * (accumulatedReward <= 1e25) * (lockedBeforehand <= 1e25) / 1e25 + * - result may go to 0, since this implies on 1 TORN locked => accumulatedReward <= 1e7, meaning a + * very small reward + * @param account address of account to calculate rewards for + * @param amountLockedBeforehand the balance locked beforehand in the governance contract + * @return claimed the rewards attributed to user since the last update + */ + function _updateReward(address account, uint256 amountLockedBeforehand) + private + returns (uint256 claimed) + { + if (amountLockedBeforehand != 0) { + claimed = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul( + amountLockedBeforehand + ).div(ratioConstant); + } + accumulatedRewardRateOnLastUpdate[account] = accumulatedRewardPerTorn; + emit RewardsUpdated(account, claimed); + } + + /** + * @notice This function should show a user his rewards. + * @param account address of account to calculate rewards for + */ + function checkReward(address account) external view returns (uint256 rewards) { + uint256 amountLocked = Governance.lockedBalance(account); + if (amountLocked != 0) { + rewards = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul( + amountLocked + ).div(ratioConstant); + } + rewards = rewards.add(accumulatedRewards[account]); + } +} diff --git a/src/v2/interfaces/IENS.sol b/src/v2/interfaces/IENS.sol new file mode 100644 index 0000000..1944c95 --- /dev/null +++ b/src/v2/interfaces/IENS.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +interface IENS { + function owner(bytes32 node) external view returns (address); +} diff --git a/src/v2/libraries/ENSNamehash.sol b/src/v2/libraries/ENSNamehash.sol new file mode 100644 index 0000000..274f406 --- /dev/null +++ b/src/v2/libraries/ENSNamehash.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +/* + * @dev Solidity implementation of the ENS namehash algorithm. + * + * Warning! Does not normalize or validate names before hashing. + * Original version can be found here https://github.com/JonahGroendal/ens-namehash/ + */ +library ENSNamehash { + function namehash(bytes memory domain) internal pure returns (bytes32) { + return namehash(domain, 0); + } + + function namehash(bytes memory domain, uint256 i) internal pure returns (bytes32) { + if (domain.length <= i) return 0x0000000000000000000000000000000000000000000000000000000000000000; + + uint256 len = labelLength(domain, i); + + return keccak256(abi.encodePacked(namehash(domain, i + len + 1), keccak(domain, i, len))); + } + + function labelLength(bytes memory domain, uint256 i) private pure returns (uint256) { + uint256 len; + while (i + len != domain.length && domain[i + len] != 0x2e) { + len++; + } + return len; + } + + function keccak(bytes memory data, uint256 offset, uint256 len) private pure returns (bytes32 ret) { + require(offset + len <= data.length); + assembly { + ret := keccak256(add(add(data, 32), offset), len) + } + } +} diff --git a/test/ProposalTests.sol b/test/ProposalTests.sol index 2cf90c6..2200067 100644 --- a/test/ProposalTests.sol +++ b/test/ProposalTests.sol @@ -347,10 +347,10 @@ contract ProposalTests is Instances, TornadoProposalTest { vm.expectRevert(); vm.prank(address(governance)); - router.initialize(address(instanceRegistry), address(instanceRegistry)); + router.initialize(address(instanceRegistry), address(instanceRegistry), address(feeOracleManager)); vm.expectRevert(); - router.initialize(address(instanceRegistry), address(instanceRegistry)); + router.initialize(address(instanceRegistry), address(instanceRegistry), address(feeOracleManager)); // Check fee logic and print to console.