commit ca9c848f6a0ea65b932580ff9c4be02b49c73ab4 Author: gozzy Date: Fri Sep 2 17:59:39 2022 +0000 initialise diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb98c92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Truffle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3a45a7 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Tornado.cash token (TORN) [![Build Status](https://github.com/tornadocash/torn-token/workflows/build/badge.svg)](https://github.com/tornadocash/torn-token/actions) + +## Dependencies + +1. node 12 +2. yarn + +## Start + +```bash +$ yarn +$ cp .env.example .env +$ yarn test +``` + +## Deploying + +Deploy to Kovan: + +```bash +$ yarn deploy:kovan +``` + +## Mainnet deploy instructions + +1. in torn-token repo `cp .env.example .env` +2. Specify deployment `PRIVATE_KEY` in the `.env`. It will be the owner during deployment. +3. Specify gas price in `truffle.js` +4. `yarn deploy:mainnet` + +5. go to [mining](https://github.com/tornadocash/tornado-anonymity-mining) repo +6. `cp .env.example .env` +7. Specify private key and TORN address +8. yarn `yarn deploy:mainnet` + +9. go to [governance](https://github.com/tornadocash/governance) repo +10. ... +11. ... +12. ... + +13. in this repo modify the `config.js` file. Put all addresses that you got during deployment +14. set `ONLY_INITIALIZE` to `true` and `TORN_TO_INITIALIZE` to the token address +15. `yarn deploy:mainnet` diff --git a/config.js b/config.js new file mode 100644 index 0000000..31f2b3b --- /dev/null +++ b/config.js @@ -0,0 +1,226 @@ +const { toWei } = require('web3-utils') + +module.exports = { + torn: { + address: 'torn.contract.tornadocash.eth', + cap: toWei('10000000'), + pausePeriod: 45 * 24 * 3600, // 45 days + distribution: { + airdrop: { to: 'voucher', amount: toWei('500000') }, + miningV2: { to: 'rewardSwap', amount: toWei('1000000') }, + governance: { to: 'vesting.governance', amount: toWei('5500000') }, + team1: { to: 'vesting.team1', amount: toWei('822407') }, + team2: { to: 'vesting.team2', amount: toWei('822407') }, + team3: { to: 'vesting.team3', amount: toWei('822407') }, + team4: { to: 'vesting.team4', amount: toWei('500000') }, + team5: { to: 'vesting.team5', amount: toWei('32779') }, + }, + }, + governance: { address: 'governance.contract.tornadocash.eth' }, + governanceImpl: { address: 'governance-impl.contract.tornadocash.eth' }, + voucher: { address: 'voucher.contract.tornadocash.eth', duration: 12 }, + miningV2: { + address: 'mining-v2.contract.tornadocash.eth', + initialBalance: toWei('25000'), + rates: [ + { instance: 'eth-01.tornadocash.eth', value: '10' }, + { instance: 'eth-1.tornadocash.eth', value: '20' }, + { instance: 'eth-10.tornadocash.eth', value: '50' }, + { instance: 'eth-100.tornadocash.eth', value: '400' }, + ], + }, + rewardSwap: { address: 'reward-swap.contract.tornadocash.eth', poolWeight: 1e11 }, + tornadoTrees: { address: 'tornado-trees.contract.tornadocash.eth', levels: 20 }, + tornadoProxy: { address: 'tornado-proxy.contract.tornadocash.eth' }, + rewardVerifier: { address: 'reward-verifier.contract.tornadocash.eth' }, + treeUpdateVerifier: { address: 'tree-update-verifier.contract.tornadocash.eth' }, + withdrawVerifier: { address: 'withdraw-verifier.contract.tornadocash.eth' }, + poseidonHasher2: { address: 'poseidon2.contract.tornadocash.eth' }, + poseidonHasher3: { address: 'poseidon3.contract.tornadocash.eth' }, + deployer: { address: 'deployer.contract.tornadocash.eth' }, + vesting: { + team1: { + address: 'team1.vesting.contract.tornadocash.eth', + beneficiary: '0x5A7a51bFb49F190e5A6060a5bc6052Ac14a3b59f', + cliff: 12, + duration: 36, + }, + team2: { + address: 'team2.vesting.contract.tornadocash.eth', + beneficiary: '0xF50D442e48E11F16e105431a2664141f44F9feD8', + cliff: 12, + duration: 36, + }, + team3: { + address: 'team3.vesting.contract.tornadocash.eth', + beneficiary: '0x6D2C515Ff6A40554869C3Da05494b8D6910D075E', + cliff: 12, + duration: 36, + }, + team4: { + address: 'team4.vesting.contract.tornadocash.eth', + beneficiary: '0x504a9c37794a2341F4861bF0A44E8d4016DF8cF2', + cliff: 12, + duration: 36, + }, + team5: { + address: 'team5.vesting.contract.tornadocash.eth', + beneficiary: '0x2D81713c58452c92C19b2917e1C770eEcF53Fe41', + cliff: 12, + duration: 36, + }, + governance: { + address: 'governance.vesting.contract.tornadocash.eth', + cliff: 3, + duration: 60, + }, + }, + instances: { + netId1: { + eth: { + instanceAddress: { + 0.1: '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc', + 1: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936', + 10: '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF', + 100: '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291', + }, + symbol: 'ETH', + decimals: 18, + }, + dai: { + instanceAddress: { + 100: '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3', + 1000: '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144', + 10000: '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A', + 100000: undefined, + }, + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + }, + cdai: { + instanceAddress: { + 5000: '0x22aaA7720ddd5388A3c0A3333430953C68f1849b', + 50000: '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659', + 500000: '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00', + 5000000: undefined, + }, + tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', + symbol: 'cDAI', + decimals: 8, + }, + usdc: { + instanceAddress: { + 100: '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307', + 1000: '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D', + 10000: '0xD691F27f38B395864Ea86CfC7253969B409c362d', + 100000: undefined, + }, + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + }, + cusdc: { + instanceAddress: { + 5000: '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6', + 50000: '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a', + 500000: '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D', + 5000000: undefined, + }, + tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563', + symbol: 'cUSDC', + decimals: 8, + }, + usdt: { + instanceAddress: { + 100: '0x169AD27A470D064DEDE56a2D3ff727986b15D52B', + 1000: '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f', + 10000: '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a', + 100000: '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E', + }, + tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + symbol: 'USDT', + decimals: 6, + }, + }, + netId42: { + eth: { + instanceAddress: { + 0.1: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f', + 1: '0xD6a6AC46d02253c938B96D12BE439F570227aE8E', + 10: '0xe1BE96331391E519471100c3c1528B66B8F4e5a7', + 100: '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D', + }, + symbol: 'ETH', + decimals: 18, + }, + dai: { + instanceAddress: { + 100: '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D', + 1000: '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD', + 10000: '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3', + 100000: undefined, + }, + tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', + symbol: 'DAI', + decimals: 18, + }, + cdai: { + instanceAddress: { + 5000: '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8', + 50000: '0x7182EA067e0f050997444FCb065985Fd677C16b6', + 500000: '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae', + 5000000: undefined, + }, + tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c', + symbol: 'cDAI', + decimals: 8, + }, + usdc: { + instanceAddress: { + 100: '0x137E2B6d185018e7f09f6cf175a970e7fC73826C', + 1000: '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af', + 10000: '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF', + 100000: undefined, + }, + tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF', + symbol: 'USDC', + decimals: 6, + }, + cusdc: { + instanceAddress: { + 5000: '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0', + 50000: '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D', + 500000: '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1', + 5000000: undefined, + }, + tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35', + symbol: 'cUSDC', + decimals: 8, + }, + usdt: { + instanceAddress: { + 100: '0x327853Da7916a6A0935563FB1919A48843036b42', + 1000: '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0', + 10000: '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD', + 100000: '0x14aEd24B67EaF3FF28503eB92aeb217C47514364', + }, + tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1', + symbol: 'USDT', + decimals: 6, + }, + }, + netId5: { + eth: { + instanceAddress: { + 0.1: '0x6Bf694a291DF3FeC1f7e69701E3ab6c592435Ae7', + 1: '0x3aac1cC67c2ec5Db4eA850957b967Ba153aD6279', + 10: '0x723B78e67497E85279CB204544566F4dC5d2acA0', + 100: '0x0E3A09dDA6B20aFbB34aC7cD4A6881493f3E7bf7', + }, + symbol: 'ETH', + decimals: 18, + }, + }, + }, +} diff --git a/contracts/Airdrop.sol b/contracts/Airdrop.sol new file mode 100644 index 0000000..e1ce16a --- /dev/null +++ b/contracts/Airdrop.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./ENS.sol"; + +contract Airdrop is EnsResolve { + struct Recipient { + address to; + uint256 amount; + } + + constructor(bytes32 tokenAddress, Recipient[] memory targets) public { + IERC20 token = IERC20(resolve(tokenAddress)); + require(token.balanceOf(address(this)) > 0, "Balance is 0, airdrop already done"); + for (uint256 i = 0; i < targets.length; i++) { + token.transfer(targets[i].to, targets[i].amount); + } + selfdestruct(address(0)); + } +} diff --git a/contracts/ECDSA.sol b/contracts/ECDSA.sol new file mode 100644 index 0000000..6c17311 --- /dev/null +++ b/contracts/ECDSA.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +// A copy from https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237/files + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + // Check the signature length + if (signature.length != 65) { + revert("ECDSA: invalid signature length"); + } + + // Divide the signature in r, s and v variables + bytes32 r; + bytes32 s; + uint8 v; + + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solhint-disable-next-line no-inline-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := mload(add(signature, 0x41)) + } + + return recover(hash, v, r, s); + } + + /** + * @dev Overload of {ECDSA-recover-bytes32-bytes-} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "ECDSA: invalid signature 's' value"); + require(v == 27 || v == 28, "ECDSA: invalid signature 'v' value"); + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + require(signer != address(0), "ECDSA: invalid signature"); + + return signer; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * replicates the behavior of the + * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] + * JSON-RPC method. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } +} diff --git a/contracts/ENS.sol b/contracts/ENS.sol new file mode 100644 index 0000000..17da901 --- /dev/null +++ b/contracts/ENS.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +interface ENS { + function resolver(bytes32 node) external view returns (Resolver); +} + +interface Resolver { + function addr(bytes32 node) external view returns (address); +} + +contract EnsResolve { + function resolve(bytes32 node) public view virtual returns (address) { + ENS Registry = ENS( + getChainId() == 1 ? 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e : 0x8595bFb0D940DfEDC98943FA8a907091203f25EE + ); + return Registry.resolver(node).addr(node); + } + + function bulkResolve(bytes32[] memory domains) public view returns (address[] memory result) { + result = new address[](domains.length); + for (uint256 i = 0; i < domains.length; i++) { + result[i] = resolve(domains[i]); + } + } + + function getChainId() internal pure returns (uint256) { + uint256 chainId; + assembly { + chainId := chainid() + } + return chainId; + } +} diff --git a/contracts/ERC20Permit.sol b/contracts/ERC20Permit.sol new file mode 100644 index 0000000..9f71bc6 --- /dev/null +++ b/contracts/ERC20Permit.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +// Adapted copy from https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237/files + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "./ECDSA.sol"; + +/** + * @dev Extension of {ERC20} that allows token holders to use their tokens + * without sending any transactions by setting {IERC20-allowance} with a + * signature using the {permit} method, and then spend them via + * {IERC20-transferFrom}. + * + * The {permit} signature mechanism conforms to the {IERC2612Permit} interface. + */ +abstract contract ERC20Permit is ERC20 { + mapping(address => uint256) private _nonces; + + bytes32 private constant _PERMIT_TYPEHASH = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // Mapping of ChainID to domain separators. This is a very gas efficient way + // to not recalculate the domain separator on every call, while still + // automatically detecting ChainID changes. + mapping(uint256 => bytes32) private _domainSeparators; + + constructor() internal { + _updateDomainSeparator(); + } + + /** + * @dev See {IERC2612Permit-permit}. + * + * If https://eips.ethereum.org/EIPS/eip-1344[ChainID] ever changes, the + * EIP712 Domain Separator is automatically recalculated. + */ + function permit( + address owner, + address spender, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + require(blockTimestamp() <= deadline, "ERC20Permit: expired deadline"); + + bytes32 hashStruct = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, amount, _nonces[owner], deadline)); + + bytes32 hash = keccak256(abi.encodePacked(uint16(0x1901), _domainSeparator(), hashStruct)); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _nonces[owner]++; + _approve(owner, spender, amount); + } + + /** + * @dev See {IERC2612Permit-nonces}. + */ + function nonces(address owner) public view returns (uint256) { + return _nonces[owner]; + } + + function _updateDomainSeparator() private returns (bytes32) { + uint256 _chainID = chainID(); + + bytes32 newDomainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name())), + keccak256(bytes("1")), // Version + _chainID, + address(this) + ) + ); + + _domainSeparators[_chainID] = newDomainSeparator; + + return newDomainSeparator; + } + + // Returns the domain separator, updating it if chainID changes + function _domainSeparator() private returns (bytes32) { + bytes32 domainSeparator = _domainSeparators[chainID()]; + if (domainSeparator != 0x00) { + return domainSeparator; + } else { + return _updateDomainSeparator(); + } + } + + function chainID() public view virtual returns (uint256 _chainID) { + assembly { + _chainID := chainid() + } + } + + function blockTimestamp() public view virtual returns (uint256) { + return block.timestamp; + } +} diff --git a/contracts/TORN.sol b/contracts/TORN.sol new file mode 100644 index 0000000..9dbac80 --- /dev/null +++ b/contracts/TORN.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "./ERC20Permit.sol"; +import "./ENS.sol"; + +contract TORN is ERC20("TornadoCash", "TORN"), ERC20Burnable, ERC20Permit, Pausable, EnsResolve { + using SafeERC20 for IERC20; + + uint256 public immutable canUnpauseAfter; + address public immutable governance; + mapping(address => bool) public allowedTransferee; + + event Allowed(address target); + event Disallowed(address target); + + struct Recipient { + bytes32 to; + uint256 amount; + } + + constructor( + bytes32 _governance, + uint256 _pausePeriod, + Recipient[] memory _vestings + ) public { + address _resolvedGovernance = resolve(_governance); + governance = _resolvedGovernance; + allowedTransferee[_resolvedGovernance] = true; + + for (uint256 i = 0; i < _vestings.length; i++) { + address to = resolve(_vestings[i].to); + _mint(to, _vestings[i].amount); + allowedTransferee[to] = true; + } + + canUnpauseAfter = blockTimestamp().add(_pausePeriod); + _pause(); + require(totalSupply() == 10000000 ether, "TORN: incorrect distribution"); + } + + modifier onlyGovernance() { + require(_msgSender() == governance, "TORN: only governance can perform this action"); + _; + } + + function changeTransferability(bool decision) public onlyGovernance { + require(blockTimestamp() > canUnpauseAfter, "TORN: cannot change transferability yet"); + if (decision) { + _unpause(); + } else { + _pause(); + } + } + + function addToAllowedList(address[] memory target) public onlyGovernance { + for (uint256 i = 0; i < target.length; i++) { + allowedTransferee[target[i]] = true; + emit Allowed(target[i]); + } + } + + function removeFromAllowedList(address[] memory target) public onlyGovernance { + for (uint256 i = 0; i < target.length; i++) { + allowedTransferee[target[i]] = false; + emit Disallowed(target[i]); + } + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override { + super._beforeTokenTransfer(from, to, amount); + require(!paused() || allowedTransferee[from] || allowedTransferee[to], "TORN: paused"); + require(to != address(this), "TORN: invalid recipient"); + } + + /// @dev Method to claim junk and accidentally sent tokens + function rescueTokens( + IERC20 _token, + address payable _to, + uint256 _balance + ) external onlyGovernance { + require(_to != address(0), "TORN: can not send to zero address"); + + if (_token == IERC20(0)) { + // for Ether + uint256 totalBalance = address(this).balance; + uint256 balance = _balance == 0 ? totalBalance : Math.min(totalBalance, _balance); + _to.transfer(balance); + } else { + // any other erc20 + uint256 totalBalance = _token.balanceOf(address(this)); + uint256 balance = _balance == 0 ? totalBalance : Math.min(totalBalance, _balance); + require(balance > 0, "TORN: trying to send 0 balance"); + _token.safeTransfer(_to, balance); + } + } +} diff --git a/contracts/Vesting.sol b/contracts/Vesting.sol new file mode 100644 index 0000000..d52954c --- /dev/null +++ b/contracts/Vesting.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "./ENS.sol"; + +/** + * @title Vesting + * @dev A token holder contract that can release its token balance gradually like a + * typical vesting scheme, with a cliff and vesting period. Optionally revocable by the + * owner. + */ +contract Vesting is EnsResolve { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + uint256 public constant SECONDS_PER_MONTH = 30 days; + + event Released(uint256 amount); + + // beneficiary of tokens after they are released + address public immutable beneficiary; + IERC20 public immutable token; + + uint256 public immutable cliffInMonths; + uint256 public immutable startTimestamp; + uint256 public immutable durationInMonths; + uint256 public released; + + /** + * @dev Creates a vesting contract that vests its balance of any ERC20 token to the + * _beneficiary, monthly in a linear fashion until duration has passed. By then all + * of the balance will have vested. + * @param _beneficiary address of the beneficiary to whom vested tokens are transferred + * @param _cliffInMonths duration in months of the cliff in which tokens will begin to vest + * @param _durationInMonths duration in months of the period in which the tokens will vest + */ + constructor( + bytes32 _token, + address _beneficiary, + uint256 _startTimestamp, + uint256 _cliffInMonths, + uint256 _durationInMonths + ) public { + require(_beneficiary != address(0), "Beneficiary cannot be empty"); + require(_cliffInMonths <= _durationInMonths, "Cliff is greater than duration"); + + token = IERC20(resolve(_token)); + beneficiary = _beneficiary; + durationInMonths = _durationInMonths; + cliffInMonths = _cliffInMonths; + startTimestamp = _startTimestamp == 0 ? blockTimestamp() : _startTimestamp; + } + + /** + * @notice Transfers vested tokens to beneficiary. + */ + function release() external { + uint256 vested = vestedAmount(); + require(vested > 0, "No tokens to release"); + + released = released.add(vested); + token.safeTransfer(beneficiary, vested); + + emit Released(vested); + } + + /** + * @dev Calculates the amount that has already vested but hasn't been released yet. + */ + function vestedAmount() public view returns (uint256) { + if (blockTimestamp() < startTimestamp) { + return 0; + } + + uint256 elapsedTime = blockTimestamp().sub(startTimestamp); + uint256 elapsedMonths = elapsedTime.div(SECONDS_PER_MONTH); + + if (elapsedMonths < cliffInMonths) { + return 0; + } + + // If over vesting duration, all tokens vested + if (elapsedMonths >= durationInMonths) { + return token.balanceOf(address(this)); + } else { + uint256 currentBalance = token.balanceOf(address(this)); + uint256 totalBalance = currentBalance.add(released); + + uint256 vested = totalBalance.mul(elapsedMonths).div(durationInMonths); + uint256 unreleased = vested.sub(released); + + // currentBalance can be 0 in case of vesting being revoked earlier. + return Math.min(currentBalance, unreleased); + } + } + + function blockTimestamp() public view virtual returns (uint256) { + return block.timestamp; + } +} diff --git a/contracts/Voucher.sol b/contracts/Voucher.sol new file mode 100644 index 0000000..0b584a1 --- /dev/null +++ b/contracts/Voucher.sol @@ -0,0 +1,66 @@ +/** + * This is tornado.cash airdrop for early adopters. In order to claim your TORN token please follow https://tornado.cash/airdrop + */ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "./ENS.sol"; + +contract Voucher is ERC20("TornadoCash voucher for early adopters", "vTORN"), EnsResolve { + using SafeERC20 for IERC20; + + IERC20 public immutable torn; + uint256 public immutable expiresAt; + address public immutable governance; + mapping(address => bool) public allowedTransferee; + + struct Recipient { + address to; + uint256 amount; + } + + constructor( + bytes32 _torn, + bytes32 _governance, + uint256 _duration, + Recipient[] memory _airdrops + ) public { + torn = IERC20(resolve(_torn)); + governance = resolve(_governance); + expiresAt = blockTimestamp().add(_duration); + for (uint256 i = 0; i < _airdrops.length; i++) { + _mint(_airdrops[i].to, _airdrops[i].amount); + allowedTransferee[_airdrops[i].to] = true; + } + } + + function redeem() external { + require(blockTimestamp() < expiresAt, "Airdrop redeem period has ended"); + uint256 amount = balanceOf(msg.sender); + _burn(msg.sender, amount); + torn.safeTransfer(msg.sender, amount); + } + + function rescueExpiredTokens() external { + require(blockTimestamp() >= expiresAt, "Airdrop redeem period has not ended yet"); + torn.safeTransfer(governance, torn.balanceOf(address(this))); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override { + super._beforeTokenTransfer(from, to, amount); + require(to == address(0) || from == address(0) || allowedTransferee[from], "ERC20: transfer is not allowed"); + } + + function blockTimestamp() public view virtual returns (uint256) { + return block.timestamp; + } +} diff --git a/contracts/mocks/AirdropMock.sol b/contracts/mocks/AirdropMock.sol new file mode 100644 index 0000000..bb3079d --- /dev/null +++ b/contracts/mocks/AirdropMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "../Airdrop.sol"; + +contract AirdropMock is Airdrop { + constructor(bytes32 tokenAddress, Recipient[] memory targets) public Airdrop(tokenAddress, targets) {} + + function resolve(bytes32 addr) public view override returns (address) { + return address(uint160(uint256(addr) >> (12 * 8))); + } +} diff --git a/contracts/mocks/ENSMock.sol b/contracts/mocks/ENSMock.sol new file mode 100644 index 0000000..34f67e8 --- /dev/null +++ b/contracts/mocks/ENSMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +contract ENSMock { + mapping(bytes32 => address) public registry; + + function resolver( + bytes32 /* _node */ + ) external view returns (address) { + return address(this); + } + + function addr(bytes32 _node) external view returns (address) { + return registry[_node]; + } + + function setAddr(bytes32 _node, address _addr) external { + registry[_node] = _addr; + } + + function multicall(bytes[] calldata data) external returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + require(success); + results[i] = result; + } + return results; + } +} diff --git a/contracts/mocks/TORNMock.sol b/contracts/mocks/TORNMock.sol new file mode 100644 index 0000000..e030589 --- /dev/null +++ b/contracts/mocks/TORNMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "../TORN.sol"; +import "./Timestamp.sol"; + +contract TORNMock is TORN, Timestamp { + uint256 public chainId; + + constructor( + bytes32 _governance, + uint256 _pausePeriod, + Recipient[] memory _vesting + ) public TORN(_governance, _pausePeriod, _vesting) {} + + function resolve(bytes32 addr) public view override returns (address) { + return address(uint160(uint256(addr) >> (12 * 8))); + } + + function setChainId(uint256 _chainId) public { + chainId = _chainId; + } + + function chainID() public view override returns (uint256) { + return chainId; + } + + function blockTimestamp() public view override(Timestamp, ERC20Permit) returns (uint256) { + return Timestamp.blockTimestamp(); + } +} diff --git a/contracts/mocks/Timestamp.sol b/contracts/mocks/Timestamp.sol new file mode 100644 index 0000000..9172a64 --- /dev/null +++ b/contracts/mocks/Timestamp.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +contract Timestamp { + uint256 public fakeTimestamp; + + function setFakeTimestamp(uint256 _fakeTimestamp) public { + fakeTimestamp = _fakeTimestamp; + } + + function blockTimestamp() public view virtual returns (uint256) { + return fakeTimestamp == 0 ? block.timestamp : fakeTimestamp; + } +} diff --git a/contracts/mocks/VestingMock.sol b/contracts/mocks/VestingMock.sol new file mode 100644 index 0000000..ce617a6 --- /dev/null +++ b/contracts/mocks/VestingMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "../Vesting.sol"; +import "./Timestamp.sol"; + +contract VestingMock is Vesting, Timestamp { + constructor( + bytes32 _token, + address _beneficiary, + uint256 _startTimestamp, + uint256 _cliffInMonths, + uint256 _durationInMonths + ) public Vesting(_token, _beneficiary, _startTimestamp, _cliffInMonths, _durationInMonths) {} + + function resolve(bytes32 addr) public view override returns (address) { + return address(uint160(uint256(addr) >> (12 * 8))); + } + + function blockTimestamp() public view override(Timestamp, Vesting) returns (uint256) { + return Timestamp.blockTimestamp(); + } +} diff --git a/contracts/mocks/VoucherMock.sol b/contracts/mocks/VoucherMock.sol new file mode 100644 index 0000000..ce494aa --- /dev/null +++ b/contracts/mocks/VoucherMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "../Voucher.sol"; +import "./Timestamp.sol"; + +contract VoucherMock is Voucher, Timestamp { + constructor( + bytes32 _torn, + bytes32 _governance, + uint256 _duration, + Recipient[] memory _airdrops + ) public Voucher(_torn, _governance, _duration, _airdrops) {} + + function resolve(bytes32 addr) public view override returns (address) { + return address(uint160(uint256(addr) >> (12 * 8))); + } + + function blockTimestamp() public view override(Timestamp, Voucher) returns (uint256) { + return Timestamp.blockTimestamp(); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..13ca180 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "torn-token", + "version": "1.0.0", + "main": "config.js", + "repository": "https://github.com/tornadocash/torn-token.git", + "author": "Tornadocash team ", + "license": "MIT", + "files": [ + "config.js", + "contracts/*" + ], + "scripts": { + "compile": "truffle compile", + "coverage": "yarn compile && truffle run coverage", + "test": "truffle test", + "test:stacktrace": "yarn test --stacktrace", + "eslint": "eslint --ext .js --ignore-path .gitignore .", + "prettier:check": "prettier --check . --config .prettierrc", + "prettier:fix": "prettier --write . --config .prettierrc", + "lint": "yarn eslint && yarn prettier:check", + "deploy:mainnet": "truffle migrate --network mainnet", + "deploy:kovan": "truffle migrate --network kovan", + "deploy:dev": "truffle migrate --skip-dry-run --network test", + "init:mainnet": "truffle migrate -f 2 --to 2 --network mainnet", + "init:kovan": "truffle migrate -f 2 --to 2 --network kovan", + "init:test": "truffle migrate -f 2 --to 2 --network test", + "voucher:mainnet": "truffle migrate -f 3 --network mainnet", + "voucher:kovan": "truffle migrate -f 3 --network kovan", + "voucher:dev": "truffle migrate -f 3 --skip-dry-run --network test", + "verify": "truffle run verify --network $NETWORK" + }, + "devDependencies": { + "@ticket721/e712": "^0.4.1", + "babel-eslint": "^10.1.0", + "bn-chai": "^1.0.1", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "eslint": "^7.5.0", + "prettier": "^2.1.2", + "prettier-plugin-solidity": "^1.0.0-alpha.59", + "rlp": "^2.2.6", + "solhint-plugin-prettier": "^0.0.4", + "solidity-coverage": "^0.7.7", + "truffle": "^5.1.29", + "truffle-flattener": "^1.4.4", + "truffle-hdwallet-provider": "^1.0.17", + "truffle-plugin-verify": "^0.3.11" + }, + "dependencies": { + "@openzeppelin/contracts": "^3.1.0", + "dotenv": "^8.2.0", + "eth-sig-util": "^2.5.3", + "ethereumjs-util": "^7.0.3", + "web3": "^1.2.11" + } +}