This commit is contained in:
poma 2020-12-15 18:05:35 +03:00
commit ed68304596
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
35 changed files with 7978 additions and 0 deletions

9
.editorconfig Normal file
View 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
View File

@ -0,0 +1,2 @@
PRIVATE_KEY=
INFURA_TOKEN=97c8bf358b9942a9853fab1ba93dc5b3

26
.eslintrc Normal file
View 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
View File

@ -0,0 +1 @@
*.sol linguist-language=Solidity

29
.github/workflows/build.yml vendored Normal file
View 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
View File

@ -0,0 +1,6 @@
node_modules
coverage
coverage.json
.DS_Store
build
.env

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
12

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
.vscode
build
scripts
contracts/ECDSA.sol

16
.prettierrc Normal file
View 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
View File

@ -0,0 +1,13 @@
{
"extends": "solhint:recommended",
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 110
}
],
"quotes": ["error", "double"]
},
"plugins": ["prettier"]
}

22
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
}

View 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)));
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'],
}

6128
yarn.lock Normal file

File diff suppressed because it is too large Load Diff