diff --git a/contracts/v1/Governance.sol b/contracts/v1/Governance.sol index 23c9001..7031de8 100644 --- a/contracts/v1/Governance.sol +++ b/contracts/v1/Governance.sol @@ -143,7 +143,7 @@ contract Governance is Initializable, Configuration, Delegation, EnsResolve { address proposer, address target, string memory description - ) internal override(Delegation) returns (uint256) { + ) internal virtual override(Delegation) returns (uint256) { uint256 votingPower = lockedBalance[proposer]; require(votingPower >= PROPOSAL_THRESHOLD, "Governance::propose: proposer votes below proposal threshold"); // target should be a contract @@ -181,7 +181,7 @@ contract Governance is Initializable, Configuration, Delegation, EnsResolve { return proposalId; } - function execute(uint256 proposalId) external payable virtual { + function execute(uint256 proposalId) public payable virtual { require(state(proposalId) == ProposalState.AwaitingExecution, "Governance::execute: invalid proposal state"); Proposal storage proposal = proposals[proposalId]; proposal.executed = true; diff --git a/contracts/v4-patch/GovernancePatchUpgrade.sol b/contracts/v4-patch/GovernancePatchUpgrade.sol index 237e30a..7f705b1 100644 --- a/contracts/v4-patch/GovernancePatchUpgrade.sol +++ b/contracts/v4-patch/GovernancePatchUpgrade.sol @@ -1,33 +1,60 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.6.0; +pragma solidity ^0.6.12; pragma experimental ABIEncoderV2; import "../v1/Governance.sol"; import "../v3-relayer-registry/GovernanceStakingUpgrade.sol"; -contract GovernancePatchProposal is GovernanceStakingUpgrade { - mapping(uint256 => bytes32) public _proposalCodeHashes; +contract GovernancePatchUpgrade is GovernanceStakingUpgrade { + mapping(uint256 => bytes32) public proposalCodehashes; - event CodeHashDifferent(address target, bytes32 oldHash, bytes32 newHash); + event CodehashDifferent(address target, bytes32 oldHash, bytes32 newHash); + + // The stakingRewardsAddress sets the immutable to the new staking contract + constructor( + address stakingRewardsAddress, + address gasCompLogic, + address userVaultAddress + ) public GovernanceStakingUpgrade(stakingRewardsAddress, gasCompLogic, userVaultAddress) {} + + // This should guarantee that the proposal extcodehashes are good + function execute(uint256 proposalId) public payable virtual override(Governance) { + require(msg.sender != address(this), "pseudo-external function"); - function execute(uint256 proposalId) external payable virtual override(Governance) { Proposal storage proposal = proposals[proposalId]; - if (proposal.target.codehash == _proposalCodeHashes[proposalId]) { + address target = proposal.target; + + bytes32 proposalCodehash; + + assembly { + proposalCodehash := extcodehash(target) + } + + if (proposalCodehash == proposalCodehashes[proposalId]) { super.execute(proposalId); } else { proposal.executed = true; - emit CodeHashDifferent(proposal.target, _proposalCodeHashes[proposalId], proposal.target.codehash); + emit CodehashDifferent(proposal.target, proposalCodehashes[proposalId], proposalCodehash); } } + // This should store the proposal extcodehash function _propose( address proposer, address target, string memory description - ) internal virtual override(Governance) { - uint256 proposalId = super._propose(proposer, target, description); - _proposalCodeHashes[proposalId] = target.codeHash; + ) internal virtual override(Governance) returns (uint256 proposalId) { + // Implies all former predicates were valid + proposalId = super._propose(proposer, target, description); + + bytes32 proposalCodehash; + + assembly { + proposalCodehash := extcodehash(target) + } + + proposalCodehashes[proposalId] = proposalCodehash; } } diff --git a/contracts/v4-patch/PatchProposal.sol b/contracts/v4-patch/PatchProposal.sol index e69de29..5d0e208 100644 --- a/contracts/v4-patch/PatchProposal.sol +++ b/contracts/v4-patch/PatchProposal.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { LoopbackProxy } from "../v1/LoopbackProxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +import { GovernancePatchUpgrade } from "./GovernancePatchUpgrade.sol"; +import { TornadoStakingRewards } from "./TornadoStakingRewards.sol"; + +contract RelayerRegistryProposal { + using SafeMath for uint256; + using Address for address; + + IERC20 public constant TORN = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); + + address public immutable registry; + + constructor(address _registry) public { + registry = _registry; + } + + // Aight lets do this sirs + function executeProposal() external { + // address(this) has to be governance + address payable governance = payable(address(this)); + + // Get the two contracts gov depends on + address gasComp = address(GovernancePatchUpgrade(governance).gasCompensationVault()); + address vault = address(GovernancePatchUpgrade(governance).userVault()); + + // Get the old staking contract + TornadoStakingRewards oldStaking = TornadoStakingRewards(address(GovernancePatchUpgrade(governance).Staking())); + + // Get all of the TORN out cuz broken + oldStaking.withdrawTorn(TORN.balanceOf(address(oldStaking))); + + // And create a new staking contract + TornadoStakingRewards newStaking = new TornadoStakingRewards(governance, address(TORN), address(registry)); + + // Now upgrade the governance to the latest stuff + LoopbackProxy(payable(governance)).upgradeTo(address(new GovernancePatchUpgrade(address(newStaking), gasComp, vault))); + } +} diff --git a/contracts/v4-patch/RelayerRegistry.sol b/contracts/v4-patch/RelayerRegistry.sol new file mode 100644 index 0000000..3f3b248 --- /dev/null +++ b/contracts/v4-patch/RelayerRegistry.sol @@ -0,0 +1,384 @@ +// 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 "./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, + bytes32 _staking, + bytes32 _feeManager + ) public { + torn = TORN(_torn); + governance = _governance; + ens = IENS(_ens); + staking = TornadoStakingRewards(resolve(_staking)); + feeManager = IFeeManager(resolve(_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/contracts/v4-patch/TornadoStakingRewards.sol b/contracts/v4-patch/TornadoStakingRewards.sol new file mode 100644 index 0000000..79092c8 --- /dev/null +++ b/contracts/v4-patch/TornadoStakingRewards.sol @@ -0,0 +1,143 @@ +// 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/hardhat.config.js b/hardhat.config.js index a832aa7..0ac5761 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -25,6 +25,15 @@ module.exports = { }, }, }, + { + version: '0.8.20', + settings: { + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, ], }, networks: {