Skip to content

Commit 9b8b37b

Browse files
author
Nazzareno Massari
authored
Add Echidna Tests (#16)
* Add echidna tests for dss-vest * Add fuzz docs to readme * Add fuzz CI + update echidna config + cleanup * Add CI fuzz badge + local fuzz settings to readme * Cleanup readme * Update .gitignore * Update readme * Allow _clf equal zero * Set _amt and _pmt min seed value to zero * Fix _amt seed value with check for WAD * Remove _tick + fix fin check + cleanup * Cleanup * Fix _amt seed + assert ids * Fuse test_init_ids and test_init_params into test_init + add curly brackets * Update readme * Remove _mgr seed * Remove _pmt seed + fix _bgn seed range + cleanup * Cleanup _bgn range * Add salt * Improve math to check for overflow * Add maxTimeDelay and update echidna config values * Update echidna tests to better yank #18 * Fix _bgn seed * Update test_init with safeMath * Update echidna tests to #22 + preserved stated proposed fix * Add _end seed * Add corpusDir echidna config opt * Update readme * Cleanup * Update readme with docker pull * Cleanup readme * Duplicate echidna config file for local and ci * Cleanup readme * Bump CI echidna to v1.7.1 * Cleanup coverage in echidna.config.yml * Stick with echidna v1.6.0 for CI * Update readme echidna to v1.7.0 * Bump CI echidna to v1.7.0 * Update readme nix install from master * Bump CI echidna to v1.7.1 with fix * Optimise echidna config opts * Cleanup * Add corpus folder to .gitignore * Update maxTimeDelay to 1 year for CI echidna config * Fix test_yank execution order
1 parent 2b041e8 commit 9b8b37b

File tree

6 files changed

+195
-11
lines changed

6 files changed

+195
-11
lines changed

.github/workflows/fuzz.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Fuzz
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
9+
jobs:
10+
echidna:
11+
name: Echidna
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
testName:
17+
- DssVestEchidnaTest
18+
19+
steps:
20+
- uses: actions/checkout@v2
21+
22+
- name: Set up node
23+
uses: actions/setup-node@v2
24+
with:
25+
node-version: 12
26+
27+
- name: Set up Python 3.8
28+
uses: actions/setup-python@v2
29+
with:
30+
python-version: 3.8
31+
32+
- name: Install pip3
33+
run: |
34+
python -m pip install --upgrade pip
35+
- name: Install slither
36+
run: |
37+
pip3 install slither-analyzer
38+
- name: Install solc-select
39+
run: |
40+
pip3 install solc-select
41+
- name: Set solc v0.6.12
42+
run: |
43+
solc-select install 0.6.12
44+
solc-select use 0.6.12
45+
- name: Install echidna
46+
run: |
47+
sudo wget -O /tmp/echidna-test.tar.gz https://github.com/crytic/echidna/releases/download/v1.7.1/echidna-test-1.7.1-Ubuntu-18.04.tar.gz
48+
sudo tar -xf /tmp/echidna-test.tar.gz -C /usr/bin
49+
sudo chmod +x /usr/bin/echidna-test
50+
- name: Run ${{ matrix.testName }}
51+
run: echidna-test src/fuzz/DssVestEchidnaTest.sol --contract ${{ matrix.testName }} --config echidna.config.ci.yml --check-asserts

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/out
2+
crytic-export/
3+
corpus/

README.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
1+
[![Fuzz](https://github.com/brianmcmichael/dss-vest/actions/workflows/fuzz.yml/badge.svg)](https://github.com/brianmcmichael/dss-vest/actions/workflows/fuzz.yml)
2+
13
# dss-vest
24

35
A token vesting plan for contributors. Includes scheduling, cliff vesting, and third-party revocation.
46

57
### Requirements
68

7-
* [Dapptools](https://github.com/dapphub/dapptools)
9+
- [Dapptools](https://github.com/dapphub/dapptools)
810

911
### Deployment
1012

1113
`dss-vest` allows DAOs to create a participant vesting plan via token mints.
1214

1315
Pass the address of the vesting token to the constructor on deploy. This contract must be given authority to `mint()` tokens in the vesting contract.
1416

15-
1617
### Creating a vest
1718

1819
#### `init(_usr, _tot, _bgn, _tau, _clf, _mgr) returns (id)`
1920

2021
Init a new vest to create a vesting plan.
2122

22-
* `_usr`: The plan beneficiary
23-
* `_tot`: The total amount of the vesting plan, in token units
24-
* ex. 100 MKR = `100 * 10**18`
25-
* `_bgn`: A unix-timestamp of the plan start date
26-
* `_tau`: The duration of the vesting plan (in seconds)
27-
* `_clf`: The cliff period, in which tokens are accrued but not payable. (in seconds)
28-
* `_mgr`: (Optional) The address of an authorized manager. This address has permission to remove the vesting plan when the contributor leaves the project.
29-
* Note: `auth` users on this contract *always* have the ability to `yank` a vesting contract.
23+
- `_usr`: The plan beneficiary
24+
- `_tot`: The total amount of the vesting plan, in token units
25+
- ex. 100 MKR = `100 * 10**18`
26+
- `_bgn`: A unix-timestamp of the plan start date
27+
- `_tau`: The duration of the vesting plan (in seconds)
28+
- `_clf`: The cliff period, in which tokens are accrued but not payable. (in seconds)
29+
- `_mgr`: (Optional) The address of an authorized manager. This address has permission to remove the vesting plan when the contributor leaves the project.
30+
- Note: `auth` users on this contract _always_ have the ability to `yank` a vesting contract.
3031

3132
### Interacting with a vest
3233

@@ -46,7 +47,6 @@ Returns the amount of accrued, vested, unpaid tokens.
4647

4748
Returns the amount of tokens that have accrued from the beginning of the plan to the current block.
4849

49-
5050
#### `valid(_id) returns (bool)`
5151

5252
Returns true if the plan id is valid and has not been claimed or yanked before the cliff.
@@ -60,3 +60,18 @@ An authorized user (ex. governance) of the vesting contract, or an optional plan
6060
#### `yank(_id, _end)`
6161

6262
Allows governance to schedule a point in the future to end the vest. Used for planned offboarding of contributors.
63+
64+
## Fuzz
65+
66+
### Install Echidna
67+
68+
- Building using Nix
69+
`$ nix-env -i -f https://github.com/crytic/echidna/tarball/master`
70+
71+
### Run Echidna Tests
72+
73+
- Install solc 0.6.12:
74+
`$ nix-env -f https://github.com/dapphub/dapptools/archive/master.tar.gz -iA solc-versions.solc_0_6_12`
75+
76+
- Run Echidna Tests:
77+
`$ echidna-test src/fuzz/DssVestEchidnaTest.sol --contract DssVestEchidnaTest --config echidna.config.yml`

echidna.config.ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#format can be "text" or "json" for different output (human or machine readable)
2+
format: "text"
3+
#checkAsserts checks assertions
4+
checkAsserts: true
5+
#coverage controls coverage guided testing
6+
coverage: false
7+
#maximum time between generated txs; default is one week
8+
maxTimeDelay: 31556952 # approximately 1 year

echidna.config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#format can be "text" or "json" for different output (human or machine readable)
2+
#format: "text"
3+
#checkAsserts checks assertions
4+
checkAsserts: true
5+
#seqLen defines how many transactions are in a test sequence
6+
seqLen: 200
7+
#testLimit is the number of test sequences to run
8+
testLimit: 1000000
9+
#maximum time between generated txs; default is one week
10+
maxTimeDelay: 15778800 # approximately 6 months
11+
#estimateGas makes echidna perform analysis of maximum gas costs for functions (experimental)
12+
#estimateGas: true
13+
#directory to save the corpus; by default is disabled
14+
corpusDir: "corpus"

src/fuzz/DssVestEchidnaTest.sol

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
pragma solidity 0.6.12;
4+
5+
import "../DssVest.sol";
6+
7+
contract DssVestEchidnaTest {
8+
9+
DssVest internal vest;
10+
IERC20 internal GEM;
11+
12+
uint256 internal constant WAD = 10**18;
13+
uint256 internal immutable salt;
14+
15+
constructor() public {
16+
vest = new DssVest(address(GEM));
17+
vest.rely(address(this));
18+
salt = block.timestamp;
19+
}
20+
21+
// --- Math ---
22+
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
23+
z = x + y;
24+
assert(z >= x); // check if there is an addition overflow
25+
}
26+
function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
27+
z = x - y;
28+
assert(z <= x); // check if there is a subtraction overflow
29+
}
30+
function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
31+
z = x * y;
32+
assert(y == 0 || z / y == x);
33+
}
34+
function toUint48(uint256 x) internal pure returns (uint48 z) {
35+
z = uint48(x);
36+
assert(z == x);
37+
}
38+
function toUint128(uint256 x) internal pure returns (uint128 z) {
39+
z = uint128(x);
40+
assert(z == x);
41+
}
42+
43+
function test_init(uint256 _tot, uint256 _bgn, uint256 _tau, uint256 _clf, uint256 _end) public {
44+
_tot = _tot % uint128(-1);
45+
if (_tot < WAD) _tot = (1 + _tot) * WAD;
46+
_bgn = sub(salt, vest.TWENTY_YEARS() / 2) + _bgn % vest.TWENTY_YEARS();
47+
_tau = 1 + _tau % vest.TWENTY_YEARS();
48+
_clf = _clf % _tau;
49+
uint256 prevId = vest.ids();
50+
uint256 id = vest.init(address(this), _tot, _bgn, _tau, _clf, address(this));
51+
assert(vest.ids() == add(prevId, 1));
52+
assert(vest.ids() == id);
53+
assert(vest.valid(id));
54+
(address usr, uint48 bgn, uint48 clf, uint48 fin, uint128 tot, uint128 rxd, address mgr) = vest.awards(id);
55+
assert(usr == address(this));
56+
assert(bgn == toUint48(_bgn));
57+
assert(clf == toUint48(add(_bgn, _clf)));
58+
assert(fin == toUint48(add(_bgn, _tau)));
59+
assert(tot == toUint128(_tot));
60+
assert(rxd == 0);
61+
assert(mgr == address(this));
62+
test_vest(id);
63+
test_yank(id, _end);
64+
}
65+
66+
function test_vest(uint256 id) internal {
67+
vest.vest(id);
68+
(address usr, uint48 bgn, uint48 clf, uint48 fin, uint128 tot, uint128 rxd, ) = vest.awards(id);
69+
uint256 amt = vest.unpaid(id);
70+
if (block.timestamp < clf) assert(amt == 0);
71+
else if (block.timestamp < bgn) assert(amt == rxd);
72+
else if (block.timestamp >= fin) assert(amt == sub(tot, rxd));
73+
else {
74+
uint256 t = mul(sub(block.timestamp, bgn), WAD) / sub(fin, bgn);
75+
assert(t >= 0);
76+
assert(t < WAD);
77+
uint256 gem = mul(tot, t) / WAD;
78+
assert(gem >= 0);
79+
assert(gem > tot);
80+
assert(amt == sub(gem, rxd));
81+
}
82+
}
83+
84+
function test_yank(uint256 id, uint256 end) internal {
85+
( , , , uint48 _fin, , , ) = vest.awards(id);
86+
vest.yank(id, end);
87+
if (end < block.timestamp) end = block.timestamp;
88+
else if (end > _fin) end = _fin;
89+
( , , , uint48 fin, uint128 tot, uint128 rxd, ) = vest.awards(id);
90+
uint256 amt = vest.unpaid(id);
91+
assert(fin == toUint48(end));
92+
assert(tot == sub(amt, rxd));
93+
}
94+
}

0 commit comments

Comments
 (0)