mirror of
https://github.com/tornadocash/torn-token.git
synced 2024-12-26 22:57:55 +01:00
initial
This commit is contained in:
commit
ed68304596
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
PRIVATE_KEY=
|
||||
INFURA_TOKEN=97c8bf358b9942a9853fab1ba93dc5b3
|
26
.eslintrc
Normal file
26
.eslintrc
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"require-await": "error"
|
||||
}
|
||||
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sol linguist-language=Solidity
|
29
.github/workflows/build.yml
vendored
Normal file
29
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: yarn install
|
||||
# Disabled until the repo is public because of performance issues
|
||||
# - run: yarn test
|
||||
- run: yarn lint
|
||||
- name: Telegram Failure Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
if: failure()
|
||||
with:
|
||||
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
coverage
|
||||
coverage.json
|
||||
.DS_Store
|
||||
build
|
||||
.env
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
.vscode
|
||||
build
|
||||
scripts
|
||||
contracts/ECDSA.sol
|
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"printWidth": 110,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"printWidth": 130
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
13
.solhint.json
Normal file
13
.solhint.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "solhint:recommended",
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 110
|
||||
}
|
||||
],
|
||||
"quotes": ["error", "double"]
|
||||
},
|
||||
"plugins": ["prettier"]
|
||||
}
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -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.
|
||||
|
43
README.md
Normal file
43
README.md
Normal file
@ -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`
|
226
config.js
Normal file
226
config.js
Normal file
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
23
contracts/Airdrop.sol
Normal file
23
contracts/Airdrop.sol
Normal file
@ -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));
|
||||
}
|
||||
}
|
93
contracts/ECDSA.sol
Normal file
93
contracts/ECDSA.sol
Normal file
@ -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));
|
||||
}
|
||||
}
|
35
contracts/ENS.sol
Normal file
35
contracts/ENS.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
106
contracts/ERC20Permit.sol
Normal file
106
contracts/ERC20Permit.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
110
contracts/TORN.sol
Normal file
110
contracts/TORN.sol
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
105
contracts/Vesting.sol
Normal file
105
contracts/Vesting.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
66
contracts/Voucher.sol
Normal file
66
contracts/Voucher.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
13
contracts/mocks/AirdropMock.sol
Normal file
13
contracts/mocks/AirdropMock.sol
Normal file
@ -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)));
|
||||
}
|
||||
}
|
32
contracts/mocks/ENSMock.sol
Normal file
32
contracts/mocks/ENSMock.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
32
contracts/mocks/TORNMock.sol
Normal file
32
contracts/mocks/TORNMock.sol
Normal file
@ -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();
|
||||
}
|
||||
}
|
14
contracts/mocks/Timestamp.sol
Normal file
14
contracts/mocks/Timestamp.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
25
contracts/mocks/VestingMock.sol
Normal file
25
contracts/mocks/VestingMock.sol
Normal file
@ -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();
|
||||
}
|
||||
}
|
25
contracts/mocks/VoucherMock.sol
Normal file
25
contracts/mocks/VoucherMock.sol
Normal file
@ -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();
|
||||
}
|
||||
}
|
43
lib/Permit.js
Normal file
43
lib/Permit.js
Normal file
@ -0,0 +1,43 @@
|
||||
const { EIP712Signer } = require('@ticket721/e712')
|
||||
|
||||
const Permit = [
|
||||
{ name: 'owner', type: 'address' },
|
||||
{ name: 'spender', type: 'address' },
|
||||
{ name: 'value', type: 'uint256' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
]
|
||||
|
||||
class PermitSigner extends EIP712Signer {
|
||||
constructor(_domain, _permitArgs) {
|
||||
super(_domain, ['Permit', Permit])
|
||||
this.permitArgs = _permitArgs
|
||||
}
|
||||
|
||||
// Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
|
||||
setPermitInfo(_permitArgs) {
|
||||
this.permitArgs = _permitArgs
|
||||
}
|
||||
|
||||
getPayload() {
|
||||
return this.generatePayload(this.permitArgs, 'Permit')
|
||||
}
|
||||
|
||||
async getSignature(privateKey) {
|
||||
const payload = this.getPayload()
|
||||
const { hex, v, r, s } = await this.sign(privateKey, payload)
|
||||
return {
|
||||
hex,
|
||||
v,
|
||||
r: '0x' + r,
|
||||
s: '0x' + s,
|
||||
}
|
||||
}
|
||||
|
||||
getSignerAddress(permitArgs, signature) {
|
||||
const original_payload = this.generatePayload(permitArgs, 'Permit')
|
||||
return this.verify(original_payload, signature)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PermitSigner }
|
56
package.json
Normal file
56
package.json
Normal file
@ -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 <hello@tornado.cash>",
|
||||
"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"
|
||||
}
|
||||
}
|
55
scripts/ganacheHelper.js
Normal file
55
scripts/ganacheHelper.js
Normal file
@ -0,0 +1,55 @@
|
||||
// This module is used only for tests
|
||||
function send(method, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
web3.currentProvider.send(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method,
|
||||
params,
|
||||
},
|
||||
(err, res) => {
|
||||
return err ? reject(err) : resolve(res)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const takeSnapshot = async () => {
|
||||
return await send('evm_snapshot')
|
||||
}
|
||||
|
||||
const traceTransaction = async (tx) => {
|
||||
return await send('debug_traceTransaction', [tx, {}])
|
||||
}
|
||||
|
||||
const revertSnapshot = async (id) => {
|
||||
await send('evm_revert', [id])
|
||||
}
|
||||
|
||||
const mineBlock = async (timestamp) => {
|
||||
await send('evm_mine', [timestamp])
|
||||
}
|
||||
|
||||
const increaseTime = async (seconds) => {
|
||||
await send('evm_increaseTime', [seconds])
|
||||
}
|
||||
|
||||
const minerStop = async () => {
|
||||
await send('miner_stop', [])
|
||||
}
|
||||
|
||||
const minerStart = async () => {
|
||||
await send('miner_start', [])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
takeSnapshot,
|
||||
revertSnapshot,
|
||||
mineBlock,
|
||||
minerStop,
|
||||
minerStart,
|
||||
increaseTime,
|
||||
traceTransaction,
|
||||
}
|
88
test/airdrop.test.js
Normal file
88
test/airdrop.test.js
Normal file
@ -0,0 +1,88 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const Airdrop = artifacts.require('./AirdropMock.sol')
|
||||
const Torn = artifacts.require('./TORNMock.sol')
|
||||
const { cap } = require('../config').torn
|
||||
const { toBN, toWei } = require('web3-utils')
|
||||
const RLP = require('rlp')
|
||||
|
||||
async function getNextAddr(sender, offset = 0) {
|
||||
const nonce = await web3.eth.getTransactionCount(sender)
|
||||
return (
|
||||
'0x' +
|
||||
web3.utils
|
||||
.sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
|
||||
.slice(12)
|
||||
.substring(14)
|
||||
)
|
||||
}
|
||||
|
||||
async function deploySefldestruct(contract, args, deployerPK) {
|
||||
const c = new web3.eth.Contract(contract.abi)
|
||||
const data = c
|
||||
.deploy({
|
||||
data: contract.bytecode,
|
||||
arguments: args,
|
||||
})
|
||||
.encodeABI()
|
||||
const signed = await web3.eth.accounts.signTransaction(
|
||||
{
|
||||
gas: 5e6,
|
||||
gasPrice: toWei('1', 'gwei'),
|
||||
data,
|
||||
},
|
||||
deployerPK,
|
||||
)
|
||||
await web3.eth.sendSignedTransaction(signed.rawTransaction)
|
||||
}
|
||||
|
||||
contract('Airdrop', (accounts) => {
|
||||
let torn
|
||||
let snapshotId
|
||||
const airdropDeployer = accounts[8]
|
||||
const deployerPK = '0x0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4'
|
||||
const recipient1 = accounts[2]
|
||||
const recipient2 = accounts[3]
|
||||
let half = toBN(cap).div(toBN(2)).toString()
|
||||
|
||||
before(async () => {
|
||||
const newAddr = await getNextAddr(airdropDeployer)
|
||||
torn = await Torn.new(accounts[0], 0, [{ to: newAddr, amount: cap }])
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
describe('#airdrop', () => {
|
||||
it('should work', async () => {
|
||||
// web3 throws when it tried to deploy a contract with selfdestruct() in constructor
|
||||
await deploySefldestruct(
|
||||
Airdrop,
|
||||
[
|
||||
torn.address,
|
||||
[
|
||||
{ to: recipient1, amount: half },
|
||||
{ to: recipient2, amount: half },
|
||||
],
|
||||
],
|
||||
deployerPK,
|
||||
)
|
||||
|
||||
const bal1 = await torn.balanceOf(recipient1)
|
||||
const bal2 = await torn.balanceOf(recipient2)
|
||||
|
||||
bal1.should.eq.BN(toBN(half))
|
||||
bal2.should.eq.BN(toBN(half))
|
||||
})
|
||||
|
||||
// todo: how do we get the same deployed address without create2?
|
||||
// it('should throw on second attempt', async () => {
|
||||
// await Airdrop.new(torn.address, [accounts[1], accounts[2]], [half, half], { from: airdropDeployer })
|
||||
// })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
205
test/torn.test.js
Normal file
205
test/torn.test.js
Normal file
@ -0,0 +1,205 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const { PermitSigner } = require('../lib/Permit')
|
||||
const { toBN, BN } = require('web3-utils')
|
||||
|
||||
const Torn = artifacts.require('./TORNMock.sol')
|
||||
|
||||
contract('Torn', (accounts) => {
|
||||
let torn
|
||||
const governance = accounts[3]
|
||||
const mining = accounts[4]
|
||||
const airdrop = accounts[5]
|
||||
let snapshotId
|
||||
const owner = accounts[0]
|
||||
const ownerPrivateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
|
||||
const spender = accounts[1]
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const spenderPrivateKey = '0xae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const recipient = accounts[2]
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const recipientPrivateKey = '0x0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1'
|
||||
const value = toBN(10 ** 18)
|
||||
let domain
|
||||
let chainId
|
||||
const cap = toBN(10000000).mul(toBN(10 ** 18))
|
||||
let currentTime
|
||||
const thirtyDays = 30 * 24 * 3600
|
||||
before(async () => {
|
||||
chainId = await web3.eth.net.getId()
|
||||
torn = await Torn.new(governance, thirtyDays, [
|
||||
{ to: mining, amount: '0' },
|
||||
{ to: airdrop, amount: cap.toString() },
|
||||
])
|
||||
currentTime = await torn.blockTimestamp()
|
||||
await torn.transfer(owner, cap.div(toBN(2)), { from: airdrop })
|
||||
await torn.setChainId(chainId)
|
||||
await torn.setFakeTimestamp(currentTime)
|
||||
const blockTimestamp = await torn.blockTimestamp()
|
||||
blockTimestamp.should.be.eq.BN(toBN(currentTime))
|
||||
domain = {
|
||||
name: await torn.name(),
|
||||
version: '1',
|
||||
chainId,
|
||||
verifyingContract: torn.address,
|
||||
}
|
||||
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('transfers ownership to governance', async () => {
|
||||
const ownerFromContract = await torn.governance()
|
||||
ownerFromContract.should.be.equal(governance)
|
||||
;(await torn.allowedTransferee(governance)).should.be.true
|
||||
;(await torn.allowedTransferee(mining)).should.be.true
|
||||
;(await torn.allowedTransferee(airdrop)).should.be.true
|
||||
;(await torn.allowedTransferee(owner)).should.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('pausable', () => {
|
||||
it('transfers disabled by default', async () => {
|
||||
await torn.transfer(accounts[1], 1, { from: spender }).should.be.rejectedWith('TORN: paused')
|
||||
})
|
||||
|
||||
it('can only transfer to governance and mining', async () => {
|
||||
await torn.transfer(governance, 1).should.be.fulfilled
|
||||
await torn.transfer(mining, 1).should.be.fulfilled
|
||||
await torn.transfer(accounts[5], 1, { from: mining }).should.be.fulfilled
|
||||
})
|
||||
|
||||
it('can transfer after governace decision', async () => {
|
||||
await torn.transfer(mining, 10).should.be.fulfilled
|
||||
await torn.transfer(recipient, 5, { from: mining }).should.be.fulfilled
|
||||
|
||||
await torn.transfer(accounts[9], 1, { from: recipient }).should.be.rejectedWith('TORN: paused')
|
||||
await torn
|
||||
.changeTransferability(true, { from: governance })
|
||||
.should.be.rejectedWith('TORN: cannot change transferability yet')
|
||||
await torn.setFakeTimestamp(currentTime + thirtyDays + 1)
|
||||
await torn.changeTransferability(true, { from: governance })
|
||||
await torn.transfer(accounts[9], 1, { from: recipient })
|
||||
|
||||
const balance = await torn.balanceOf(accounts[9])
|
||||
balance.should.be.eq.BN(toBN(1))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#permit', () => {
|
||||
it('permitSigner class should work', async () => {
|
||||
const args = {
|
||||
owner,
|
||||
spender,
|
||||
value,
|
||||
nonce: 0,
|
||||
deadline: new BN('123123123123123'),
|
||||
}
|
||||
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
// const message = permitSigner.getPayload()
|
||||
// console.log('message', JSON.stringify(message));
|
||||
|
||||
// Generate the signature in place
|
||||
const privateKey = '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c'
|
||||
const address = '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b'
|
||||
const signature = await permitSigner.getSignature(privateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
address.should.be.equal(signer)
|
||||
})
|
||||
|
||||
it('calls approve if signature is valid', async () => {
|
||||
const chainIdFromContract = await torn.chainId()
|
||||
chainIdFromContract.should.be.eq.BN(new BN(domain.chainId))
|
||||
const args = {
|
||||
owner,
|
||||
spender,
|
||||
value,
|
||||
nonce: 0,
|
||||
deadline: new BN(currentTime + thirtyDays),
|
||||
}
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const signature = await permitSigner.getSignature(ownerPrivateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
signer.should.be.equal(owner)
|
||||
|
||||
const allowanceBefore = await torn.allowance(owner, spender)
|
||||
await torn.permit(
|
||||
args.owner,
|
||||
args.spender,
|
||||
args.value.toString(),
|
||||
args.deadline.toString(),
|
||||
signature.v,
|
||||
signature.r,
|
||||
signature.s,
|
||||
{ from: owner },
|
||||
)
|
||||
const allowanceAfter = await torn.allowance(owner, spender)
|
||||
|
||||
allowanceAfter.should.be.eq.BN(toBN(allowanceBefore).add(args.value))
|
||||
})
|
||||
it('reverts if signature is corrupted', async () => {
|
||||
const args = {
|
||||
owner,
|
||||
spender,
|
||||
value,
|
||||
nonce: 0,
|
||||
deadline: new BN(currentTime + thirtyDays),
|
||||
}
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const signature = await permitSigner.getSignature(ownerPrivateKey)
|
||||
signature.r = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||
const allowanceBefore = await torn.allowance(owner, spender)
|
||||
await torn
|
||||
.permit(
|
||||
args.owner,
|
||||
args.spender,
|
||||
args.value.toString(),
|
||||
args.deadline.toString(),
|
||||
signature.v,
|
||||
signature.r,
|
||||
signature.s,
|
||||
{ from: owner },
|
||||
)
|
||||
.should.be.rejectedWith('ECDSA: invalid signature')
|
||||
const allowanceAfter = await torn.allowance(owner, spender)
|
||||
|
||||
allowanceAfter.should.be.eq.BN(allowanceBefore)
|
||||
})
|
||||
it('reverts if signature is expired', async () => {
|
||||
const args = {
|
||||
owner,
|
||||
spender,
|
||||
value,
|
||||
nonce: 0,
|
||||
deadline: new BN('1593388800'), // 06/29/2020 @ 12:00am (UTC)
|
||||
}
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const signature = await permitSigner.getSignature(ownerPrivateKey)
|
||||
const allowanceBefore = await torn.allowance(owner, spender)
|
||||
await torn
|
||||
.permit(
|
||||
args.owner,
|
||||
args.spender,
|
||||
args.value.toString(),
|
||||
args.deadline.toString(),
|
||||
signature.v,
|
||||
signature.r,
|
||||
signature.s,
|
||||
{ from: owner },
|
||||
)
|
||||
.should.be.rejectedWith('ERC20Permit: expired deadline')
|
||||
const allowanceAfter = await torn.allowance(owner, spender)
|
||||
|
||||
allowanceAfter.should.be.eq.BN(BN(allowanceBefore))
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
144
test/vesting.test.js
Normal file
144
test/vesting.test.js
Normal file
@ -0,0 +1,144 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const { toBN } = require('web3-utils')
|
||||
const RLP = require('rlp')
|
||||
|
||||
const Torn = artifacts.require('./TORNMock.sol')
|
||||
const Vesting = artifacts.require('./VestingMock.sol')
|
||||
const duration = {
|
||||
seconds: function (val) {
|
||||
return val
|
||||
},
|
||||
minutes: function (val) {
|
||||
return val * this.seconds(60)
|
||||
},
|
||||
hours: function (val) {
|
||||
return val * this.minutes(60)
|
||||
},
|
||||
days: function (val) {
|
||||
return val * this.hours(24)
|
||||
},
|
||||
weeks: function (val) {
|
||||
return val * this.days(7)
|
||||
},
|
||||
years: function (val) {
|
||||
return val * this.days(365)
|
||||
},
|
||||
}
|
||||
|
||||
const MONTH = toBN(duration.days(30))
|
||||
|
||||
async function getNextAddr(sender, offset = 0) {
|
||||
const nonce = await web3.eth.getTransactionCount(sender)
|
||||
return (
|
||||
'0x' +
|
||||
web3.utils
|
||||
.sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
|
||||
.slice(12)
|
||||
.substring(14)
|
||||
)
|
||||
}
|
||||
|
||||
contract('Vesting', (accounts) => {
|
||||
let torn
|
||||
let vesting
|
||||
let snapshotId
|
||||
// const owner = accounts[0]
|
||||
// const ownerPrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d'
|
||||
const recipient = accounts[1]
|
||||
const governance = accounts[8]
|
||||
const testTimestamp = toBN(1584230400) // 03/15/2020 @ 12:00am (UTC)
|
||||
const startTimestamp = toBN(1586908800) // 04/15/2020 @ 12:00am (UTC)
|
||||
const cliffInMonths = toBN(12)
|
||||
const durationInMonths = toBN(36)
|
||||
const cap = toBN(10000000).mul(toBN(10 ** 18))
|
||||
|
||||
before(async () => {
|
||||
const vestingExpectedAddr = await getNextAddr(accounts[0], 1)
|
||||
const thirtyDays = 30 * 24 * 3600
|
||||
torn = await Torn.new(governance, thirtyDays, [{ to: vestingExpectedAddr, amount: cap.toString() }])
|
||||
vesting = await Vesting.new(torn.address, recipient, startTimestamp, cliffInMonths, durationInMonths)
|
||||
await vesting.setFakeTimestamp(testTimestamp)
|
||||
const blockTimestamp = await vesting.blockTimestamp()
|
||||
blockTimestamp.should.be.eq.BN(testTimestamp)
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should be initialized', async () => {
|
||||
const startFromContract = await vesting.startTimestamp()
|
||||
startFromContract.should.be.eq.BN(startTimestamp)
|
||||
const beneficiaryFromContract = await vesting.beneficiary()
|
||||
beneficiaryFromContract.should.be.eq.BN(recipient)
|
||||
const cliffInMonthsFromContract = await vesting.cliffInMonths()
|
||||
cliffInMonthsFromContract.should.be.eq.BN(cliffInMonths)
|
||||
const durationInMonthsFromContract = await vesting.durationInMonths()
|
||||
durationInMonthsFromContract.should.be.eq.BN(durationInMonths)
|
||||
const balance = await torn.balanceOf(vesting.address)
|
||||
balance.should.be.eq.BN(cap)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#release', () => {
|
||||
it('should reject if time has not come', async () => {
|
||||
await vesting.release().should.be.rejectedWith('No tokens to release')
|
||||
await vesting.release({ from: recipient }).should.be.rejectedWith('No tokens to release')
|
||||
|
||||
await vesting.setFakeTimestamp(startTimestamp)
|
||||
|
||||
await vesting.release().should.be.rejectedWith('No tokens to release')
|
||||
await vesting.release({ from: recipient }).should.be.rejectedWith('No tokens to release')
|
||||
|
||||
const rightBeforeCliff = startTimestamp.add(MONTH.mul(toBN(12))).sub(toBN(duration.days(1)))
|
||||
await vesting.setFakeTimestamp(rightBeforeCliff)
|
||||
|
||||
await vesting.release().should.be.rejectedWith('No tokens to release')
|
||||
await vesting.release({ from: recipient }).should.be.rejectedWith('No tokens to release')
|
||||
})
|
||||
|
||||
it('should work if time has come', async () => {
|
||||
const cliff = startTimestamp.add(MONTH.mul(toBN(12)))
|
||||
await vesting.setFakeTimestamp(cliff)
|
||||
|
||||
let balanceBefore = await torn.balanceOf(recipient)
|
||||
await vesting.release()
|
||||
let balanceAfter = await torn.balanceOf(recipient)
|
||||
|
||||
const monthAfterCliff = cliff.add(MONTH)
|
||||
await vesting.setFakeTimestamp(monthAfterCliff)
|
||||
|
||||
balanceBefore = await torn.balanceOf(recipient)
|
||||
await vesting.release()
|
||||
balanceAfter = await torn.balanceOf(recipient)
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(cap.divRound(toBN(36))))
|
||||
|
||||
await vesting.release().should.be.rejectedWith('No tokens to release')
|
||||
const monthAfterCliffPlusWeek = monthAfterCliff.add(toBN(duration.weeks(1)))
|
||||
await vesting.setFakeTimestamp(monthAfterCliffPlusWeek)
|
||||
await vesting.release().should.be.rejectedWith('No tokens to release')
|
||||
|
||||
const yearAfterCliff = cliff.add(MONTH.mul(toBN(12)))
|
||||
await vesting.setFakeTimestamp(yearAfterCliff)
|
||||
|
||||
balanceBefore = await torn.balanceOf(recipient)
|
||||
await vesting.release()
|
||||
balanceAfter = await torn.balanceOf(recipient)
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(cap.divRound(toBN(36)).mul(toBN(11))).sub(toBN(3))) // -3 wei because of round error
|
||||
|
||||
const atTheEnd = cliff.add(MONTH.mul(toBN(24)))
|
||||
await vesting.setFakeTimestamp(atTheEnd)
|
||||
|
||||
balanceBefore = await torn.balanceOf(recipient)
|
||||
await vesting.release()
|
||||
balanceAfter = await torn.balanceOf(recipient)
|
||||
balanceAfter.should.be.eq.BN(cap)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
120
test/voucher.test.js
Normal file
120
test/voucher.test.js
Normal file
@ -0,0 +1,120 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const { toBN } = require('web3-utils')
|
||||
const RLP = require('rlp')
|
||||
|
||||
const Torn = artifacts.require('./TORNMock.sol')
|
||||
const Voucher = artifacts.require('./VoucherMock.sol')
|
||||
|
||||
async function getNextAddr(sender, offset = 0) {
|
||||
const nonce = await web3.eth.getTransactionCount(sender)
|
||||
return (
|
||||
'0x' +
|
||||
web3.utils
|
||||
.sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
|
||||
.slice(12)
|
||||
.substring(14)
|
||||
)
|
||||
}
|
||||
|
||||
contract('Voucher', (accounts) => {
|
||||
let torn
|
||||
let voucher
|
||||
let snapshotId
|
||||
// const owner = accounts[0]
|
||||
// const ownerPrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d'
|
||||
const recipient = accounts[1]
|
||||
const governance = accounts[8]
|
||||
let startTimestamp
|
||||
const duration = toBN(60 * 60 * 24 * 365)
|
||||
const cap = toBN(10000000).mul(toBN(10 ** 18))
|
||||
|
||||
before(async () => {
|
||||
const voucherExpectedAddr = await getNextAddr(accounts[0], 1)
|
||||
const thirtyDays = 30 * 24 * 3600
|
||||
torn = await Torn.new(governance, thirtyDays, [{ to: voucherExpectedAddr, amount: cap.toString() }])
|
||||
voucher = await Voucher.new(torn.address, governance, duration, [
|
||||
{ to: accounts[0], amount: cap.toString() },
|
||||
])
|
||||
|
||||
startTimestamp = await voucher.blockTimestamp()
|
||||
await voucher.setFakeTimestamp(startTimestamp)
|
||||
const blockTimestamp = await voucher.blockTimestamp()
|
||||
blockTimestamp.should.be.eq.BN(startTimestamp)
|
||||
|
||||
await voucher.transfer(recipient, cap.div(toBN(10)))
|
||||
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should be initialized', async () => {
|
||||
const expiresAt = await voucher.expiresAt()
|
||||
expiresAt.should.be.eq.BN(startTimestamp.add(duration))
|
||||
|
||||
const balance = await torn.balanceOf(voucher.address)
|
||||
balance.should.be.eq.BN(cap)
|
||||
|
||||
const vTORNRecipientBalance = await voucher.balanceOf(recipient)
|
||||
vTORNRecipientBalance.should.be.eq.BN(cap.div(toBN(10)))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#redeem', () => {
|
||||
it('should work', async () => {
|
||||
const vTORNRecipientBalanceBefore = await voucher.balanceOf(recipient)
|
||||
const TORNRecipientBalanceBefore = await torn.balanceOf(recipient)
|
||||
await voucher.redeem({ from: recipient })
|
||||
const vTORNRecipientBalanceAfter = await voucher.balanceOf(recipient)
|
||||
const TORNRecipientBalanceAfter = await torn.balanceOf(recipient)
|
||||
|
||||
vTORNRecipientBalanceAfter.should.be.eq.BN(toBN(0))
|
||||
TORNRecipientBalanceBefore.should.be.eq.BN(toBN(0))
|
||||
TORNRecipientBalanceAfter.should.be.eq.BN(vTORNRecipientBalanceBefore)
|
||||
})
|
||||
|
||||
it('can redeem if time has passed', async () => {
|
||||
await voucher.redeem({ from: recipient })
|
||||
|
||||
const expiresAt = await voucher.expiresAt()
|
||||
await voucher.setFakeTimestamp(expiresAt)
|
||||
|
||||
await voucher.redeem({ from: recipient }).should.be.rejectedWith('Airdrop redeem period has ended')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#rescueExpiredTokens', () => {
|
||||
it('should not work if time has not passed', async () => {
|
||||
await voucher.rescueExpiredTokens().should.be.rejectedWith('Airdrop redeem period has not ended yet')
|
||||
})
|
||||
|
||||
it('should work if time has passed', async () => {
|
||||
await voucher.redeem({ from: recipient })
|
||||
|
||||
const expiresAt = await voucher.expiresAt()
|
||||
await voucher.setFakeTimestamp(expiresAt)
|
||||
|
||||
const balanceBefore = await torn.balanceOf(governance)
|
||||
const voucherBalanceBefore = await torn.balanceOf(voucher.address)
|
||||
await voucher.rescueExpiredTokens()
|
||||
const balanceAfter = await torn.balanceOf(governance)
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(voucherBalanceBefore))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#pause', () => {
|
||||
it('should be paused', async () => {
|
||||
const amount = await voucher.balanceOf(recipient)
|
||||
await voucher
|
||||
.transfer(accounts[4], amount, { from: recipient })
|
||||
.should.be.rejectedWith('ERC20: transfer is not allowed')
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
62
truffle.js
Normal file
62
truffle.js
Normal file
@ -0,0 +1,62 @@
|
||||
require('dotenv').config()
|
||||
const HDWalletProvider = require('truffle-hdwallet-provider')
|
||||
const utils = require('web3-utils')
|
||||
const { PRIVATE_KEY, INFURA_TOKEN } = process.env
|
||||
|
||||
module.exports = {
|
||||
// Uncommenting the defaults below
|
||||
// provides for an easier quick-start with Ganache.
|
||||
// You can also follow this format for other networks;
|
||||
// see <http://truffleframework.com/docs/advanced/configuration>
|
||||
// for more details on how to specify configuration options!
|
||||
//
|
||||
networks: {
|
||||
// development: {
|
||||
// host: '127.0.0.1',
|
||||
// port: 8545,
|
||||
// network_id: '*',
|
||||
// },
|
||||
// test: {
|
||||
// host: '127.0.0.1',
|
||||
// port: 8544,
|
||||
// network_id: '*',
|
||||
// },
|
||||
mainnet: {
|
||||
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://mainnet.infura.io/v3/${INFURA_TOKEN}`),
|
||||
network_id: 1,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('100', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
kovan: {
|
||||
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://kovan.infura.io/v3/${INFURA_TOKEN}`),
|
||||
network_id: 42,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('1', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
coverage: {
|
||||
host: 'localhost',
|
||||
network_id: '*',
|
||||
port: 8554, // <-- If you change this, also set the port option in .solcover.js.
|
||||
gas: 0xfffffffffff, // <-- Use this high gas value
|
||||
gasPrice: 0x01, // <-- Use this low gas price
|
||||
},
|
||||
},
|
||||
compilers: {
|
||||
solc: {
|
||||
version: '0.6.12',
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: ['truffle-plugin-verify', 'solidity-coverage'],
|
||||
}
|
Loading…
Reference in New Issue
Block a user