|
| 1 | +pragma solidity <0.6 >=0.4.24; |
| 2 | + |
| 3 | +import "./math/SafeMath.sol"; |
| 4 | +import "./token/IMintableToken.sol"; |
| 5 | +import "./utils/Address.sol"; |
| 6 | +import "./zksnarklib/MerkleTreeWithHistory.sol"; |
| 7 | +import "./zksnarklib/IVerifier.sol"; |
| 8 | +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; |
| 9 | + |
| 10 | +contract CycloneV2dot2 is MerkleTreeWithHistory, ReentrancyGuard { |
| 11 | + |
| 12 | + using SafeMath for uint256; |
| 13 | + uint256 public tokenDenomination; // (10K or 100k or 1M) * 10^18 |
| 14 | + uint256 public coinDenomination; |
| 15 | + uint256 public initCYCDenomination; |
| 16 | + mapping(bytes32 => bool) public nullifierHashes; |
| 17 | + mapping(bytes32 => bool) public commitments; // we store all commitments just to prevent accidental deposits with the same commitment |
| 18 | + IVerifier public verifier; |
| 19 | + IERC20 public token; |
| 20 | + IMintableToken public cycToken; |
| 21 | + address public treasury; |
| 22 | + address public govDAO; |
| 23 | + uint256 public numOfShares; |
| 24 | + uint256 public lastRewardBlock; |
| 25 | + uint256 public rewardPerBlock; |
| 26 | + uint256 public accumulateCYC; |
| 27 | + uint256 public anonymityFee; |
| 28 | + |
| 29 | + modifier onlyGovDAO { |
| 30 | + // Start with an governance DAO address and will transfer to a governance DAO, e.g., Timelock + GovernorAlpha, after launch |
| 31 | + require(msg.sender == govDAO, "Only Governance DAO can call this function."); |
| 32 | + _; |
| 33 | + } |
| 34 | + |
| 35 | + event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp, uint256 cycDenomination, uint256 anonymityFee); |
| 36 | + event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 reward, uint256 relayerFee); |
| 37 | + event RewardPerBlockUpdated(uint256 oldValue, uint256 newValue); |
| 38 | + event AnonymityFeeUpdated(uint256 oldValue, uint256 newValue); |
| 39 | + |
| 40 | + /** |
| 41 | + @dev The constructor |
| 42 | + @param _verifier the address of SNARK verifier for this contract |
| 43 | + @param _merkleTreeHeight the height of deposits' Merkle Tree |
| 44 | + @param _govDAO governance DAO address |
| 45 | + */ |
| 46 | + constructor( |
| 47 | + address _govDAO, |
| 48 | + IERC20 _token, |
| 49 | + IMintableToken _cycToken, |
| 50 | + address _treasury, |
| 51 | + uint256 _initCYCDenomination, |
| 52 | + uint256 _coinDenomination, |
| 53 | + uint256 _tokenDenomination, |
| 54 | + uint256 _startBlock, |
| 55 | + IVerifier _verifier, |
| 56 | + uint32 _merkleTreeHeight |
| 57 | + ) MerkleTreeWithHistory(_merkleTreeHeight) public { |
| 58 | + require(address(_token) != address(_cycToken), "token cannot be identical to CYC token"); |
| 59 | + verifier = _verifier; |
| 60 | + treasury = _treasury; |
| 61 | + cycToken = _cycToken; |
| 62 | + token = _token; |
| 63 | + govDAO = _govDAO; |
| 64 | + if (_startBlock < block.number) { |
| 65 | + lastRewardBlock = block.number; |
| 66 | + } else { |
| 67 | + lastRewardBlock = _startBlock; |
| 68 | + } |
| 69 | + initCYCDenomination = _initCYCDenomination; |
| 70 | + coinDenomination = _coinDenomination; |
| 71 | + tokenDenomination = _tokenDenomination; |
| 72 | + numOfShares = 0; |
| 73 | + } |
| 74 | + |
| 75 | + function calcAccumulateCYC() internal view returns (uint256) { |
| 76 | + uint256 reward = block.number.sub(lastRewardBlock).mul(rewardPerBlock); |
| 77 | + uint256 remaining = cycToken.balanceOf(address(this)).sub(accumulateCYC); |
| 78 | + if (remaining < reward) { |
| 79 | + reward = remaining; |
| 80 | + } |
| 81 | + return accumulateCYC.add(reward); |
| 82 | + } |
| 83 | + |
| 84 | + function updateBlockReward() public { |
| 85 | + uint256 blockNumber = block.number; |
| 86 | + if (blockNumber <= lastRewardBlock) { |
| 87 | + return; |
| 88 | + } |
| 89 | + if (rewardPerBlock != 0) { |
| 90 | + accumulateCYC = calcAccumulateCYC(); |
| 91 | + } |
| 92 | + // always update lastRewardBlock no matter there is sufficient reward or not |
| 93 | + lastRewardBlock = blockNumber; |
| 94 | + } |
| 95 | + |
| 96 | + function cycDenomination() public view returns (uint256) { |
| 97 | + if (numOfShares == 0) { |
| 98 | + return initCYCDenomination; |
| 99 | + } |
| 100 | + uint256 blockNumber = block.number; |
| 101 | + uint256 accCYC = accumulateCYC; |
| 102 | + if (blockNumber > lastRewardBlock && rewardPerBlock > 0) { |
| 103 | + accCYC = calcAccumulateCYC(); |
| 104 | + } |
| 105 | + return accCYC.add(numOfShares - 1).div(numOfShares); |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + @dev Deposit funds into the contract. The caller must send (for Coin) or approve (for ERC20) value equal to or `denomination` of this instance. |
| 110 | + @param _commitment the note commitment, which is PedersenHash(nullifier + secret) |
| 111 | + */ |
| 112 | + function deposit(bytes32 _commitment) external payable nonReentrant { |
| 113 | + require(!commitments[_commitment], "The commitment has been submitted"); |
| 114 | + require(msg.value >= coinDenomination, "insufficient coin amount"); |
| 115 | + uint256 refund = msg.value - coinDenomination; |
| 116 | + uint32 insertedIndex = _insert(_commitment); |
| 117 | + commitments[_commitment] = true; |
| 118 | + updateBlockReward(); |
| 119 | + uint256 cycDeno = cycDenomination(); |
| 120 | + uint256 fee = anonymityFee; |
| 121 | + if (cycDeno.add(fee) > 0) { |
| 122 | + require(cycToken.transferFrom(msg.sender, address(this), cycDeno.add(fee)), "insufficient CYC allowance"); |
| 123 | + } |
| 124 | + if (fee > 0) { |
| 125 | + address t = treasury; |
| 126 | + if (t == address(0)) { |
| 127 | + require(cycToken.burn(fee), "failed to burn anonymity fee"); |
| 128 | + } else { |
| 129 | + require(safeTransfer(cycToken, t, fee), "failed to transfer anonymity fee"); |
| 130 | + } |
| 131 | + } |
| 132 | + uint256 td = tokenDenomination; |
| 133 | + if (td > 0) { |
| 134 | + require(token.transferFrom(msg.sender, address(this), td), "insufficient allowance"); |
| 135 | + } |
| 136 | + accumulateCYC += cycDeno; |
| 137 | + numOfShares += 1; |
| 138 | + if (refund > 0) { |
| 139 | + (bool success, ) = msg.sender.call.value(refund)(""); |
| 140 | + require(success, "failed to refund"); |
| 141 | + } |
| 142 | + emit Deposit(_commitment, insertedIndex, block.timestamp, cycDeno, fee); |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + @dev Withdraw a deposit from the contract. `proof` is a zkSNARK proof data, and input is an array of circuit public inputs |
| 147 | + `input` array consists of: |
| 148 | + - merkle root of all deposits in the contract |
| 149 | + - hash of unique deposit nullifier to prevent double spends |
| 150 | + - the recipient of funds |
| 151 | + - optional fee that goes to the transaction sender (usually a relay) |
| 152 | + */ |
| 153 | + function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _relayerFee, uint256 _refund) external payable nonReentrant { |
| 154 | + require(_refund == 0, "refund is not zero"); |
| 155 | + require(!Address.isContract(_recipient), "recipient of cannot be contract"); |
| 156 | + require(!nullifierHashes[_nullifierHash], "The note has been already spent"); |
| 157 | + require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one |
| 158 | + require(verifier.verifyProof(_proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _relayerFee, _refund]), "Invalid withdraw proof"); |
| 159 | + |
| 160 | + nullifierHashes[_nullifierHash] = true; |
| 161 | + uint256 td = tokenDenomination; |
| 162 | + if (td > 0) { |
| 163 | + require(safeTransfer(token, _recipient, td), "failed to withdraw token"); |
| 164 | + } |
| 165 | + updateBlockReward(); |
| 166 | + uint256 relayerFee = 0; |
| 167 | + // numOfShares should be larger than 0 |
| 168 | + uint256 cycDeno = accumulateCYC.div(numOfShares); |
| 169 | + if (cycDeno > 0) { |
| 170 | + accumulateCYC -= cycDeno; |
| 171 | + require(safeTransfer(cycToken, _recipient, cycDeno), "failed to reward CYC"); |
| 172 | + } |
| 173 | + uint256 cd = coinDenomination; |
| 174 | + if (_relayerFee > cd) { |
| 175 | + _relayerFee = cd; |
| 176 | + } |
| 177 | + if (_relayerFee > 0) { |
| 178 | + (bool success,) = _relayer.call.value(_relayerFee)(""); |
| 179 | + require(success, "failed to send relayer fee"); |
| 180 | + cd -= _relayerFee; |
| 181 | + } |
| 182 | + if (cd > 0) { |
| 183 | + (bool success,) = _recipient.call.value(cd)(""); |
| 184 | + require(success, "failed to withdraw coin"); |
| 185 | + } |
| 186 | + numOfShares -= 1; |
| 187 | + emit Withdrawal(_recipient, _nullifierHash, _relayer, cycDeno, relayerFee); |
| 188 | + } |
| 189 | + |
| 190 | + /** @dev whether a note is already spent */ |
| 191 | + function isSpent(bytes32 _nullifierHash) public view returns(bool) { |
| 192 | + return nullifierHashes[_nullifierHash]; |
| 193 | + } |
| 194 | + |
| 195 | + /** @dev whether an array of notes is already spent */ |
| 196 | + function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns(bool[] memory spent) { |
| 197 | + spent = new bool[](_nullifierHashes.length); |
| 198 | + for(uint i = 0; i < _nullifierHashes.length; i++) { |
| 199 | + if (isSpent(_nullifierHashes[i])) { |
| 200 | + spent[i] = true; |
| 201 | + } |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + /** |
| 206 | + @dev allow governance DAO to update SNARK verification keys. This is needed to |
| 207 | + update keys if tornado.cash update their keys in production. |
| 208 | + */ |
| 209 | + function updateVerifier(address _newVerifier) external onlyGovDAO { |
| 210 | + verifier = IVerifier(_newVerifier); |
| 211 | + } |
| 212 | + |
| 213 | + /** @dev governance DAO can change his address */ |
| 214 | + function changeGovDAO(address _newGovDAO) external onlyGovDAO { |
| 215 | + govDAO = _newGovDAO; |
| 216 | + } |
| 217 | + |
| 218 | + function setRewardPerBlock(uint256 _rewardPerBlock) public onlyGovDAO { |
| 219 | + updateBlockReward(); |
| 220 | + emit RewardPerBlockUpdated(rewardPerBlock, _rewardPerBlock); |
| 221 | + rewardPerBlock = _rewardPerBlock; |
| 222 | + } |
| 223 | + |
| 224 | + function setAnonymityFee(uint256 _fee) public onlyGovDAO { |
| 225 | + emit AnonymityFeeUpdated(anonymityFee, _fee); |
| 226 | + anonymityFee = _fee; |
| 227 | + } |
| 228 | + |
| 229 | + // Safe transfer function, just in case if rounding error causes pool to not have enough CYCs. |
| 230 | + function safeTransfer(IERC20 _token, address _to, uint256 _amount) internal returns (bool) { |
| 231 | + uint256 balance = _token.balanceOf(address(this)); |
| 232 | + if (_amount > balance) { |
| 233 | + return _token.transfer(_to, balance); |
| 234 | + } |
| 235 | + return _token.transfer(_to, _amount); |
| 236 | + } |
| 237 | + |
| 238 | + function version() public pure returns(string memory) { |
| 239 | + return "2.2"; |
| 240 | + } |
| 241 | + |
| 242 | +} |
0 commit comments