├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── LICENSE ├── README.md ├── config.js ├── contracts ├── Airdrop.sol ├── ECDSA.sol ├── ENS.sol ├── ERC20Permit.sol ├── TORN.sol ├── Vesting.sol ├── Voucher.sol └── mocks │ ├── AirdropMock.sol │ ├── ENSMock.sol │ ├── TORNMock.sol │ ├── Timestamp.sol │ ├── VestingMock.sol │ └── VoucherMock.sol ├── index.d.ts ├── lib └── Permit.js ├── package.json ├── scripts └── ganacheHelper.js ├── test ├── airdrop.test.js ├── torn.test.js ├── vesting.test.js └── voucher.test.js ├── truffle.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parser": "babel-eslint", 14 | "parserOptions": { 15 | "ecmaVersion": 2018 16 | }, 17 | "rules": { 18 | "indent": ["error", 2], 19 | "linebreak-style": ["error", "unix"], 20 | "quotes": ["error", "single"], 21 | "semi": ["error", "never"], 22 | "object-curly-spacing": ["error", "always"], 23 | "comma-dangle": ["error", "always-multiline"], 24 | "require-await": "error" 25 | }, 26 | "ignorePatterns": ["*.d.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | tags: ['v[0-9]+.[0-9]+.[0-9]+'] 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: yarn install 19 | - run: yarn test 20 | - run: yarn lint 21 | - name: Telegram Failure Notification 22 | uses: appleboy/telegram-action@0.0.7 23 | if: failure() 24 | with: 25 | message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }} 26 | format: markdown 27 | to: ${{ secrets.TELEGRAM_CHAT_ID }} 28 | token: ${{ secrets.TELEGRAM_BOT_TOKEN }} 29 | 30 | publish: 31 | runs-on: ubuntu-latest 32 | needs: build 33 | if: startsWith(github.ref, 'refs/tags') 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | 38 | - name: NPM login 39 | # NPM doesn't understand env vars and needs auth file lol 40 | run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | 44 | - name: Set vars 45 | id: vars 46 | run: | 47 | echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/v})" 48 | echo "::set-output name=repo_name::$(echo ${GITHUB_REPOSITORY#*/})" 49 | 50 | - name: Check package.json version vs tag 51 | run: | 52 | [ ${{ steps.vars.outputs.version }} = $(grep '"version":' package.json | grep -o "[0-9.]*") ] || (echo "Git tag doesn't match version in package.json" && false) 53 | 54 | - name: Publish to npm 55 | run: npm publish 56 | 57 | - name: Create GitHub Release Draft 58 | uses: actions/create-release@v1 59 | with: 60 | tag_name: ${{ github.ref }} 61 | release_name: Release ${{ steps.vars.outputs.version }} 62 | draft: true 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | - name: Telegram Notification 67 | uses: appleboy/telegram-action@0.0.7 68 | with: 69 | message: 🚀 Published a [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) version [${{ steps.vars.outputs.version }}](https://hub.docker.com/repository/docker/${{ github.repository }}) to docker hub 70 | format: markdown 71 | to: ${{ secrets.TELEGRAM_CHAT_ID }} 72 | token: ${{ secrets.TELEGRAM_BOT_TOKEN }} 73 | 74 | - name: Telegram Failure Notification 75 | uses: appleboy/telegram-action@0.0.7 76 | if: failure() 77 | with: 78 | message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }} 79 | format: markdown 80 | to: ${{ secrets.TELEGRAM_CHAT_ID }} 81 | token: ${{ secrets.TELEGRAM_BOT_TOKEN }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | coverage.json 4 | .DS_Store 5 | build 6 | .env 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | scripts 4 | contracts/ECDSA.sol 5 | *.d.ts 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "semi": false, 6 | "printWidth": 110, 7 | "overrides": [ 8 | { 9 | "files": "*.sol", 10 | "options": { 11 | "singleQuote": false, 12 | "printWidth": 130 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "prettier/prettier": [ 5 | "error", 6 | { 7 | "printWidth": 110 8 | } 9 | ], 10 | "quotes": ["error", "double"] 11 | }, 12 | "plugins": ["prettier"] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Truffle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tornado.cash token (TORN) [![Build Status](https://github.com/tornadocash/torn-token/workflows/build/badge.svg)](https://github.com/tornadocash/torn-token/actions) [![npm](https://img.shields.io/npm/v/torn-token)](https://www.npmjs.com/package/torn-token) 2 | 3 | ## Requirements 4 | 5 | 1. node 12 6 | 2. yarn 7 | 8 | ## Start 9 | 10 | ```bash 11 | $ yarn 12 | $ yarn test 13 | ``` 14 | 15 | ## Licence 16 | 17 | MIT 18 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const { toWei } = require('web3-utils') 2 | 3 | module.exports = { 4 | torn: { 5 | address: 'torn.contract.tornadocash.eth', 6 | cap: toWei('10000000'), 7 | pausePeriod: 45 * 24 * 3600, // 45 days 8 | distribution: { 9 | airdrop: { to: 'voucher', amount: toWei('500000') }, 10 | miningV2: { to: 'rewardSwap', amount: toWei('1000000') }, 11 | governance: { to: 'vesting.governance', amount: toWei('5500000') }, 12 | team1: { to: 'vesting.team1', amount: toWei('822407') }, 13 | team2: { to: 'vesting.team2', amount: toWei('822407') }, 14 | team3: { to: 'vesting.team3', amount: toWei('822407') }, 15 | team4: { to: 'vesting.team4', amount: toWei('500000') }, 16 | team5: { to: 'vesting.team5', amount: toWei('32779') }, 17 | }, 18 | }, 19 | governance: { address: 'governance.contract.tornadocash.eth' }, 20 | governanceImpl: { address: 'governance-impl.contract.tornadocash.eth' }, 21 | voucher: { address: 'voucher.contract.tornadocash.eth', duration: 12 }, 22 | miningV2: { 23 | address: 'mining-v2.contract.tornadocash.eth', 24 | initialBalance: toWei('25000'), 25 | rates: [ 26 | { instance: 'eth-01.tornadocash.eth', value: '10' }, 27 | { instance: 'eth-1.tornadocash.eth', value: '20' }, 28 | { instance: 'eth-10.tornadocash.eth', value: '50' }, 29 | { instance: 'eth-100.tornadocash.eth', value: '400' }, 30 | ], 31 | }, 32 | rewardSwap: { address: 'reward-swap.contract.tornadocash.eth', poolWeight: 1e11 }, 33 | tornadoTrees: { address: 'tornado-trees.contract.tornadocash.eth', levels: 20 }, 34 | tornadoProxy: { address: 'tornado-proxy.contract.tornadocash.eth' }, 35 | tornadoProxyLight: { address: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17' }, 36 | rewardVerifier: { address: 'reward-verifier.contract.tornadocash.eth' }, 37 | treeUpdateVerifier: { address: 'tree-update-verifier.contract.tornadocash.eth' }, 38 | withdrawVerifier: { address: 'withdraw-verifier.contract.tornadocash.eth' }, 39 | poseidonHasher2: { address: 'poseidon2.contract.tornadocash.eth' }, 40 | poseidonHasher3: { address: 'poseidon3.contract.tornadocash.eth' }, 41 | feeManager: { address: 'fee-manager.contract.tornadocash.eth' }, 42 | tornadoStakingRewards: { address: 'staking-rewards.contract.tornadocash.eth' }, 43 | relayerRegistry: { address: 'relayer-registry.contract.tornadocash.eth' }, 44 | tornadoRouter: { address: 'tornado-router.contract.tornadocash.eth' }, 45 | instanceRegistry: { address: 'instance-registry.contract.tornadocash.eth' }, 46 | deployer: { address: 'deployer.contract.tornadocash.eth' }, 47 | vesting: { 48 | team1: { 49 | address: 'team1.vesting.contract.tornadocash.eth', 50 | beneficiary: '0x5A7a51bFb49F190e5A6060a5bc6052Ac14a3b59f', 51 | cliff: 12, 52 | duration: 36, 53 | }, 54 | team2: { 55 | address: 'team2.vesting.contract.tornadocash.eth', 56 | beneficiary: '0xF50D442e48E11F16e105431a2664141f44F9feD8', 57 | cliff: 12, 58 | duration: 36, 59 | }, 60 | team3: { 61 | address: 'team3.vesting.contract.tornadocash.eth', 62 | beneficiary: '0x6D2C515Ff6A40554869C3Da05494b8D6910D075E', 63 | cliff: 12, 64 | duration: 36, 65 | }, 66 | team4: { 67 | address: 'team4.vesting.contract.tornadocash.eth', 68 | beneficiary: '0x504a9c37794a2341F4861bF0A44E8d4016DF8cF2', 69 | cliff: 12, 70 | duration: 36, 71 | }, 72 | team5: { 73 | address: 'team5.vesting.contract.tornadocash.eth', 74 | beneficiary: '0x2D81713c58452c92C19b2917e1C770eEcF53Fe41', 75 | cliff: 12, 76 | duration: 36, 77 | }, 78 | governance: { 79 | address: 'governance.vesting.contract.tornadocash.eth', 80 | cliff: 3, 81 | duration: 60, 82 | }, 83 | }, 84 | instances: { 85 | netId1: { 86 | eth: { 87 | instanceAddress: { 88 | 0.1: '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc', 89 | 1: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936', 90 | 10: '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF', 91 | 100: '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291', 92 | }, 93 | symbol: 'ETH', 94 | decimals: 18, 95 | }, 96 | dai: { 97 | instanceAddress: { 98 | 100: '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3', 99 | 1000: '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144', 100 | 10000: '0x07687e702b410Fa43f4cB4Af7FA097918ffD2730', 101 | 100000: '0x23773E65ed146A459791799d01336DB287f25334', 102 | }, 103 | tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', 104 | symbol: 'DAI', 105 | decimals: 18, 106 | }, 107 | cdai: { 108 | instanceAddress: { 109 | 5000: '0x22aaA7720ddd5388A3c0A3333430953C68f1849b', 110 | 50000: '0x03893a7c7463AE47D46bc7f091665f1893656003', 111 | 500000: '0x2717c5e28cf931547B621a5dddb772Ab6A35B701', 112 | 5000000: '0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af', 113 | }, 114 | tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 115 | symbol: 'cDAI', 116 | decimals: 8, 117 | }, 118 | usdc: { 119 | instanceAddress: { 120 | 100: '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307', 121 | 1000: '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D', 122 | 10000: '0xD691F27f38B395864Ea86CfC7253969B409c362d', 123 | 100000: undefined, 124 | }, 125 | tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 126 | symbol: 'USDC', 127 | decimals: 6, 128 | }, 129 | usdt: { 130 | instanceAddress: { 131 | 100: '0x169AD27A470D064DEDE56a2D3ff727986b15D52B', 132 | 1000: '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f', 133 | 10000: '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a', 134 | 100000: '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E', 135 | }, 136 | tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', 137 | symbol: 'USDT', 138 | decimals: 6, 139 | }, 140 | wbtc: { 141 | instanceAddress: { 142 | 0.1: '0x178169B423a011fff22B9e3F3abeA13414dDD0F1', 143 | 1: '0x610B717796ad172B316836AC95a2ffad065CeaB4', 144 | 10: '0xbB93e510BbCD0B7beb5A853875f9eC60275CF498', 145 | 100: undefined, 146 | }, 147 | tokenAddress: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 148 | symbol: 'WBTC', 149 | decimals: 8, 150 | }, 151 | }, 152 | netId5: { 153 | eth: { 154 | instanceAddress: { 155 | 0.1: '0x6Bf694a291DF3FeC1f7e69701E3ab6c592435Ae7', 156 | 1: '0x3aac1cC67c2ec5Db4eA850957b967Ba153aD6279', 157 | 10: '0x723B78e67497E85279CB204544566F4dC5d2acA0', 158 | 100: '0x0E3A09dDA6B20aFbB34aC7cD4A6881493f3E7bf7', 159 | }, 160 | symbol: 'ETH', 161 | decimals: 18, 162 | }, 163 | dai: { 164 | instanceAddress: { 165 | 100: '0x76D85B4C0Fc497EeCc38902397aC608000A06607', 166 | 1000: '0xCC84179FFD19A1627E79F8648d09e095252Bc418', 167 | 10000: '0xD5d6f8D9e784d0e26222ad3834500801a68D027D', 168 | 100000: '0x407CcEeaA7c95d2FE2250Bf9F2c105aA7AAFB512', 169 | }, 170 | tokenAddress: '0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60', 171 | symbol: 'DAI', 172 | decimals: 18, 173 | }, 174 | cdai: { 175 | instanceAddress: { 176 | 5000: '0x833481186f16Cece3f1Eeea1a694c42034c3a0dB', 177 | 50000: '0xd8D7DE3349ccaA0Fde6298fe6D7b7d0d34586193', 178 | 500000: '0x8281Aa6795aDE17C8973e1aedcA380258Bc124F9', 179 | 5000000: '0x57b2B8c82F065de8Ef5573f9730fC1449B403C9f', 180 | }, 181 | tokenAddress: '0x822397d9a55d0fefd20F5c4bCaB33C5F65bd28Eb', 182 | symbol: 'cDAI', 183 | decimals: 8, 184 | }, 185 | usdc: { 186 | instanceAddress: { 187 | 100: '0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45', 188 | 1000: '0x23173fE8b96A4Ad8d2E17fB83EA5dcccdCa1Ae52', 189 | 10000: undefined, 190 | 100000: undefined, 191 | }, 192 | tokenAddress: '0xD87Ba7A50B2E7E660f678A895E4B72E7CB4CCd9C', 193 | symbol: 'USDC', 194 | decimals: 6, 195 | }, 196 | usdt: { 197 | instanceAddress: { 198 | 100: '0x538Ab61E8A9fc1b2f93b3dd9011d662d89bE6FE6', 199 | 1000: '0x94Be88213a387E992Dd87DE56950a9aef34b9448', 200 | 10000: undefined, 201 | 100000: undefined, 202 | }, 203 | tokenAddress: '0xb7FC2023D96AEa94Ba0254AA5Aeb93141e4aad66', 204 | symbol: 'USDT', 205 | decimals: 6, 206 | }, 207 | wbtc: { 208 | instanceAddress: { 209 | 0.1: '0x242654336ca2205714071898f67E254EB49ACdCe', 210 | 1: '0x776198CCF446DFa168347089d7338879273172cF', 211 | 10: '0xeDC5d01286f99A066559F60a585406f3878a033e', 212 | 100: undefined, 213 | }, 214 | tokenAddress: '0xC04B0d3107736C32e19F1c62b2aF67BE61d63a05', 215 | symbol: 'WBTC', 216 | decimals: 8, 217 | }, 218 | }, 219 | netId10: { 220 | eth: { 221 | instanceAddress: { 222 | 0.1: '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F', 223 | 1: '0xd47438C816c9E7f2E2888E060936a499Af9582b3', 224 | 10: '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a', 225 | 100: '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD', 226 | }, 227 | symbol: 'ETH', 228 | decimals: 18, 229 | }, 230 | }, 231 | netId56: { 232 | bnb: { 233 | instanceAddress: { 234 | 0.1: '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F', 235 | 1: '0xd47438C816c9E7f2E2888E060936a499Af9582b3', 236 | 10: '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a', 237 | 100: '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD', 238 | }, 239 | symbol: 'BNB', 240 | decimals: 18, 241 | }, 242 | }, 243 | netId100: { 244 | xdai: { 245 | instanceAddress: { 246 | 100: '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD', 247 | 1000: '0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178', 248 | 10000: '0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040', 249 | 100000: '0xa5C2254e4253490C54cef0a4347fddb8f75A4998', 250 | }, 251 | symbol: 'xDAI', 252 | decimals: 18, 253 | }, 254 | }, 255 | netId137: { 256 | matic: { 257 | instanceAddress: { 258 | 100: '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD', 259 | 1000: '0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178', 260 | 10000: '0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040', 261 | 100000: '0xa5C2254e4253490C54cef0a4347fddb8f75A4998', 262 | }, 263 | symbol: 'MATIC', 264 | decimals: 18, 265 | }, 266 | }, 267 | netId42161: { 268 | eth: { 269 | instanceAddress: { 270 | 0.1: '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F', 271 | 1: '0xd47438C816c9E7f2E2888E060936a499Af9582b3', 272 | 10: '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a', 273 | 100: '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD', 274 | }, 275 | symbol: 'ETH', 276 | decimals: 18, 277 | }, 278 | }, 279 | netId43114: { 280 | avax: { 281 | instanceAddress: { 282 | 10: '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a', 283 | 100: '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD', 284 | 500: '0xaf8d1839c3c67cf571aa74B5c12398d4901147B3', 285 | }, 286 | symbol: 'AVAX', 287 | decimals: 18, 288 | }, 289 | }, 290 | }, 291 | } 292 | -------------------------------------------------------------------------------- /contracts/Airdrop.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "./ENS.sol"; 8 | 9 | contract Airdrop is EnsResolve { 10 | struct Recipient { 11 | address to; 12 | uint256 amount; 13 | } 14 | 15 | constructor(bytes32 tokenAddress, Recipient[] memory targets) public { 16 | IERC20 token = IERC20(resolve(tokenAddress)); 17 | require(token.balanceOf(address(this)) > 0, "Balance is 0, airdrop already done"); 18 | for (uint256 i = 0; i < targets.length; i++) { 19 | token.transfer(targets[i].to, targets[i].amount); 20 | } 21 | selfdestruct(address(0)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/ECDSA.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | // A copy from https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237/files 6 | 7 | /** 8 | * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. 9 | * 10 | * These functions can be used to verify that a message was signed by the holder 11 | * of the private keys of a given address. 12 | */ 13 | library ECDSA { 14 | /** 15 | * @dev Returns the address that signed a hashed message (`hash`) with 16 | * `signature`. This address can then be used for verification purposes. 17 | * 18 | * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: 19 | * this function rejects them by requiring the `s` value to be in the lower 20 | * half order, and the `v` value to be either 27 or 28. 21 | * 22 | * IMPORTANT: `hash` _must_ be the result of a hash operation for the 23 | * verification to be secure: it is possible to craft signatures that 24 | * recover to arbitrary addresses for non-hashed data. A safe way to ensure 25 | * this is by receiving a hash of the original message (which may otherwise 26 | * be too long), and then calling {toEthSignedMessageHash} on it. 27 | */ 28 | function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { 29 | // Check the signature length 30 | if (signature.length != 65) { 31 | revert("ECDSA: invalid signature length"); 32 | } 33 | 34 | // Divide the signature in r, s and v variables 35 | bytes32 r; 36 | bytes32 s; 37 | uint8 v; 38 | 39 | // ecrecover takes the signature parameters, and the only way to get them 40 | // currently is to use assembly. 41 | // solhint-disable-next-line no-inline-assembly 42 | assembly { 43 | r := mload(add(signature, 0x20)) 44 | s := mload(add(signature, 0x40)) 45 | v := mload(add(signature, 0x41)) 46 | } 47 | 48 | return recover(hash, v, r, s); 49 | } 50 | 51 | /** 52 | * @dev Overload of {ECDSA-recover-bytes32-bytes-} that receives the `v`, 53 | * `r` and `s` signature fields separately. 54 | */ 55 | function recover( 56 | bytes32 hash, 57 | uint8 v, 58 | bytes32 r, 59 | bytes32 s 60 | ) internal pure returns (address) { 61 | // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature 62 | // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines 63 | // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most 64 | // signatures from current libraries generate a unique signature with an s-value in the lower half order. 65 | // 66 | // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value 67 | // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or 68 | // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept 69 | // these malleable signatures as well. 70 | require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "ECDSA: invalid signature 's' value"); 71 | require(v == 27 || v == 28, "ECDSA: invalid signature 'v' value"); 72 | 73 | // If the signature is valid (and not malleable), return the signer address 74 | address signer = ecrecover(hash, v, r, s); 75 | require(signer != address(0), "ECDSA: invalid signature"); 76 | 77 | return signer; 78 | } 79 | 80 | /** 81 | * @dev Returns an Ethereum Signed Message, created from a `hash`. This 82 | * replicates the behavior of the 83 | * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] 84 | * JSON-RPC method. 85 | * 86 | * See {recover}. 87 | */ 88 | function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { 89 | // 32 is the length in bytes of hash, 90 | // enforced by the type signature above 91 | return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/ENS.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | interface ENS { 6 | function resolver(bytes32 node) external view returns (Resolver); 7 | } 8 | 9 | interface Resolver { 10 | function addr(bytes32 node) external view returns (address); 11 | } 12 | 13 | contract EnsResolve { 14 | function resolve(bytes32 node) public view virtual returns (address) { 15 | ENS Registry = ENS( 16 | getChainId() == 1 ? 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e : 0x8595bFb0D940DfEDC98943FA8a907091203f25EE 17 | ); 18 | return Registry.resolver(node).addr(node); 19 | } 20 | 21 | function bulkResolve(bytes32[] memory domains) public view returns (address[] memory result) { 22 | result = new address[](domains.length); 23 | for (uint256 i = 0; i < domains.length; i++) { 24 | result[i] = resolve(domains[i]); 25 | } 26 | } 27 | 28 | function getChainId() internal pure returns (uint256) { 29 | uint256 chainId; 30 | assembly { 31 | chainId := chainid() 32 | } 33 | return chainId; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/ERC20Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | // Adapted copy from https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237/files 6 | 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "./ECDSA.sol"; 9 | 10 | /** 11 | * @dev Extension of {ERC20} that allows token holders to use their tokens 12 | * without sending any transactions by setting {IERC20-allowance} with a 13 | * signature using the {permit} method, and then spend them via 14 | * {IERC20-transferFrom}. 15 | * 16 | * The {permit} signature mechanism conforms to the {IERC2612Permit} interface. 17 | */ 18 | abstract contract ERC20Permit is ERC20 { 19 | mapping(address => uint256) private _nonces; 20 | 21 | bytes32 private constant _PERMIT_TYPEHASH = keccak256( 22 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" 23 | ); 24 | 25 | // Mapping of ChainID to domain separators. This is a very gas efficient way 26 | // to not recalculate the domain separator on every call, while still 27 | // automatically detecting ChainID changes. 28 | mapping(uint256 => bytes32) private _domainSeparators; 29 | 30 | constructor() internal { 31 | _updateDomainSeparator(); 32 | } 33 | 34 | /** 35 | * @dev See {IERC2612Permit-permit}. 36 | * 37 | * If https://eips.ethereum.org/EIPS/eip-1344[ChainID] ever changes, the 38 | * EIP712 Domain Separator is automatically recalculated. 39 | */ 40 | function permit( 41 | address owner, 42 | address spender, 43 | uint256 amount, 44 | uint256 deadline, 45 | uint8 v, 46 | bytes32 r, 47 | bytes32 s 48 | ) public { 49 | require(blockTimestamp() <= deadline, "ERC20Permit: expired deadline"); 50 | 51 | bytes32 hashStruct = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, amount, _nonces[owner], deadline)); 52 | 53 | bytes32 hash = keccak256(abi.encodePacked(uint16(0x1901), _domainSeparator(), hashStruct)); 54 | 55 | address signer = ECDSA.recover(hash, v, r, s); 56 | require(signer == owner, "ERC20Permit: invalid signature"); 57 | 58 | _nonces[owner]++; 59 | _approve(owner, spender, amount); 60 | } 61 | 62 | /** 63 | * @dev See {IERC2612Permit-nonces}. 64 | */ 65 | function nonces(address owner) public view returns (uint256) { 66 | return _nonces[owner]; 67 | } 68 | 69 | function _updateDomainSeparator() private returns (bytes32) { 70 | uint256 _chainID = chainID(); 71 | 72 | bytes32 newDomainSeparator = keccak256( 73 | abi.encode( 74 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 75 | keccak256(bytes(name())), 76 | keccak256(bytes("1")), // Version 77 | _chainID, 78 | address(this) 79 | ) 80 | ); 81 | 82 | _domainSeparators[_chainID] = newDomainSeparator; 83 | 84 | return newDomainSeparator; 85 | } 86 | 87 | // Returns the domain separator, updating it if chainID changes 88 | function _domainSeparator() private returns (bytes32) { 89 | bytes32 domainSeparator = _domainSeparators[chainID()]; 90 | if (domainSeparator != 0x00) { 91 | return domainSeparator; 92 | } else { 93 | return _updateDomainSeparator(); 94 | } 95 | } 96 | 97 | function chainID() public view virtual returns (uint256 _chainID) { 98 | assembly { 99 | _chainID := chainid() 100 | } 101 | } 102 | 103 | function blockTimestamp() public view virtual returns (uint256) { 104 | return block.timestamp; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /contracts/TORN.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol"; 9 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 10 | import "@openzeppelin/contracts/access/Ownable.sol"; 11 | import "@openzeppelin/contracts/utils/Pausable.sol"; 12 | import "@openzeppelin/contracts/math/Math.sol"; 13 | import "./ERC20Permit.sol"; 14 | import "./ENS.sol"; 15 | 16 | contract TORN is ERC20("TornadoCash", "TORN"), ERC20Burnable, ERC20Permit, Pausable, EnsResolve { 17 | using SafeERC20 for IERC20; 18 | 19 | uint256 public immutable canUnpauseAfter; 20 | address public immutable governance; 21 | mapping(address => bool) public allowedTransferee; 22 | 23 | event Allowed(address target); 24 | event Disallowed(address target); 25 | 26 | struct Recipient { 27 | bytes32 to; 28 | uint256 amount; 29 | } 30 | 31 | constructor( 32 | bytes32 _governance, 33 | uint256 _pausePeriod, 34 | Recipient[] memory _vestings 35 | ) public { 36 | address _resolvedGovernance = resolve(_governance); 37 | governance = _resolvedGovernance; 38 | allowedTransferee[_resolvedGovernance] = true; 39 | 40 | for (uint256 i = 0; i < _vestings.length; i++) { 41 | address to = resolve(_vestings[i].to); 42 | _mint(to, _vestings[i].amount); 43 | allowedTransferee[to] = true; 44 | } 45 | 46 | canUnpauseAfter = blockTimestamp().add(_pausePeriod); 47 | _pause(); 48 | require(totalSupply() == 10000000 ether, "TORN: incorrect distribution"); 49 | } 50 | 51 | modifier onlyGovernance() { 52 | require(_msgSender() == governance, "TORN: only governance can perform this action"); 53 | _; 54 | } 55 | 56 | function changeTransferability(bool decision) public onlyGovernance { 57 | require(blockTimestamp() > canUnpauseAfter, "TORN: cannot change transferability yet"); 58 | if (decision) { 59 | _unpause(); 60 | } else { 61 | _pause(); 62 | } 63 | } 64 | 65 | function addToAllowedList(address[] memory target) public onlyGovernance { 66 | for (uint256 i = 0; i < target.length; i++) { 67 | allowedTransferee[target[i]] = true; 68 | emit Allowed(target[i]); 69 | } 70 | } 71 | 72 | function removeFromAllowedList(address[] memory target) public onlyGovernance { 73 | for (uint256 i = 0; i < target.length; i++) { 74 | allowedTransferee[target[i]] = false; 75 | emit Disallowed(target[i]); 76 | } 77 | } 78 | 79 | function _beforeTokenTransfer( 80 | address from, 81 | address to, 82 | uint256 amount 83 | ) internal override { 84 | super._beforeTokenTransfer(from, to, amount); 85 | require(!paused() || allowedTransferee[from] || allowedTransferee[to], "TORN: paused"); 86 | require(to != address(this), "TORN: invalid recipient"); 87 | } 88 | 89 | /// @dev Method to claim junk and accidentally sent tokens 90 | function rescueTokens( 91 | IERC20 _token, 92 | address payable _to, 93 | uint256 _balance 94 | ) external onlyGovernance { 95 | require(_to != address(0), "TORN: can not send to zero address"); 96 | 97 | if (_token == IERC20(0)) { 98 | // for Ether 99 | uint256 totalBalance = address(this).balance; 100 | uint256 balance = _balance == 0 ? totalBalance : Math.min(totalBalance, _balance); 101 | _to.transfer(balance); 102 | } else { 103 | // any other erc20 104 | uint256 totalBalance = _token.balanceOf(address(this)); 105 | uint256 balance = _balance == 0 ? totalBalance : Math.min(totalBalance, _balance); 106 | require(balance > 0, "TORN: trying to send 0 balance"); 107 | _token.safeTransfer(_to, balance); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /contracts/Vesting.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/math/Math.sol"; 8 | import "@openzeppelin/contracts/math/SafeMath.sol"; 9 | import "./ENS.sol"; 10 | 11 | /** 12 | * @title Vesting 13 | * @dev A token holder contract that can release its token balance gradually like a 14 | * typical vesting scheme, with a cliff and vesting period. Optionally revocable by the 15 | * owner. 16 | */ 17 | contract Vesting is EnsResolve { 18 | using SafeERC20 for IERC20; 19 | using SafeMath for uint256; 20 | 21 | uint256 public constant SECONDS_PER_MONTH = 30 days; 22 | 23 | event Released(uint256 amount); 24 | 25 | // beneficiary of tokens after they are released 26 | address public immutable beneficiary; 27 | IERC20 public immutable token; 28 | 29 | uint256 public immutable cliffInMonths; 30 | uint256 public immutable startTimestamp; 31 | uint256 public immutable durationInMonths; 32 | uint256 public released; 33 | 34 | /** 35 | * @dev Creates a vesting contract that vests its balance of any ERC20 token to the 36 | * _beneficiary, monthly in a linear fashion until duration has passed. By then all 37 | * of the balance will have vested. 38 | * @param _beneficiary address of the beneficiary to whom vested tokens are transferred 39 | * @param _cliffInMonths duration in months of the cliff in which tokens will begin to vest 40 | * @param _durationInMonths duration in months of the period in which the tokens will vest 41 | */ 42 | constructor( 43 | bytes32 _token, 44 | address _beneficiary, 45 | uint256 _startTimestamp, 46 | uint256 _cliffInMonths, 47 | uint256 _durationInMonths 48 | ) public { 49 | require(_beneficiary != address(0), "Beneficiary cannot be empty"); 50 | require(_cliffInMonths <= _durationInMonths, "Cliff is greater than duration"); 51 | 52 | token = IERC20(resolve(_token)); 53 | beneficiary = _beneficiary; 54 | durationInMonths = _durationInMonths; 55 | cliffInMonths = _cliffInMonths; 56 | startTimestamp = _startTimestamp == 0 ? blockTimestamp() : _startTimestamp; 57 | } 58 | 59 | /** 60 | * @notice Transfers vested tokens to beneficiary. 61 | */ 62 | function release() external { 63 | uint256 vested = vestedAmount(); 64 | require(vested > 0, "No tokens to release"); 65 | 66 | released = released.add(vested); 67 | token.safeTransfer(beneficiary, vested); 68 | 69 | emit Released(vested); 70 | } 71 | 72 | /** 73 | * @dev Calculates the amount that has already vested but hasn't been released yet. 74 | */ 75 | function vestedAmount() public view returns (uint256) { 76 | if (blockTimestamp() < startTimestamp) { 77 | return 0; 78 | } 79 | 80 | uint256 elapsedTime = blockTimestamp().sub(startTimestamp); 81 | uint256 elapsedMonths = elapsedTime.div(SECONDS_PER_MONTH); 82 | 83 | if (elapsedMonths < cliffInMonths) { 84 | return 0; 85 | } 86 | 87 | // If over vesting duration, all tokens vested 88 | if (elapsedMonths >= durationInMonths) { 89 | return token.balanceOf(address(this)); 90 | } else { 91 | uint256 currentBalance = token.balanceOf(address(this)); 92 | uint256 totalBalance = currentBalance.add(released); 93 | 94 | uint256 vested = totalBalance.mul(elapsedMonths).div(durationInMonths); 95 | uint256 unreleased = vested.sub(released); 96 | 97 | // currentBalance can be 0 in case of vesting being revoked earlier. 98 | return Math.min(currentBalance, unreleased); 99 | } 100 | } 101 | 102 | function blockTimestamp() public view virtual returns (uint256) { 103 | return block.timestamp; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /contracts/Voucher.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * This is tornado.cash airdrop for early adopters. In order to claim your TORN token please follow https://tornado.cash/airdrop 3 | */ 4 | 5 | // SPDX-License-Identifier: MIT 6 | pragma solidity ^0.6.0; 7 | pragma experimental ABIEncoderV2; 8 | 9 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 10 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 11 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 12 | import "./ENS.sol"; 13 | 14 | contract Voucher is ERC20("TornadoCash voucher for early adopters", "vTORN"), EnsResolve { 15 | using SafeERC20 for IERC20; 16 | 17 | IERC20 public immutable torn; 18 | uint256 public immutable expiresAt; 19 | address public immutable governance; 20 | mapping(address => bool) public allowedTransferee; 21 | 22 | struct Recipient { 23 | address to; 24 | uint256 amount; 25 | } 26 | 27 | constructor( 28 | bytes32 _torn, 29 | bytes32 _governance, 30 | uint256 _duration, 31 | Recipient[] memory _airdrops 32 | ) public { 33 | torn = IERC20(resolve(_torn)); 34 | governance = resolve(_governance); 35 | expiresAt = blockTimestamp().add(_duration); 36 | for (uint256 i = 0; i < _airdrops.length; i++) { 37 | _mint(_airdrops[i].to, _airdrops[i].amount); 38 | allowedTransferee[_airdrops[i].to] = true; 39 | } 40 | } 41 | 42 | function redeem() external { 43 | require(blockTimestamp() < expiresAt, "Airdrop redeem period has ended"); 44 | uint256 amount = balanceOf(msg.sender); 45 | _burn(msg.sender, amount); 46 | torn.safeTransfer(msg.sender, amount); 47 | } 48 | 49 | function rescueExpiredTokens() external { 50 | require(blockTimestamp() >= expiresAt, "Airdrop redeem period has not ended yet"); 51 | torn.safeTransfer(governance, torn.balanceOf(address(this))); 52 | } 53 | 54 | function _beforeTokenTransfer( 55 | address from, 56 | address to, 57 | uint256 amount 58 | ) internal override { 59 | super._beforeTokenTransfer(from, to, amount); 60 | require(to == address(0) || from == address(0) || allowedTransferee[from], "ERC20: transfer is not allowed"); 61 | } 62 | 63 | function blockTimestamp() public view virtual returns (uint256) { 64 | return block.timestamp; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/mocks/AirdropMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../Airdrop.sol"; 6 | 7 | contract AirdropMock is Airdrop { 8 | constructor(bytes32 tokenAddress, Recipient[] memory targets) public Airdrop(tokenAddress, targets) {} 9 | 10 | function resolve(bytes32 addr) public view override returns (address) { 11 | return address(uint160(uint256(addr) >> (12 * 8))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mocks/ENSMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | contract ENSMock { 7 | mapping(bytes32 => address) public registry; 8 | 9 | function resolver( 10 | bytes32 /* _node */ 11 | ) external view returns (address) { 12 | return address(this); 13 | } 14 | 15 | function addr(bytes32 _node) external view returns (address) { 16 | return registry[_node]; 17 | } 18 | 19 | function setAddr(bytes32 _node, address _addr) external { 20 | registry[_node] = _addr; 21 | } 22 | 23 | function multicall(bytes[] calldata data) external returns (bytes[] memory results) { 24 | results = new bytes[](data.length); 25 | for (uint256 i = 0; i < data.length; i++) { 26 | (bool success, bytes memory result) = address(this).delegatecall(data[i]); 27 | require(success); 28 | results[i] = result; 29 | } 30 | return results; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/mocks/TORNMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "../TORN.sol"; 6 | import "./Timestamp.sol"; 7 | 8 | contract TORNMock is TORN, Timestamp { 9 | uint256 public chainId; 10 | 11 | constructor( 12 | bytes32 _governance, 13 | uint256 _pausePeriod, 14 | Recipient[] memory _vesting 15 | ) public TORN(_governance, _pausePeriod, _vesting) {} 16 | 17 | function resolve(bytes32 addr) public view override returns (address) { 18 | return address(uint160(uint256(addr) >> (12 * 8))); 19 | } 20 | 21 | function setChainId(uint256 _chainId) public { 22 | chainId = _chainId; 23 | } 24 | 25 | function chainID() public view override returns (uint256) { 26 | return chainId; 27 | } 28 | 29 | function blockTimestamp() public view override(Timestamp, ERC20Permit) returns (uint256) { 30 | return Timestamp.blockTimestamp(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/mocks/Timestamp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | contract Timestamp { 5 | uint256 public fakeTimestamp; 6 | 7 | function setFakeTimestamp(uint256 _fakeTimestamp) public { 8 | fakeTimestamp = _fakeTimestamp; 9 | } 10 | 11 | function blockTimestamp() public view virtual returns (uint256) { 12 | return fakeTimestamp == 0 ? block.timestamp : fakeTimestamp; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/mocks/VestingMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import "../Vesting.sol"; 7 | import "./Timestamp.sol"; 8 | 9 | contract VestingMock is Vesting, Timestamp { 10 | constructor( 11 | bytes32 _token, 12 | address _beneficiary, 13 | uint256 _startTimestamp, 14 | uint256 _cliffInMonths, 15 | uint256 _durationInMonths 16 | ) public Vesting(_token, _beneficiary, _startTimestamp, _cliffInMonths, _durationInMonths) {} 17 | 18 | function resolve(bytes32 addr) public view override returns (address) { 19 | return address(uint160(uint256(addr) >> (12 * 8))); 20 | } 21 | 22 | function blockTimestamp() public view override(Timestamp, Vesting) returns (uint256) { 23 | return Timestamp.blockTimestamp(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/mocks/VoucherMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "../Voucher.sol"; 8 | import "./Timestamp.sol"; 9 | 10 | contract VoucherMock is Voucher, Timestamp { 11 | constructor( 12 | bytes32 _torn, 13 | bytes32 _governance, 14 | uint256 _duration, 15 | Recipient[] memory _airdrops 16 | ) public Voucher(_torn, _governance, _duration, _airdrops) {} 17 | 18 | function resolve(bytes32 addr) public view override returns (address) { 19 | return address(uint160(uint256(addr) >> (12 * 8))); 20 | } 21 | 22 | function blockTimestamp() public view override(Timestamp, Voucher) returns (uint256) { 23 | return Timestamp.blockTimestamp(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export const torn: Torn 2 | export const governance: Address 3 | export const governanceImpl: Address 4 | export const voucher: Voucher 5 | export const miningV2: Mining 6 | export const rewardSwap: RewardSwap 7 | export const tornadoTrees: TornadoTrees 8 | export const tornadoProxy: Address 9 | export const tornadoProxyLight: Address 10 | export const rewardVerifier: Address 11 | export const treeUpdateVerifier: Address 12 | export const withdrawVerifier: Address 13 | export const poseidonHasher2: Address 14 | export const poseidonHasher3: Address 15 | export const feeManager: Address 16 | export const tornadoStakingRewards: Address 17 | export const relayerRegistry: Address 18 | export const tornadoRouter: Address 19 | export const instanceRegistry: Address 20 | export const deployer: Address 21 | export const vesting: Vesting 22 | export const instances: Instances 23 | 24 | 25 | export type availableIds = 1 | 5 | 10 | 56 | 100 | 137 | 42161 | 43114 26 | export type availableTokens = 'eth' | 'dai' | 'cdai' | 'usdc' | 'usdt' | 'wbtc' | 'xdai' | 'matic' | 'avax' | 'bnb' 27 | export type netIds = `netId${availableIds}` 28 | 29 | export type Address = { 30 | address: string 31 | } 32 | 33 | export type Instances = { 34 | [p in netIds]: NetInstances; 35 | }; 36 | 37 | export type NetInstances = { 38 | [p in availableTokens]?: TInstance; 39 | } 40 | 41 | export type TInstance = { 42 | instanceAddress: InstanceAddress 43 | tokenAddress?: string 44 | symbol: string 45 | decimals: number 46 | } 47 | 48 | export type InstanceAddress = { 49 | '0.1'?: string 50 | '1'?: string 51 | '10'?: string 52 | '100'?: string 53 | '500'?: string 54 | '1000'?: string 55 | '5000'?: string 56 | '10000'?: string 57 | '50000'?: string 58 | '100000'?: string 59 | '500000'?: string 60 | '5000000'?: string 61 | } 62 | 63 | export type Mining = Address & { 64 | initialBalance: string 65 | rates: Rate[] 66 | } 67 | 68 | export type Rate = { 69 | instance: string 70 | value: string 71 | } 72 | 73 | export type RewardSwap = Address & { 74 | poolWeight: number 75 | } 76 | 77 | export type Torn = Address & { 78 | cap: string 79 | pausePeriod: number 80 | distribution: { [key: string]: Distribution } 81 | } 82 | 83 | export type Distribution = { 84 | to: string 85 | amount: string 86 | } 87 | 88 | export type TornadoTrees = Address & { 89 | levels: number 90 | } 91 | 92 | export interface Vesting { 93 | team1: Governance; 94 | team2: Governance; 95 | team3: Governance; 96 | team4: Governance; 97 | team5: Governance; 98 | governance: Governance; 99 | } 100 | 101 | export type Governance = Address & { 102 | cliff: number 103 | duration: number 104 | beneficiary?: string 105 | } 106 | 107 | export type Voucher = Address & { 108 | duration: number 109 | } 110 | -------------------------------------------------------------------------------- /lib/Permit.js: -------------------------------------------------------------------------------- 1 | const { EIP712Signer } = require('@ticket721/e712') 2 | 3 | const Permit = [ 4 | { name: 'owner', type: 'address' }, 5 | { name: 'spender', type: 'address' }, 6 | { name: 'value', type: 'uint256' }, 7 | { name: 'nonce', type: 'uint256' }, 8 | { name: 'deadline', type: 'uint256' }, 9 | ] 10 | 11 | class PermitSigner extends EIP712Signer { 12 | constructor(_domain, _permitArgs) { 13 | super(_domain, ['Permit', Permit]) 14 | this.permitArgs = _permitArgs 15 | } 16 | 17 | // Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline) 18 | setPermitInfo(_permitArgs) { 19 | this.permitArgs = _permitArgs 20 | } 21 | 22 | getPayload() { 23 | return this.generatePayload(this.permitArgs, 'Permit') 24 | } 25 | 26 | async getSignature(privateKey) { 27 | const payload = this.getPayload() 28 | const { hex, v, r, s } = await this.sign(privateKey, payload) 29 | return { 30 | hex, 31 | v, 32 | r: '0x' + r, 33 | s: '0x' + s, 34 | } 35 | } 36 | 37 | getSignerAddress(permitArgs, signature) { 38 | const original_payload = this.generatePayload(permitArgs, 'Permit') 39 | return this.verify(original_payload, signature) 40 | } 41 | } 42 | 43 | module.exports = { PermitSigner } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torn-token", 3 | "version": "1.0.8", 4 | "main": "config.js", 5 | "repository": "https://github.com/tornadocash/torn-token.git", 6 | "author": "Tornadocash team ", 7 | "license": "MIT", 8 | "keywords": [ 9 | "web3", 10 | "ethereum", 11 | "solidity" 12 | ], 13 | "files": [ 14 | "config.js", 15 | "contracts/*", 16 | "index.d.ts" 17 | ], 18 | "types": "index.d.ts", 19 | "scripts": { 20 | "compile": "truffle compile", 21 | "coverage": "yarn compile && truffle run coverage", 22 | "test": "truffle test", 23 | "test:stacktrace": "yarn test --stacktrace", 24 | "eslint": "eslint --ext .js --ignore-path .gitignore .", 25 | "prettier:check": "prettier --check . --config .prettierrc", 26 | "prettier:fix": "prettier --write . --config .prettierrc", 27 | "lint": "yarn eslint && yarn prettier:check", 28 | "verify": "truffle run verify --network $NETWORK" 29 | }, 30 | "devDependencies": { 31 | "@ticket721/e712": "^0.4.1", 32 | "babel-eslint": "^10.1.0", 33 | "bn-chai": "^1.0.1", 34 | "chai": "^4.2.0", 35 | "chai-as-promised": "^7.1.1", 36 | "eslint": "^7.5.0", 37 | "prettier": "^2.1.2", 38 | "prettier-plugin-solidity": "^1.0.0-alpha.59", 39 | "rlp": "^2.2.6", 40 | "solhint-plugin-prettier": "^0.0.4", 41 | "solidity-coverage": "^0.7.7", 42 | "truffle": "^5.1.29", 43 | "truffle-flattener": "^1.4.4", 44 | "truffle-hdwallet-provider": "^1.0.17", 45 | "truffle-plugin-verify": "^0.3.11" 46 | }, 47 | "dependencies": { 48 | "@openzeppelin/contracts": "^3.1.0", 49 | "eth-sig-util": "^2.5.3", 50 | "ethereumjs-util": "^7.0.3", 51 | "web3": "^1.2.11", 52 | "web3-utils": "^1.7.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/ganacheHelper.js: -------------------------------------------------------------------------------- 1 | // This module is used only for tests 2 | function send(method, params = []) { 3 | return new Promise((resolve, reject) => { 4 | // eslint-disable-next-line no-undef 5 | web3.currentProvider.send( 6 | { 7 | jsonrpc: '2.0', 8 | id: Date.now(), 9 | method, 10 | params, 11 | }, 12 | (err, res) => { 13 | return err ? reject(err) : resolve(res) 14 | }, 15 | ) 16 | }) 17 | } 18 | 19 | const takeSnapshot = async () => { 20 | return await send('evm_snapshot') 21 | } 22 | 23 | const traceTransaction = async (tx) => { 24 | return await send('debug_traceTransaction', [tx, {}]) 25 | } 26 | 27 | const revertSnapshot = async (id) => { 28 | await send('evm_revert', [id]) 29 | } 30 | 31 | const mineBlock = async (timestamp) => { 32 | await send('evm_mine', [timestamp]) 33 | } 34 | 35 | const increaseTime = async (seconds) => { 36 | await send('evm_increaseTime', [seconds]) 37 | } 38 | 39 | const minerStop = async () => { 40 | await send('miner_stop', []) 41 | } 42 | 43 | const minerStart = async () => { 44 | await send('miner_start', []) 45 | } 46 | 47 | module.exports = { 48 | takeSnapshot, 49 | revertSnapshot, 50 | mineBlock, 51 | minerStop, 52 | minerStart, 53 | increaseTime, 54 | traceTransaction, 55 | } 56 | -------------------------------------------------------------------------------- /test/airdrop.test.js: -------------------------------------------------------------------------------- 1 | /* global artifacts, web3, contract */ 2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should() 3 | 4 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper') 5 | const Airdrop = artifacts.require('./AirdropMock.sol') 6 | const Torn = artifacts.require('./TORNMock.sol') 7 | const { cap } = require('../config').torn 8 | const { toBN, toWei } = require('web3-utils') 9 | const RLP = require('rlp') 10 | 11 | async function getNextAddr(sender, offset = 0) { 12 | const nonce = await web3.eth.getTransactionCount(sender) 13 | return ( 14 | '0x' + 15 | web3.utils 16 | .sha3(RLP.encode([sender, Number(nonce) + Number(offset)])) 17 | .slice(12) 18 | .substring(14) 19 | ) 20 | } 21 | 22 | async function deploySefldestruct(contract, args, deployerPK) { 23 | const c = new web3.eth.Contract(contract.abi) 24 | const data = c 25 | .deploy({ 26 | data: contract.bytecode, 27 | arguments: args, 28 | }) 29 | .encodeABI() 30 | const signed = await web3.eth.accounts.signTransaction( 31 | { 32 | gas: 5e6, 33 | gasPrice: toWei('1', 'gwei'), 34 | data, 35 | }, 36 | deployerPK, 37 | ) 38 | await web3.eth.sendSignedTransaction(signed.rawTransaction) 39 | } 40 | 41 | contract('Airdrop', (accounts) => { 42 | let torn 43 | let snapshotId 44 | const airdropDeployer = accounts[8] 45 | const deployerPK = '0x0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4' 46 | const recipient1 = accounts[2] 47 | const recipient2 = accounts[3] 48 | let half = toBN(cap).div(toBN(2)).toString() 49 | 50 | before(async () => { 51 | const newAddr = await getNextAddr(airdropDeployer) 52 | torn = await Torn.new(accounts[0], 0, [{ to: newAddr, amount: cap }]) 53 | snapshotId = await takeSnapshot() 54 | }) 55 | describe('#airdrop', () => { 56 | it('should work', async () => { 57 | // web3 throws when it tried to deploy a contract with selfdestruct() in constructor 58 | await deploySefldestruct( 59 | Airdrop, 60 | [ 61 | torn.address, 62 | [ 63 | { to: recipient1, amount: half }, 64 | { to: recipient2, amount: half }, 65 | ], 66 | ], 67 | deployerPK, 68 | ) 69 | 70 | const bal1 = await torn.balanceOf(recipient1) 71 | const bal2 = await torn.balanceOf(recipient2) 72 | 73 | bal1.should.eq.BN(toBN(half)) 74 | bal2.should.eq.BN(toBN(half)) 75 | }) 76 | 77 | // todo: how do we get the same deployed address without create2? 78 | // it('should throw on second attempt', async () => { 79 | // await Airdrop.new(torn.address, [accounts[1], accounts[2]], [half, half], { from: airdropDeployer }) 80 | // }) 81 | }) 82 | 83 | afterEach(async () => { 84 | await revertSnapshot(snapshotId.result) 85 | // eslint-disable-next-line require-atomic-updates 86 | snapshotId = await takeSnapshot() 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/torn.test.js: -------------------------------------------------------------------------------- 1 | /* global artifacts, web3, contract */ 2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should() 3 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper') 4 | const { PermitSigner } = require('../lib/Permit') 5 | const { toBN, BN } = require('web3-utils') 6 | 7 | const Torn = artifacts.require('./TORNMock.sol') 8 | 9 | contract('Torn', (accounts) => { 10 | let torn 11 | const governance = accounts[3] 12 | const mining = accounts[4] 13 | const airdrop = accounts[5] 14 | let snapshotId 15 | const owner = accounts[0] 16 | const ownerPrivateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3' 17 | const spender = accounts[1] 18 | // eslint-disable-next-line no-unused-vars 19 | const spenderPrivateKey = '0xae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f' 20 | // eslint-disable-next-line no-unused-vars 21 | const recipient = accounts[2] 22 | // eslint-disable-next-line no-unused-vars 23 | const recipientPrivateKey = '0x0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1' 24 | const value = toBN(10 ** 18) 25 | let domain 26 | let chainId 27 | const cap = toBN(10000000).mul(toBN(10 ** 18)) 28 | let currentTime 29 | const thirtyDays = 30 * 24 * 3600 30 | before(async () => { 31 | chainId = await web3.eth.net.getId() 32 | torn = await Torn.new(governance, thirtyDays, [ 33 | { to: mining, amount: '0' }, 34 | { to: airdrop, amount: cap.toString() }, 35 | ]) 36 | currentTime = await torn.blockTimestamp() 37 | await torn.transfer(owner, cap.div(toBN(2)), { from: airdrop }) 38 | await torn.setChainId(chainId) 39 | await torn.setFakeTimestamp(currentTime) 40 | const blockTimestamp = await torn.blockTimestamp() 41 | blockTimestamp.should.be.eq.BN(toBN(currentTime)) 42 | domain = { 43 | name: await torn.name(), 44 | version: '1', 45 | chainId, 46 | verifyingContract: torn.address, 47 | } 48 | 49 | snapshotId = await takeSnapshot() 50 | }) 51 | 52 | describe('#constructor', () => { 53 | it('transfers ownership to governance', async () => { 54 | const ownerFromContract = await torn.governance() 55 | ownerFromContract.should.be.equal(governance) 56 | ;(await torn.allowedTransferee(governance)).should.be.true 57 | ;(await torn.allowedTransferee(mining)).should.be.true 58 | ;(await torn.allowedTransferee(airdrop)).should.be.true 59 | ;(await torn.allowedTransferee(owner)).should.be.false 60 | }) 61 | }) 62 | 63 | describe('pausable', () => { 64 | it('transfers disabled by default', async () => { 65 | await torn.transfer(accounts[1], 1, { from: spender }).should.be.rejectedWith('TORN: paused') 66 | }) 67 | 68 | it('can only transfer to governance and mining', async () => { 69 | await torn.transfer(governance, 1).should.be.fulfilled 70 | await torn.transfer(mining, 1).should.be.fulfilled 71 | await torn.transfer(accounts[5], 1, { from: mining }).should.be.fulfilled 72 | }) 73 | 74 | it('can transfer after governace decision', async () => { 75 | await torn.transfer(mining, 10).should.be.fulfilled 76 | await torn.transfer(recipient, 5, { from: mining }).should.be.fulfilled 77 | 78 | await torn.transfer(accounts[9], 1, { from: recipient }).should.be.rejectedWith('TORN: paused') 79 | await torn 80 | .changeTransferability(true, { from: governance }) 81 | .should.be.rejectedWith('TORN: cannot change transferability yet') 82 | await torn.setFakeTimestamp(currentTime + thirtyDays + 1) 83 | await torn.changeTransferability(true, { from: governance }) 84 | await torn.transfer(accounts[9], 1, { from: recipient }) 85 | 86 | const balance = await torn.balanceOf(accounts[9]) 87 | balance.should.be.eq.BN(toBN(1)) 88 | }) 89 | }) 90 | 91 | describe('#permit', () => { 92 | it('permitSigner class should work', async () => { 93 | const args = { 94 | owner, 95 | spender, 96 | value, 97 | nonce: 0, 98 | deadline: new BN('123123123123123'), 99 | } 100 | 101 | const permitSigner = new PermitSigner(domain, args) 102 | // const message = permitSigner.getPayload() 103 | // console.log('message', JSON.stringify(message)); 104 | 105 | // Generate the signature in place 106 | const privateKey = '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c' 107 | const address = '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b' 108 | const signature = await permitSigner.getSignature(privateKey) 109 | const signer = await permitSigner.getSignerAddress(args, signature.hex) 110 | address.should.be.equal(signer) 111 | }) 112 | 113 | it('calls approve if signature is valid', async () => { 114 | const chainIdFromContract = await torn.chainId() 115 | chainIdFromContract.should.be.eq.BN(new BN(domain.chainId)) 116 | const args = { 117 | owner, 118 | spender, 119 | value, 120 | nonce: 0, 121 | deadline: new BN(currentTime + thirtyDays), 122 | } 123 | const permitSigner = new PermitSigner(domain, args) 124 | const signature = await permitSigner.getSignature(ownerPrivateKey) 125 | const signer = await permitSigner.getSignerAddress(args, signature.hex) 126 | signer.should.be.equal(owner) 127 | 128 | const allowanceBefore = await torn.allowance(owner, spender) 129 | await torn.permit( 130 | args.owner, 131 | args.spender, 132 | args.value.toString(), 133 | args.deadline.toString(), 134 | signature.v, 135 | signature.r, 136 | signature.s, 137 | { from: owner }, 138 | ) 139 | const allowanceAfter = await torn.allowance(owner, spender) 140 | 141 | allowanceAfter.should.be.eq.BN(toBN(allowanceBefore).add(args.value)) 142 | }) 143 | it('reverts if signature is corrupted', async () => { 144 | const args = { 145 | owner, 146 | spender, 147 | value, 148 | nonce: 0, 149 | deadline: new BN(currentTime + thirtyDays), 150 | } 151 | const permitSigner = new PermitSigner(domain, args) 152 | const signature = await permitSigner.getSignature(ownerPrivateKey) 153 | signature.r = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' 154 | const allowanceBefore = await torn.allowance(owner, spender) 155 | await torn 156 | .permit( 157 | args.owner, 158 | args.spender, 159 | args.value.toString(), 160 | args.deadline.toString(), 161 | signature.v, 162 | signature.r, 163 | signature.s, 164 | { from: owner }, 165 | ) 166 | .should.be.rejectedWith('ECDSA: invalid signature') 167 | const allowanceAfter = await torn.allowance(owner, spender) 168 | 169 | allowanceAfter.should.be.eq.BN(allowanceBefore) 170 | }) 171 | it('reverts if signature is expired', async () => { 172 | const args = { 173 | owner, 174 | spender, 175 | value, 176 | nonce: 0, 177 | deadline: new BN('1593388800'), // 06/29/2020 @ 12:00am (UTC) 178 | } 179 | const permitSigner = new PermitSigner(domain, args) 180 | const signature = await permitSigner.getSignature(ownerPrivateKey) 181 | const allowanceBefore = await torn.allowance(owner, spender) 182 | await torn 183 | .permit( 184 | args.owner, 185 | args.spender, 186 | args.value.toString(), 187 | args.deadline.toString(), 188 | signature.v, 189 | signature.r, 190 | signature.s, 191 | { from: owner }, 192 | ) 193 | .should.be.rejectedWith('ERC20Permit: expired deadline') 194 | const allowanceAfter = await torn.allowance(owner, spender) 195 | 196 | allowanceAfter.should.be.eq.BN(BN(allowanceBefore)) 197 | }) 198 | }) 199 | 200 | afterEach(async () => { 201 | await revertSnapshot(snapshotId.result) 202 | // eslint-disable-next-line require-atomic-updates 203 | snapshotId = await takeSnapshot() 204 | }) 205 | }) 206 | -------------------------------------------------------------------------------- /test/vesting.test.js: -------------------------------------------------------------------------------- 1 | /* global artifacts, web3, contract */ 2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should() 3 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper') 4 | const { toBN } = require('web3-utils') 5 | const RLP = require('rlp') 6 | 7 | const Torn = artifacts.require('./TORNMock.sol') 8 | const Vesting = artifacts.require('./VestingMock.sol') 9 | const duration = { 10 | seconds: function (val) { 11 | return val 12 | }, 13 | minutes: function (val) { 14 | return val * this.seconds(60) 15 | }, 16 | hours: function (val) { 17 | return val * this.minutes(60) 18 | }, 19 | days: function (val) { 20 | return val * this.hours(24) 21 | }, 22 | weeks: function (val) { 23 | return val * this.days(7) 24 | }, 25 | years: function (val) { 26 | return val * this.days(365) 27 | }, 28 | } 29 | 30 | const MONTH = toBN(duration.days(30)) 31 | 32 | async function getNextAddr(sender, offset = 0) { 33 | const nonce = await web3.eth.getTransactionCount(sender) 34 | return ( 35 | '0x' + 36 | web3.utils 37 | .sha3(RLP.encode([sender, Number(nonce) + Number(offset)])) 38 | .slice(12) 39 | .substring(14) 40 | ) 41 | } 42 | 43 | contract('Vesting', (accounts) => { 44 | let torn 45 | let vesting 46 | let snapshotId 47 | // const owner = accounts[0] 48 | // const ownerPrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' 49 | const recipient = accounts[1] 50 | const governance = accounts[8] 51 | const testTimestamp = toBN(1584230400) // 03/15/2020 @ 12:00am (UTC) 52 | const startTimestamp = toBN(1586908800) // 04/15/2020 @ 12:00am (UTC) 53 | const cliffInMonths = toBN(12) 54 | const durationInMonths = toBN(36) 55 | const cap = toBN(10000000).mul(toBN(10 ** 18)) 56 | 57 | before(async () => { 58 | const vestingExpectedAddr = await getNextAddr(accounts[0], 1) 59 | const thirtyDays = 30 * 24 * 3600 60 | torn = await Torn.new(governance, thirtyDays, [{ to: vestingExpectedAddr, amount: cap.toString() }]) 61 | vesting = await Vesting.new(torn.address, recipient, startTimestamp, cliffInMonths, durationInMonths) 62 | await vesting.setFakeTimestamp(testTimestamp) 63 | const blockTimestamp = await vesting.blockTimestamp() 64 | blockTimestamp.should.be.eq.BN(testTimestamp) 65 | snapshotId = await takeSnapshot() 66 | }) 67 | 68 | describe('#constructor', () => { 69 | it('should be initialized', async () => { 70 | const startFromContract = await vesting.startTimestamp() 71 | startFromContract.should.be.eq.BN(startTimestamp) 72 | const beneficiaryFromContract = await vesting.beneficiary() 73 | beneficiaryFromContract.should.be.eq.BN(recipient) 74 | const cliffInMonthsFromContract = await vesting.cliffInMonths() 75 | cliffInMonthsFromContract.should.be.eq.BN(cliffInMonths) 76 | const durationInMonthsFromContract = await vesting.durationInMonths() 77 | durationInMonthsFromContract.should.be.eq.BN(durationInMonths) 78 | const balance = await torn.balanceOf(vesting.address) 79 | balance.should.be.eq.BN(cap) 80 | }) 81 | }) 82 | 83 | describe('#release', () => { 84 | it('should reject if time has not come', async () => { 85 | await vesting.release().should.be.rejectedWith('No tokens to release') 86 | await vesting.release({ from: recipient }).should.be.rejectedWith('No tokens to release') 87 | 88 | await vesting.setFakeTimestamp(startTimestamp) 89 | 90 | await vesting.release().should.be.rejectedWith('No tokens to release') 91 | await vesting.release({ from: recipient }).should.be.rejectedWith('No tokens to release') 92 | 93 | const rightBeforeCliff = startTimestamp.add(MONTH.mul(toBN(12))).sub(toBN(duration.days(1))) 94 | await vesting.setFakeTimestamp(rightBeforeCliff) 95 | 96 | await vesting.release().should.be.rejectedWith('No tokens to release') 97 | await vesting.release({ from: recipient }).should.be.rejectedWith('No tokens to release') 98 | }) 99 | 100 | it('should work if time has come', async () => { 101 | const cliff = startTimestamp.add(MONTH.mul(toBN(12))) 102 | await vesting.setFakeTimestamp(cliff) 103 | 104 | let balanceBefore = await torn.balanceOf(recipient) 105 | await vesting.release() 106 | let balanceAfter = await torn.balanceOf(recipient) 107 | 108 | const monthAfterCliff = cliff.add(MONTH) 109 | await vesting.setFakeTimestamp(monthAfterCliff) 110 | 111 | balanceBefore = await torn.balanceOf(recipient) 112 | await vesting.release() 113 | balanceAfter = await torn.balanceOf(recipient) 114 | balanceAfter.should.be.eq.BN(balanceBefore.add(cap.divRound(toBN(36)))) 115 | 116 | await vesting.release().should.be.rejectedWith('No tokens to release') 117 | const monthAfterCliffPlusWeek = monthAfterCliff.add(toBN(duration.weeks(1))) 118 | await vesting.setFakeTimestamp(monthAfterCliffPlusWeek) 119 | await vesting.release().should.be.rejectedWith('No tokens to release') 120 | 121 | const yearAfterCliff = cliff.add(MONTH.mul(toBN(12))) 122 | await vesting.setFakeTimestamp(yearAfterCliff) 123 | 124 | balanceBefore = await torn.balanceOf(recipient) 125 | await vesting.release() 126 | balanceAfter = await torn.balanceOf(recipient) 127 | balanceAfter.should.be.eq.BN(balanceBefore.add(cap.divRound(toBN(36)).mul(toBN(11))).sub(toBN(3))) // -3 wei because of round error 128 | 129 | const atTheEnd = cliff.add(MONTH.mul(toBN(24))) 130 | await vesting.setFakeTimestamp(atTheEnd) 131 | 132 | balanceBefore = await torn.balanceOf(recipient) 133 | await vesting.release() 134 | balanceAfter = await torn.balanceOf(recipient) 135 | balanceAfter.should.be.eq.BN(cap) 136 | }) 137 | }) 138 | 139 | afterEach(async () => { 140 | await revertSnapshot(snapshotId.result) 141 | // eslint-disable-next-line require-atomic-updates 142 | snapshotId = await takeSnapshot() 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /test/voucher.test.js: -------------------------------------------------------------------------------- 1 | /* global artifacts, web3, contract */ 2 | require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should() 3 | const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper') 4 | const { toBN } = require('web3-utils') 5 | const RLP = require('rlp') 6 | 7 | const Torn = artifacts.require('./TORNMock.sol') 8 | const Voucher = artifacts.require('./VoucherMock.sol') 9 | 10 | async function getNextAddr(sender, offset = 0) { 11 | const nonce = await web3.eth.getTransactionCount(sender) 12 | return ( 13 | '0x' + 14 | web3.utils 15 | .sha3(RLP.encode([sender, Number(nonce) + Number(offset)])) 16 | .slice(12) 17 | .substring(14) 18 | ) 19 | } 20 | 21 | contract('Voucher', (accounts) => { 22 | let torn 23 | let voucher 24 | let snapshotId 25 | // const owner = accounts[0] 26 | // const ownerPrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' 27 | const recipient = accounts[1] 28 | const governance = accounts[8] 29 | let startTimestamp 30 | const duration = toBN(60 * 60 * 24 * 365) 31 | const cap = toBN(10000000).mul(toBN(10 ** 18)) 32 | 33 | before(async () => { 34 | const voucherExpectedAddr = await getNextAddr(accounts[0], 1) 35 | const thirtyDays = 30 * 24 * 3600 36 | torn = await Torn.new(governance, thirtyDays, [{ to: voucherExpectedAddr, amount: cap.toString() }]) 37 | voucher = await Voucher.new(torn.address, governance, duration, [ 38 | { to: accounts[0], amount: cap.toString() }, 39 | ]) 40 | 41 | startTimestamp = await voucher.blockTimestamp() 42 | await voucher.setFakeTimestamp(startTimestamp) 43 | const blockTimestamp = await voucher.blockTimestamp() 44 | blockTimestamp.should.be.eq.BN(startTimestamp) 45 | 46 | await voucher.transfer(recipient, cap.div(toBN(10))) 47 | 48 | snapshotId = await takeSnapshot() 49 | }) 50 | 51 | describe('#constructor', () => { 52 | it('should be initialized', async () => { 53 | const expiresAt = await voucher.expiresAt() 54 | expiresAt.should.be.eq.BN(startTimestamp.add(duration)) 55 | 56 | const balance = await torn.balanceOf(voucher.address) 57 | balance.should.be.eq.BN(cap) 58 | 59 | const vTORNRecipientBalance = await voucher.balanceOf(recipient) 60 | vTORNRecipientBalance.should.be.eq.BN(cap.div(toBN(10))) 61 | }) 62 | }) 63 | 64 | describe('#redeem', () => { 65 | it('should work', async () => { 66 | const vTORNRecipientBalanceBefore = await voucher.balanceOf(recipient) 67 | const TORNRecipientBalanceBefore = await torn.balanceOf(recipient) 68 | await voucher.redeem({ from: recipient }) 69 | const vTORNRecipientBalanceAfter = await voucher.balanceOf(recipient) 70 | const TORNRecipientBalanceAfter = await torn.balanceOf(recipient) 71 | 72 | vTORNRecipientBalanceAfter.should.be.eq.BN(toBN(0)) 73 | TORNRecipientBalanceBefore.should.be.eq.BN(toBN(0)) 74 | TORNRecipientBalanceAfter.should.be.eq.BN(vTORNRecipientBalanceBefore) 75 | }) 76 | 77 | it('can redeem if time has passed', async () => { 78 | await voucher.redeem({ from: recipient }) 79 | 80 | const expiresAt = await voucher.expiresAt() 81 | await voucher.setFakeTimestamp(expiresAt) 82 | 83 | await voucher.redeem({ from: recipient }).should.be.rejectedWith('Airdrop redeem period has ended') 84 | }) 85 | }) 86 | 87 | describe('#rescueExpiredTokens', () => { 88 | it('should not work if time has not passed', async () => { 89 | await voucher.rescueExpiredTokens().should.be.rejectedWith('Airdrop redeem period has not ended yet') 90 | }) 91 | 92 | it('should work if time has passed', async () => { 93 | await voucher.redeem({ from: recipient }) 94 | 95 | const expiresAt = await voucher.expiresAt() 96 | await voucher.setFakeTimestamp(expiresAt) 97 | 98 | const balanceBefore = await torn.balanceOf(governance) 99 | const voucherBalanceBefore = await torn.balanceOf(voucher.address) 100 | await voucher.rescueExpiredTokens() 101 | const balanceAfter = await torn.balanceOf(governance) 102 | balanceAfter.should.be.eq.BN(balanceBefore.add(voucherBalanceBefore)) 103 | }) 104 | }) 105 | 106 | describe('#pause', () => { 107 | it('should be paused', async () => { 108 | const amount = await voucher.balanceOf(recipient) 109 | await voucher 110 | .transfer(accounts[4], amount, { from: recipient }) 111 | .should.be.rejectedWith('ERC20: transfer is not allowed') 112 | }) 113 | }) 114 | 115 | afterEach(async () => { 116 | await revertSnapshot(snapshotId.result) 117 | // eslint-disable-next-line require-atomic-updates 118 | snapshotId = await takeSnapshot() 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Uncommenting the defaults below 3 | // provides for an easier quick-start with Ganache. 4 | // You can also follow this format for other networks; 5 | // see 6 | // for more details on how to specify configuration options! 7 | // 8 | networks: { 9 | // development: { 10 | // host: '127.0.0.1', 11 | // port: 8545, 12 | // network_id: '*', 13 | // }, 14 | // test: { 15 | // host: '127.0.0.1', 16 | // port: 8544, 17 | // network_id: '*', 18 | // }, 19 | coverage: { 20 | host: 'localhost', 21 | network_id: '*', 22 | port: 8554, // <-- If you change this, also set the port option in .solcover.js. 23 | gas: 0xfffffffffff, // <-- Use this high gas value 24 | gasPrice: 0x01, // <-- Use this low gas price 25 | }, 26 | }, 27 | compilers: { 28 | solc: { 29 | version: '0.6.12', 30 | settings: { 31 | optimizer: { 32 | enabled: true, 33 | runs: 200, 34 | }, 35 | }, 36 | }, 37 | }, 38 | plugins: ['truffle-plugin-verify', 'solidity-coverage'], 39 | } 40 | --------------------------------------------------------------------------------