From 4e04cec2607a6e59960ce8bd729666cfddc2f523 Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Thu, 28 Apr 2022 16:58:51 -0300 Subject: [PATCH] Add `vat.live` check to DssVestSuckable `pay` + Follow DSS Slot Pattern + Switch modifier order + others * Switch modifier order (#42) * Follow DSS Slot Pattern (#43) * Add `vat.live` check to DssVestSuckable `pay` (#48) * `kill` * tests(refactor): fetch from chainlog * rename to `cage` * tests(refactor): split interfaces for contract and tests + cleanups * tests(refactor): keep ds-token and gem/dai interfaces separated * suckable(breaker): add `vat.live` check in `pay` * echidna(suckable): add `mutlive` + `vat.live` revert case in `vest` and `vest_amt` * echidna(`v2.0.0`): update config and readme * certora(suckable): add `vat.live` revert case in `vest_revert` and `vest_amt_revert` * readme(suckable): document `vat.suck` circuit breaker * echidna(fix): extend `mutlock` behaviour * echidna(fix): address `add` warnings + remove math `sub` where unused * vest(errmsg): rename error messages to vest-specific contract * tests(refactor): replace `Award` with `DssVest.Award` * echidna(refactor): replace `Award` with `DssVest.Award` * echidna(fix): address math warnings + remove unused math * transferrable(errmsg): add missing error message in `pay` check * echidna(transferrable): update `vest` and `vest_amt` with transfer error message * readme(review): disable `vest()` * echidna(transferrable): fix `vest` and `vest_amt` catch error block * echidna(transferrable): remove transfer error message as won't be reached * echidna(transferrable): cleanup * echidna(mutations): disbale time-based fuzz mutations * tests(doc): add comment for `vat.sin(VOW)` check Co-authored-by: Nazzareno Massari Co-authored-by: Nazzareno Massari --- README.md | 10 +- certora/DssVestSuckable.spec | 117 +-- echidna.config.yml | 4 +- echidna/DssVestMintableEchidnaTest.sol | 73 +- echidna/DssVestSuckableEchidnaTest.sol | 127 ++- echidna/DssVestTransferrableEchidnaTest.sol | 71 +- src/DssVest.sol | 91 +- src/DssVest.t.sol | 951 +++++++++++++------- 8 files changed, 874 insertions(+), 570 deletions(-) diff --git a/README.md b/README.md index b4f28722..c4e4bf97 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ After deployment, governance must set the `cap` value using the `file` function. Pass the MCD [chainlog](https://github.com/makerdao/dss-chain-log) address to the constructor to set up the contract for scheduled Dai `suck`s. Note: this contract must be given authority to `suck()` Dai from the `vat`'s surplus buffer. +A `vat.live` check is introduced to disable `vest()` in the event of Emergency Shutdown (aka Global Settlement). + After deployment, governance must set the `cap` value using the `file` function. #### DssVestTransferrable @@ -105,13 +107,13 @@ Allows governance to schedule a point in the future to end the vest. Used for pl ### Install Echidna -- Install Echidna v1.7.3 +- Install Echidna v2.0.0 ``` - $ nix-env -i -f https://github.com/crytic/echidna/archive/v1.7.3.tar.gz + $ nix-env -i -f https://github.com/crytic/echidna/archive/v2.0.0.tar.gz ``` -- Install Echidna v1.7.3 via [echidnup](https://github.com/naszam/echidnup#installing) +- Install Echidna v2.0.0 via [echidnup](https://github.com/naszam/echidnup#installing) ``` - $ echidnup v1.7.3 + $ echidnup v2.0.0 ``` ### Local Dependencies diff --git a/certora/DssVestSuckable.spec b/certora/DssVestSuckable.spec index ffe235e0..9dbdc8c4 100644 --- a/certora/DssVestSuckable.spec +++ b/certora/DssVestSuckable.spec @@ -32,6 +32,7 @@ methods { vat.sin(address) returns (uint256) envfree vat.debt() returns (uint256) envfree vat.vice() returns (uint256) envfree + vat.live() returns (uint256) envfree dai.wards(address) returns (uint256) envfree dai.totalSupply() returns (uint256) envfree dai.balanceOf(address) returns (uint256) envfree @@ -421,6 +422,7 @@ rule vest_revert(uint256 _id) { uint256 usrBalance = dai.balanceOf(usr); uint256 supply = dai.totalSupply(); uint256 locked = lockedGhost(); + uint256 vatLive = vat.live(); address vow = chainlog.getAddress(e, 0x4d43445f564f5700000000000000000000000000000000000000000000000000); uint256 daiJoinLive = daiJoin.live(); uint256 wardVat = vat.wards(currentContract); @@ -458,19 +460,20 @@ rule vest_revert(uint256 _id) { bool revert6 = e.block.timestamp >= clf && e.block.timestamp >= bgn && e.block.timestamp < fin && fin == bgn; bool revert7 = e.block.timestamp >= clf && accruedAmt < rxd; bool revert8 = rxd + unpaidAmt > max_uint128; - bool revert9 = vow == 0; - bool revert10 = unpaidAmtRad > max_uint256; - bool revert11 = wardVat != 1; - bool revert12 = sinVow + unpaidAmtRad > max_uint256; - bool revert13 = vatDaiVest + unpaidAmtRad > max_uint256; - bool revert14 = vice + unpaidAmtRad > max_uint256; - bool revert15 = debt + unpaidAmtRad > max_uint256; - bool revert16 = daiJoinLive != 1; - bool revert17 = currentContract != daiJoin && canVestDaiJoin != 1; - bool revert18 = vatDaiDaiJoin + unpaidAmtRad > max_uint256; - bool revert19 = wardDai != 1; - bool revert20 = usrBalance + unpaidAmt > max_uint256; - bool revert21 = supply + unpaidAmt > max_uint256; + bool revert9 = vatLive != 1; + bool revert10 = vow == 0; + bool revert11 = unpaidAmtRad > max_uint256; + bool revert12 = wardVat != 1; + bool revert13 = sinVow + unpaidAmtRad > max_uint256; + bool revert14 = vatDaiVest + unpaidAmtRad > max_uint256; + bool revert15 = vice + unpaidAmtRad > max_uint256; + bool revert16 = debt + unpaidAmtRad > max_uint256; + bool revert17 = daiJoinLive != 1; + bool revert18 = currentContract != daiJoin && canVestDaiJoin != 1; + bool revert19 = vatDaiDaiJoin + unpaidAmtRad > max_uint256; + bool revert20 = wardDai != 1; + bool revert21 = usrBalance + unpaidAmt > max_uint256; + bool revert22 = supply + unpaidAmt > max_uint256; assert(revert1 => lastReverted, "Sending ETH did not revert"); assert(revert2 => lastReverted, "Locked did not revert"); @@ -480,19 +483,20 @@ rule vest_revert(uint256 _id) { assert(revert6 => lastReverted, "Division by zero did not revert"); assert(revert7 => lastReverted, "Underflow accruedAmt - rxd did not revert"); assert(revert8 => lastReverted, "Overflow rxd + unpaidAmt or toUint128 cast did not revert"); - assert(revert9 => lastReverted, "Zero vow address did not revert"); - assert(revert10 => lastReverted, "Overflow RAY * unpaidAmt did not revert"); - assert(revert11 => lastReverted, "Vat lack of auth did not revert"); - assert(revert12 => lastReverted, "Overflow sin + rad did not revert"); - assert(revert13 => lastReverted, "Overflow dai + rad did not revert"); - assert(revert14 => lastReverted, "Overflow vice + rad did not revert"); - assert(revert15 => lastReverted, "Overflow debt + rad did not revert"); - assert(revert16 => lastReverted, "Not live daiJoin did not revert"); - assert(revert17 => lastReverted, "The function wish did not revert"); - assert(revert18 => lastReverted, "Overflow dst + rad did not revert"); - assert(revert19 => lastReverted, "Lack of dai auth on daiJoin did not revert"); - assert(revert20 => lastReverted, "Overflow usr balance did not revert"); - assert(revert21 => lastReverted, "Overflow totalSupply did not revert"); + assert(revert9 => lastReverted, "Not live Vat did not revert"); + assert(revert10 => lastReverted, "Zero vow address did not revert"); + assert(revert11 => lastReverted, "Overflow RAY * unpaidAmt did not revert"); + assert(revert12 => lastReverted, "Vat lack of auth did not revert"); + assert(revert13 => lastReverted, "Overflow sin + rad did not revert"); + assert(revert14 => lastReverted, "Overflow dai + rad did not revert"); + assert(revert15 => lastReverted, "Overflow vice + rad did not revert"); + assert(revert16 => lastReverted, "Overflow debt + rad did not revert"); + assert(revert17 => lastReverted, "Not live daiJoin did not revert"); + assert(revert18 => lastReverted, "The function wish did not revert"); + assert(revert19 => lastReverted, "Overflow dst + rad did not revert"); + assert(revert20 => lastReverted, "Lack of dai auth on daiJoin did not revert"); + assert(revert21 => lastReverted, "Overflow usr balance did not revert"); + assert(revert22 => lastReverted, "Overflow totalSupply did not revert"); assert(lastReverted => revert1 || revert2 || revert3 || @@ -501,7 +505,8 @@ rule vest_revert(uint256 _id) { revert10 || revert11 || revert12 || revert13 || revert14 || revert15 || revert16 || revert17 || revert18 || - revert19 || revert20 || revert21, "Revert rules are not covering all the cases"); + revert19 || revert20 || revert21 || + revert22, "Revert rules are not covering all the cases"); } // Verify that awards behave correctly on vest with arbitrary max amt @@ -578,6 +583,7 @@ rule vest_amt_revert(uint256 _id, uint256 _maxAmt) { uint256 usrBalance = dai.balanceOf(usr); uint256 supply = dai.totalSupply(); uint256 locked = lockedGhost(); + uint256 vatLive = vat.live(); address vow = chainlog.getAddress(e, 0x4d43445f564f5700000000000000000000000000000000000000000000000000); uint256 daiJoinLive = daiJoin.live(); uint256 wardVat = vat.wards(currentContract); @@ -617,19 +623,20 @@ rule vest_amt_revert(uint256 _id, uint256 _maxAmt) { bool revert6 = e.block.timestamp >= clf && e.block.timestamp >= bgn && e.block.timestamp < fin && fin == bgn; bool revert7 = e.block.timestamp >= clf && accruedAmt < rxd; bool revert8 = rxd + amt > max_uint128; - bool revert9 = vow == 0; - bool revert10 = amtRad > max_uint256; - bool revert11 = wardVat != 1; - bool revert12 = sinVow + amtRad > max_uint256; - bool revert13 = vatDaiVest + amtRad > max_uint256; - bool revert14 = vice + amtRad > max_uint256; - bool revert15 = debt + amtRad > max_uint256; - bool revert16 = daiJoinLive != 1; - bool revert17 = currentContract != daiJoin && canVestDaiJoin != 1; - bool revert18 = vatDaiDaiJoin + amtRad > max_uint256; - bool revert19 = wardDai != 1; - bool revert20 = usrBalance + amt > max_uint256; - bool revert21 = supply + amt > max_uint256; + bool revert9 = vatLive != 1; + bool revert10 = vow == 0; + bool revert11 = amtRad > max_uint256; + bool revert12 = wardVat != 1; + bool revert13 = sinVow + amtRad > max_uint256; + bool revert14 = vatDaiVest + amtRad > max_uint256; + bool revert15 = vice + amtRad > max_uint256; + bool revert16 = debt + amtRad > max_uint256; + bool revert17 = daiJoinLive != 1; + bool revert18 = currentContract != daiJoin && canVestDaiJoin != 1; + bool revert19 = vatDaiDaiJoin + amtRad > max_uint256; + bool revert20 = wardDai != 1; + bool revert21 = usrBalance + amt > max_uint256; + bool revert22 = supply + amt > max_uint256; assert(revert1 => lastReverted, "Sending ETH did not revert"); assert(revert2 => lastReverted, "Locked did not revert"); @@ -639,19 +646,20 @@ rule vest_amt_revert(uint256 _id, uint256 _maxAmt) { assert(revert6 => lastReverted, "Division by zero did not revert"); assert(revert7 => lastReverted, "Underflow accruedAmt - rxd did not revert"); assert(revert8 => lastReverted, "Overflow rxd + amt or toUint128 cast did not revert"); - assert(revert9 => lastReverted, "Zero vow address did not revert"); - assert(revert10 => lastReverted, "Overflow RAY * amt did not revert"); - assert(revert11 => lastReverted, "Lack of vat auth on suckable did not revert"); - assert(revert12 => lastReverted, "Overflow sin + rad did not revert"); - assert(revert13 => lastReverted, "Overflow dai + rad did not revert"); - assert(revert14 => lastReverted, "Overflow vice + rad did not revert"); - assert(revert15 => lastReverted, "Overflow debt + rad did not revert"); - assert(revert16 => lastReverted, "Not live daiJoin did not revert"); - assert(revert17 => lastReverted, "The function wish did not revert"); - assert(revert18 => lastReverted, "Overflow dst + rad did not revert"); - assert(revert19 => lastReverted, "Lack of dai auth on daiJoin did not revert"); - assert(revert20 => lastReverted, "Overflow usr balance did not revert"); - assert(revert21 => lastReverted, "Overflow totalSupply did not revert"); + assert(revert9 => lastReverted, "Not live Vat did not revert"); + assert(revert10 => lastReverted, "Zero vow address did not revert"); + assert(revert11 => lastReverted, "Overflow RAY * amt did not revert"); + assert(revert12 => lastReverted, "Lack of vat auth on suckable did not revert"); + assert(revert13 => lastReverted, "Overflow sin + rad did not revert"); + assert(revert14 => lastReverted, "Overflow dai + rad did not revert"); + assert(revert15 => lastReverted, "Overflow vice + rad did not revert"); + assert(revert16 => lastReverted, "Overflow debt + rad did not revert"); + assert(revert17 => lastReverted, "Not live daiJoin did not revert"); + assert(revert18 => lastReverted, "The function wish did not revert"); + assert(revert19 => lastReverted, "Overflow dst + rad did not revert"); + assert(revert20 => lastReverted, "Lack of dai auth on daiJoin did not revert"); + assert(revert21 => lastReverted, "Overflow usr balance did not revert"); + assert(revert22 => lastReverted, "Overflow totalSupply did not revert"); assert(lastReverted => revert1 || revert2 || revert3 || @@ -660,7 +668,8 @@ rule vest_amt_revert(uint256 _id, uint256 _maxAmt) { revert10 || revert11 || revert12 || revert13 || revert14 || revert15 || revert16 || revert17 || revert18 || - revert19 || revert20 || revert21, "Revert rules are not covering all the cases"); + revert19 || revert20 || revert21 || + revert22, "Revert rules are not covering all the cases"); } // Verify that amt behaves correctly on accrued diff --git a/echidna.config.yml b/echidna.config.yml index 852368c3..9708ad7c 100644 --- a/echidna.config.yml +++ b/echidna.config.yml @@ -1,7 +1,7 @@ #format can be "text" or "json" for different output (human or machine readable) #format: "text" -#checkAsserts checks assertions -checkAsserts: true +#select the mode to test, which can be property, assertion, overflow, exploration, optimization +testMode: "assertion" #testLimit is the number of test sequences to run testLimit: 1000000 #seqLen defines how many transactions are in a test sequence diff --git a/echidna/DssVestMintableEchidnaTest.sol b/echidna/DssVestMintableEchidnaTest.sol index 4ae14a51..67958381 100644 --- a/echidna/DssVestMintableEchidnaTest.sol +++ b/echidna/DssVestMintableEchidnaTest.sol @@ -2,7 +2,7 @@ pragma solidity 0.6.12; -import {DssVestMintable} from "../src/DssVest.sol"; +import {DssVest, DssVestMintable} from "../src/DssVest.sol"; import {DSToken} from "./DSToken.sol"; interface Hevm { @@ -10,17 +10,6 @@ interface Hevm { function load(address, bytes32) external returns (bytes32); } -struct Award { - address usr; // Vesting recipient - uint48 bgn; // Start of vesting period [timestamp] - uint48 clf; // The cliff date [timestamp] - uint48 fin; // End of vesting period [timestamp] - address mgr; // A manager address that can yank - uint8 res; // Restricted - uint128 tot; // Total reward amount - uint128 rxd; // Amount of vest claimed -} - contract DssVestMintableEchidnaTest { DssVestMintable internal mVest; @@ -57,18 +46,10 @@ contract DssVestMintableEchidnaTest { } // --- Math --- - function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x + y; assert(z >= x); // check if there is an addition overflow } - function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = x - y; - assert(z <= x); // check if there is a subtraction overflow - } - function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = x * y; - assert(y == 0 || z / y == x); - } function toUint8(uint256 x) internal pure returns (uint8 z) { z = uint8(x); assert(z == x); @@ -120,13 +101,13 @@ contract DssVestMintableEchidnaTest { function create(address usr, uint256 tot, uint256 bgn, uint256 tau, uint256 eta, address mgr) public { uint256 prevId = mVest.ids(); try mVest.create(usr, tot, bgn, tau, eta, mgr) returns (uint256 id) { - assert(mVest.ids() == add(prevId, 1)); + assert(mVest.ids() == _add(prevId, 1)); assert(mVest.ids() == id); assert(mVest.valid(id)); assert(mVest.usr(id) == usr); assert(mVest.bgn(id) == toUint48(bgn)); - assert(mVest.clf(id) == toUint48(add(bgn, eta))); - assert(mVest.fin(id) == toUint48(add(bgn, tau))); + assert(mVest.clf(id) == toUint48(_add(bgn, eta))); + assert(mVest.fin(id) == toUint48(_add(bgn, tau))); assert(mVest.tot(id) == toUint128(tot)); assert(mVest.rxd(id) == 0); assert(mVest.mgr(id) == mgr); @@ -162,7 +143,7 @@ contract DssVestMintableEchidnaTest { function vest(uint256 id) public { id = mVest.ids() == 0 ? id : id % mVest.ids(); - Award memory award = Award({ + DssVest.Award memory award = DssVest.Award({ usr: mVest.usr(id), bgn: toUint48(mVest.bgn(id)), clf: toUint48(mVest.clf(id)), @@ -188,10 +169,10 @@ contract DssVestMintableEchidnaTest { assert(mVest.rxd(id) == award.tot); } else { - assert(mVest.rxd(id) == toUint128(add(award.rxd, unpaidAmt))); + assert(mVest.rxd(id) == toUint128(_add(award.rxd, unpaidAmt))); } - assert(gem.totalSupply() == add(supplyBefore, unpaidAmt)); - assert(gem.balanceOf(award.usr) == add(usrBalanceBefore, unpaidAmt)); + assert(gem.totalSupply() == _add(supplyBefore, unpaidAmt)); + assert(gem.balanceOf(award.usr) == _add(usrBalanceBefore, unpaidAmt)); } } catch Error(string memory errmsg) { bytes32 mLocked = hevm.load(address(mVest), bytes32(uint256(4))); // Load memory slot 0x4 @@ -215,7 +196,7 @@ contract DssVestMintableEchidnaTest { function vest_amt(uint256 id, uint256 maxAmt) public { id = mVest.ids() == 0 ? id : id % mVest.ids(); - Award memory award = Award({ + DssVest.Award memory award = DssVest.Award({ usr: mVest.usr(id), bgn: toUint48(mVest.bgn(id)), clf: toUint48(mVest.clf(id)), @@ -238,9 +219,9 @@ contract DssVestMintableEchidnaTest { assert(gem.balanceOf(award.usr) == usrBalanceBefore); } else { - assert(mVest.rxd(id) == toUint128(add(award.rxd, amt))); - assert(gem.totalSupply() == add(supplyBefore, amt)); - assert(gem.balanceOf(award.usr) == add(usrBalanceBefore, amt)); + assert(mVest.rxd(id) == toUint128(_add(award.rxd, amt))); + assert(gem.totalSupply() == _add(supplyBefore, amt)); + assert(gem.balanceOf(award.usr) == _add(usrBalanceBefore, amt)); } } catch Error(string memory errmsg) { bytes32 mLocked = hevm.load(address(mVest), bytes32(uint256(4))); // Load memory slot 0x4 @@ -315,7 +296,7 @@ contract DssVestMintableEchidnaTest { assert(mVest.clf(id) == end); assert(mVest.tot(id) == 0); } else { - assert(mVest.tot(id) == toUint128(add(unpaidAmt, rxd))); + assert(mVest.tot(id) == toUint128(_add(unpaidAmt, rxd))); } } } catch Error(string memory errmsg) { @@ -356,33 +337,37 @@ contract DssVestMintableEchidnaTest { // --- Time-Based Fuzz Mutations --- - function mutlock() public clock(1 hours) { - // Set DssVestMintable locked slot n. 4 to override 0 with 1 - hevm.store(address(mVest), bytes32(uint256(4)), bytes32(uint256(1))); + function mutlock() private clock(1 hours) { + bytes32 mLocked = hevm.load(address(mVest), bytes32(uint256(4))); // Load memory slot 0x4 + uint256 locked = uint256(mLocked) == 0 ? 1 : 0; + // Set DssVestMintable locked slot n. 4 to override 0 with 1 and vice versa + hevm.store(address(mVest), bytes32(uint256(4)), bytes32(uint256(locked))); + mLocked = hevm.load(address(mVest), bytes32(uint256(4))); + assert(uint256(mLocked) == locked); } - function mutauth() public clock(1 hours) { + function mutauth() private clock(1 hours) { uint256 wards = mVest.wards(address(this)) == 1 ? 0 : 1; // Set DssVestMintable wards slot n. 0 to override address(this) wards - hevm.store(address(mVest), keccak256(abi.encode(address(this), uint256(1))), bytes32(uint256(wards))); + hevm.store(address(mVest), keccak256(abi.encode(address(this), uint256(0))), bytes32(uint256(wards))); assert(mVest.wards(address(this)) == wards); } - function mutusr(uint256 id) public clock(1 days) { + function mutusr(uint256 id) private clock(1 days) { id = mVest.ids() == 0 ? 0 : id % mVest.ids(); if (id == 0) return; _mutusr(id); } function _mutusr(uint256 id) internal { address usr = mVest.usr(id) == address(this) ? address(0) : address(this); - // Set DssVestMintable awards slot n. 2 (clf, bgn, usr) to override awards(id).usr with address(this) - hevm.store(address(mVest), keccak256(abi.encode(uint256(id), uint256(2))), bytesToBytes32(abi.encodePacked(uint48(mVest.clf(id)), uint48(mVest.bgn(id)), usr))); + // Set DssVestMintable awards slot n. 1 (clf, bgn, usr) to override awards(id).usr with address(this) + hevm.store(address(mVest), keccak256(abi.encode(uint256(id), uint256(1))), bytesToBytes32(abi.encodePacked(uint48(mVest.clf(id)), uint48(mVest.bgn(id)), usr))); assert(mVest.usr(id) == usr); } - function mutcap(uint256 bump) public clock(90 days) { + function mutcap(uint256 bump) private clock(90 days) { bump %= MAX; if (bump == 0) return; uint256 data = bump > MIN ? bump * WAD / YEAR : MIN * WAD / YEAR; - // Set DssVestMintable cap slot n. 4 to override cap with data - hevm.store(address(mVest), bytes32(uint256(4)), bytes32(uint256(data))); + // Set DssVestMintable cap slot n. 2 to override cap with data + hevm.store(address(mVest), bytes32(uint256(2)), bytes32(uint256(data))); assert(mVest.cap() == data); } } diff --git a/echidna/DssVestSuckableEchidnaTest.sol b/echidna/DssVestSuckableEchidnaTest.sol index 192df43c..0220d9cd 100644 --- a/echidna/DssVestSuckableEchidnaTest.sol +++ b/echidna/DssVestSuckableEchidnaTest.sol @@ -2,7 +2,7 @@ pragma solidity 0.6.12; -import {DssVestSuckable} from "../src/DssVest.sol"; +import {DssVest, DssVestSuckable} from "../src/DssVest.sol"; import {ChainLog} from "./ChainLog.sol"; import {Vat} from "./Vat.sol"; import {DaiJoin} from "./DaiJoin.sol"; @@ -13,17 +13,6 @@ interface Hevm { function load(address, bytes32) external returns (bytes32); } -struct Award { - address usr; // Vesting recipient - uint48 bgn; // Start of vesting period [timestamp] - uint48 clf; // The cliff date [timestamp] - uint48 fin; // End of vesting period [timestamp] - address mgr; // A manager address that can yank - uint8 res; // Restricted - uint128 tot; // Total reward amount - uint128 rxd; // Amount of vest claimed -} - contract DssVestSuckableEchidnaTest { ChainLog internal chainlog; @@ -75,15 +64,11 @@ contract DssVestSuckableEchidnaTest { } // --- Math --- - function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x + y; assert(z >= x); // check if there is an addition overflow } - function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = x - y; - assert(z <= x); // check if there is a subtraction overflow - } - function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + function _mul(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x * y; assert(y == 0 || z / y == x); } @@ -138,13 +123,13 @@ contract DssVestSuckableEchidnaTest { function create(address usr, uint256 tot, uint256 bgn, uint256 tau, uint256 eta, address mgr) public { uint256 prevId = sVest.ids(); try sVest.create(usr, tot, bgn, tau, eta, mgr) returns (uint256 id) { - assert(sVest.ids() == add(prevId, 1)); + assert(sVest.ids() == _add(prevId, 1)); assert(sVest.ids() == id); assert(sVest.valid(id)); assert(sVest.usr(id) == usr); assert(sVest.bgn(id) == toUint48(bgn)); - assert(sVest.clf(id) == toUint48(add(bgn, eta))); - assert(sVest.fin(id) == toUint48(add(bgn, tau))); + assert(sVest.clf(id) == toUint48(_add(bgn, eta))); + assert(sVest.fin(id) == toUint48(_add(bgn, tau))); assert(sVest.tot(id) == toUint128(tot)); assert(sVest.rxd(id) == 0); assert(sVest.mgr(id) == mgr); @@ -180,7 +165,7 @@ contract DssVestSuckableEchidnaTest { function vest(uint256 id) public { id = sVest.ids() == 0 ? id : id % sVest.ids(); - Award memory award = Award({ + DssVest.Award memory award = DssVest.Award({ usr: sVest.usr(id), bgn: toUint48(sVest.bgn(id)), clf: toUint48(sVest.clf(id)), @@ -208,26 +193,27 @@ contract DssVestSuckableEchidnaTest { assert(sVest.rxd(id) == award.tot); } else { - assert(sVest.rxd(id) == toUint128(add(award.rxd, unpaidAmt))); + assert(sVest.rxd(id) == toUint128(_add(award.rxd, unpaidAmt))); } - assert(vat.sin(vow) == add(sinBefore, mul(unpaidAmt, RAY))); - assert(dai.totalSupply() == add(supplyBefore, unpaidAmt)); - assert(dai.balanceOf(award.usr) == add(usrBalanceBefore, unpaidAmt)); + assert(vat.sin(vow) == _add(sinBefore, _mul(unpaidAmt, RAY))); + assert(dai.totalSupply() == _add(supplyBefore, unpaidAmt)); + assert(dai.balanceOf(award.usr) == _add(usrBalanceBefore, unpaidAmt)); } } catch Error(string memory errmsg) { bytes32 sLocked = hevm.load(address(sVest), bytes32(uint256(4))); // Load memory slot 0x4 assert( - uint256(sLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked") || - award.usr == address(0) && cmpStr(errmsg, "DssVest/invalid-award") || - award.res != 0 && award.usr != address(this) && cmpStr(errmsg, "DssVest/only-user-can-claim") || - accruedAmt - award.rxd > accruedAmt && cmpStr(errmsg, "DssVest/sub-underflow") || - award.fin - award.bgn > award.fin && cmpStr(errmsg, "DssVest/sub-underflow") || - timeDelta > block.timestamp && cmpStr(errmsg, "DssVest/sub-underflow") || - timeDelta != 0 && (award.tot * timeDelta) / timeDelta != award.tot && cmpStr(errmsg, "DssVest/mul-overflow") || - award.rxd + unpaidAmt < award.rxd && cmpStr(errmsg, "DssVest/add-overflow") || - uint128(award.rxd + unpaidAmt) != (award.rxd + unpaidAmt) && cmpStr(errmsg, "DssVest/uint128-overflow") || - unpaidAmt * RAY / RAY != unpaidAmt && cmpStr(errmsg, "DssVest/mul-overflow") || - daiJoin.live() == 0 && cmpStr(errmsg, "DaiJoin/not-live") || + uint256(sLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked") || + award.usr == address(0) && cmpStr(errmsg, "DssVest/invalid-award") || + award.res != 0 && award.usr != address(this) && cmpStr(errmsg, "DssVest/only-user-can-claim") || + accruedAmt - award.rxd > accruedAmt && cmpStr(errmsg, "DssVest/sub-underflow") || + award.fin - award.bgn > award.fin && cmpStr(errmsg, "DssVest/sub-underflow") || + timeDelta > block.timestamp && cmpStr(errmsg, "DssVest/sub-underflow") || + timeDelta != 0 && (award.tot * timeDelta) / timeDelta != award.tot && cmpStr(errmsg, "DssVest/mul-overflow") || + award.rxd + unpaidAmt < award.rxd && cmpStr(errmsg, "DssVest/add-overflow") || + uint128(award.rxd + unpaidAmt) != (award.rxd + unpaidAmt) && cmpStr(errmsg, "DssVest/uint128-overflow") || + vat.live() == 0 && cmpStr(errmsg, "DssVestSuckable/vat-not-live") || + unpaidAmt * RAY / RAY != unpaidAmt && cmpStr(errmsg, "DssVest/mul-overflow") || + daiJoin.live() == 0 && cmpStr(errmsg, "DaiJoin/not-live") || vat.can(address(sVest), address(daiJoin)) != 1 && cmpStr(errmsg, "Vat/not-allowed") ); } catch { @@ -246,7 +232,7 @@ contract DssVestSuckableEchidnaTest { function vest_amt(uint256 id, uint256 maxAmt) public { id = sVest.ids() == 0 ? id : id % sVest.ids(); - Award memory award = Award({ + DssVest.Award memory award = DssVest.Award({ usr: sVest.usr(id), bgn: toUint48(sVest.bgn(id)), clf: toUint48(sVest.clf(id)), @@ -271,25 +257,26 @@ contract DssVestSuckableEchidnaTest { assert(dai.balanceOf(award.usr) == usrBalanceBefore); } else { - assert(sVest.rxd(id) == toUint128(add(award.rxd, amt))); - assert(vat.sin(vow) == add(sinBefore, mul(amt, RAY))); - assert(dai.totalSupply() == add(supplyBefore, amt)); - assert(dai.balanceOf(award.usr) == add(usrBalanceBefore, amt)); + assert(sVest.rxd(id) == toUint128(_add(award.rxd, amt))); + assert(vat.sin(vow) == _add(sinBefore, _mul(amt, RAY))); + assert(dai.totalSupply() == _add(supplyBefore, amt)); + assert(dai.balanceOf(award.usr) == _add(usrBalanceBefore, amt)); } } catch Error(string memory errmsg) { bytes32 sLocked = hevm.load(address(sVest), bytes32(uint256(4))); // Load memory slot 0x4 assert( - uint256(sLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked") || - award.usr == address(0) && cmpStr(errmsg, "DssVest/invalid-award") || - award.res != 0 && award.usr != address(this) && cmpStr(errmsg, "DssVest/only-user-can-claim") || - accruedAmt - award.rxd > accruedAmt && cmpStr(errmsg, "DssVest/sub-underflow") || - award.fin - award.bgn > award.fin && cmpStr(errmsg, "DssVest/sub-underflow") || - timeDelta > block.timestamp && cmpStr(errmsg, "DssVest/sub-underflow") || - timeDelta != 0 && (award.tot * timeDelta) / timeDelta != award.tot && cmpStr(errmsg, "DssVest/mul-overflow") || - award.rxd + amt < award.rxd && cmpStr(errmsg, "DssVest/add-overflow") || - uint128(award.rxd + amt) != (award.rxd + amt) && cmpStr(errmsg, "DssVest/uint128-overflow") || - amt * RAY / RAY != amt && cmpStr(errmsg, "DssVest/mul-overflow") || - daiJoin.live() == 0 && cmpStr(errmsg, "DaiJoin/not-live") || + uint256(sLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked") || + award.usr == address(0) && cmpStr(errmsg, "DssVest/invalid-award") || + award.res != 0 && award.usr != address(this) && cmpStr(errmsg, "DssVest/only-user-can-claim") || + accruedAmt - award.rxd > accruedAmt && cmpStr(errmsg, "DssVest/sub-underflow") || + award.fin - award.bgn > award.fin && cmpStr(errmsg, "DssVest/sub-underflow") || + timeDelta > block.timestamp && cmpStr(errmsg, "DssVest/sub-underflow") || + timeDelta != 0 && (award.tot * timeDelta) / timeDelta != award.tot && cmpStr(errmsg, "DssVest/mul-overflow") || + award.rxd + amt < award.rxd && cmpStr(errmsg, "DssVest/add-overflow") || + uint128(award.rxd + amt) != (award.rxd + amt) && cmpStr(errmsg, "DssVest/uint128-overflow") || + vat.live() == 0 && cmpStr(errmsg, "DssVestSuckable/vat-not-live") || + amt * RAY / RAY != amt && cmpStr(errmsg, "DssVest/mul-overflow") || + daiJoin.live() == 0 && cmpStr(errmsg, "DaiJoin/not-live") || vat.can(address(sVest), address(daiJoin)) != 1 && cmpStr(errmsg, "Vat/not-allowed") ); } catch { @@ -359,7 +346,7 @@ contract DssVestSuckableEchidnaTest { assert(sVest.clf(id) == end); assert(sVest.tot(id) == 0); } else { - assert(sVest.tot(id) == toUint128(add(unpaidAmt, rxd))); + assert(sVest.tot(id) == toUint128(_add(unpaidAmt, rxd))); } } } catch Error(string memory errmsg) { @@ -400,33 +387,43 @@ contract DssVestSuckableEchidnaTest { // --- Time-Based Fuzz Mutations --- - function mutlock() public clock(1 hours) { - // Set DssVestSuckable locked slot n. 4 to override 0 with 1 - hevm.store(address(sVest), bytes32(uint256(4)), bytes32(uint256(1))); + function mutlock() private clock(1 hours) { + bytes32 sLocked = hevm.load(address(sVest), bytes32(uint256(4))); // Load memory slot 0x4 + uint256 locked = uint256(sLocked) == 0 ? 1 : 0; + // Set DssVestSuckable locked slot n. 4 to override 0 with 1 and vice versa + hevm.store(address(sVest), bytes32(uint256(4)), bytes32(uint256(locked))); + sLocked = hevm.load(address(sVest), bytes32(uint256(4))); + assert(uint256(sLocked) == locked); } - function mutauth() public clock(1 hours) { + function mutauth() private clock(1 hours) { uint256 wards = sVest.wards(address(this)) == 1 ? 0 : 1; // Set DssVestSuckable wards slot n. 0 to override address(this) wards - hevm.store(address(sVest), keccak256(abi.encode(address(this), uint256(1))), bytes32(uint256(wards))); + hevm.store(address(sVest), keccak256(abi.encode(address(this), uint256(0))), bytes32(uint256(wards))); assert(sVest.wards(address(this)) == wards); } - function mutusr(uint256 id) public clock(1 days) { + function mutlive() private clock(1 hours) { + uint256 live = vat.live() == 1 ? 0 : 1; + // Set Vat live slot n. 10 to override 1 with 0 and vice versa + hevm.store(address(vat), bytes32(uint256(10)), bytes32(uint256(live))); + assert(vat.live() == live); + } + function mutusr(uint256 id) private clock(1 days) { id = sVest.ids() == 0 ? 0 : id % sVest.ids(); if (id == 0) return; _mutusr(id); } function _mutusr(uint256 id) internal { address usr = sVest.usr(id) == address(this) ? address(0) : address(this); - // Set DssVestSuckable awards slot n. 2 (clf, bgn, usr) to override awards(id).usr with address(this) - hevm.store(address(sVest), keccak256(abi.encode(uint256(id), uint256(2))), bytesToBytes32(abi.encodePacked(uint48(sVest.clf(id)), uint48(sVest.bgn(id)), usr))); + // Set DssVestSuckable awards slot n. 1 (clf, bgn, usr) to override awards(id).usr with address(this) + hevm.store(address(sVest), keccak256(abi.encode(uint256(id), uint256(1))), bytesToBytes32(abi.encodePacked(uint48(sVest.clf(id)), uint48(sVest.bgn(id)), usr))); assert(sVest.usr(id) == usr); } - function mutcap(uint256 bump) public clock(90 days) { + function mutcap(uint256 bump) private clock(90 days) { bump %= MAX; if (bump == 0) return; uint256 data = bump > MIN ? bump * WAD / TIME : MIN * WAD / TIME; - // Set DssVestSuckable cap slot n. 4 to override cap with data - hevm.store(address(sVest), bytes32(uint256(4)), bytes32(uint256(data))); + // Set DssVestSuckable cap slot n. 2 to override cap with data + hevm.store(address(sVest), bytes32(uint256(2)), bytes32(uint256(data))); assert(sVest.cap() == data); } } diff --git a/echidna/DssVestTransferrableEchidnaTest.sol b/echidna/DssVestTransferrableEchidnaTest.sol index 540d32d6..e7bb83e4 100644 --- a/echidna/DssVestTransferrableEchidnaTest.sol +++ b/echidna/DssVestTransferrableEchidnaTest.sol @@ -2,7 +2,7 @@ pragma solidity 0.6.12; -import {DssVestTransferrable} from "../src/DssVest.sol"; +import {DssVest, DssVestTransferrable} from "../src/DssVest.sol"; import {Dai} from "./Dai.sol"; interface Hevm { @@ -10,17 +10,6 @@ interface Hevm { function load(address, bytes32) external returns (bytes32); } -struct Award { - address usr; // Vesting recipient - uint48 bgn; // Start of vesting period [timestamp] - uint48 clf; // The cliff date [timestamp] - uint48 fin; // End of vesting period [timestamp] - address mgr; // A manager address that can yank - uint8 res; // Restricted - uint128 tot; // Total reward amount - uint128 rxd; // Amount of vest claimed -} - /// @dev A contract that will receive Dai, and allows for it to be retrieved. contract Multisig { @@ -69,18 +58,14 @@ contract DssVestTransferrableEchidnaTest { } // --- Math --- - function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x + y; assert(z >= x); // check if there is an addition overflow } - function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + function _sub(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x - y; assert(z <= x); // check if there is a subtraction overflow } - function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { - z = x * y; - assert(y == 0 || z / y == x); - } function toUint8(uint256 x) internal pure returns (uint8 z) { z = uint8(x); assert(z == x); @@ -132,13 +117,13 @@ contract DssVestTransferrableEchidnaTest { function create(address usr, uint256 tot, uint256 bgn, uint256 tau, uint256 eta, address mgr) public { uint256 prevId = tVest.ids(); try tVest.create(usr, tot, bgn, tau, eta, mgr) returns (uint256 id) { - assert(tVest.ids() == add(prevId, 1)); + assert(tVest.ids() == _add(prevId, 1)); assert(tVest.ids() == id); assert(tVest.valid(id)); assert(tVest.usr(id) == usr); assert(tVest.bgn(id) == toUint48(bgn)); - assert(tVest.clf(id) == toUint48(add(bgn, eta))); - assert(tVest.fin(id) == toUint48(add(bgn, tau))); + assert(tVest.clf(id) == toUint48(_add(bgn, eta))); + assert(tVest.fin(id) == toUint48(_add(bgn, tau))); assert(tVest.tot(id) == toUint128(tot)); assert(tVest.rxd(id) == 0); assert(tVest.mgr(id) == mgr); @@ -174,7 +159,7 @@ contract DssVestTransferrableEchidnaTest { function vest(uint256 id) public { id = tVest.ids() == 0 ? id : id % tVest.ids(); - Award memory award = Award({ + DssVest.Award memory award = DssVest.Award({ usr: tVest.usr(id), bgn: toUint48(tVest.bgn(id)), clf: toUint48(tVest.clf(id)), @@ -201,10 +186,10 @@ contract DssVestTransferrableEchidnaTest { assert(tVest.rxd(id) == award.tot); } else { - assert(tVest.rxd(id) == toUint128(add(award.rxd, unpaidAmt))); + assert(tVest.rxd(id) == toUint128(_add(award.rxd, unpaidAmt))); } - assert(gem.balanceOf(address(multisig)) == sub(msigBalanceBefore, unpaidAmt)); - assert(gem.balanceOf(award.usr) == add(usrBalanceBefore, unpaidAmt)); + assert(gem.balanceOf(address(multisig)) == _sub(msigBalanceBefore, unpaidAmt)); + assert(gem.balanceOf(award.usr) == _add(usrBalanceBefore, unpaidAmt)); } assert(gem.totalSupply() == supplyBefore); } catch Error(string memory errmsg) { @@ -235,7 +220,7 @@ contract DssVestTransferrableEchidnaTest { function vest_amt(uint256 id, uint256 maxAmt) public { id = tVest.ids() == 0 ? id : id % tVest.ids(); - Award memory award = Award({ + DssVest.Award memory award = DssVest.Award({ usr: tVest.usr(id), bgn: toUint48(tVest.bgn(id)), clf: toUint48(tVest.clf(id)), @@ -259,9 +244,9 @@ contract DssVestTransferrableEchidnaTest { assert(gem.balanceOf(award.usr) == usrBalanceBefore); } else { - assert(tVest.rxd(id) == toUint128(add(award.rxd, amt))); - assert(gem.balanceOf(address(multisig)) == sub(msigBalanceBefore, amt)); - assert(gem.balanceOf(award.usr) == add(usrBalanceBefore, amt)); + assert(tVest.rxd(id) == toUint128(_add(award.rxd, amt))); + assert(gem.balanceOf(address(multisig)) == _sub(msigBalanceBefore, amt)); + assert(gem.balanceOf(award.usr) == _add(usrBalanceBefore, amt)); } assert(gem.totalSupply() == supplyBefore); } catch Error(string memory errmsg) { @@ -343,7 +328,7 @@ contract DssVestTransferrableEchidnaTest { assert(tVest.clf(id) == end); assert(tVest.tot(id) == 0); } else { - assert(tVest.tot(id) == toUint128(add(unpaidAmt, rxd))); + assert(tVest.tot(id) == toUint128(_add(unpaidAmt, rxd))); } } } catch Error(string memory errmsg) { @@ -384,33 +369,37 @@ contract DssVestTransferrableEchidnaTest { // --- Time-Based Fuzz Mutations --- - function mutlock() public clock(1 hours) { - // Set DssVestTransferrable locked slot n. 4 to override 0 with 1 - hevm.store(address(tVest), bytes32(uint256(4)), bytes32(uint256(1))); + function mutlock() private clock(1 hours) { + bytes32 tLocked = hevm.load(address(tVest), bytes32(uint256(4))); // Load memory slot 0x4 + uint256 locked = uint256(tLocked) == 0 ? 1 : 0; + // Set DssVestTransferrable locked slot n. 4 to override 0 with 1 and vice versa + hevm.store(address(tVest), bytes32(uint256(4)), bytes32(uint256(locked))); + tLocked = hevm.load(address(tVest), bytes32(uint256(4))); + assert(uint256(tLocked) == locked); } - function mutauth() public clock(1 hours) { + function mutauth() private clock(1 hours) { uint256 wards = tVest.wards(address(this)) == 1 ? 0 : 1; // Set DssVestTransferrable wards slot n. 0 to override address(this) wards - hevm.store(address(tVest), keccak256(abi.encode(address(this), uint256(1))), bytes32(uint256(wards))); + hevm.store(address(tVest), keccak256(abi.encode(address(this), uint256(0))), bytes32(uint256(wards))); assert(tVest.wards(address(this)) == wards); } - function mutusr(uint256 id) public clock(1 days) { + function mutusr(uint256 id) private clock(1 days) { id = tVest.ids() == 0 ? 0 : id % tVest.ids(); if (id == 0) return; _mutusr(id); } function _mutusr(uint256 id) internal { address usr = tVest.usr(id) == address(this) ? address(0) : address(this); - // Set DssVestTrasferrable awards slot n. 2 (clf, bgn, usr) to override awards(id).usr with address(this) - hevm.store(address(tVest), keccak256(abi.encode(uint256(id), uint256(2))), bytesToBytes32(abi.encodePacked(uint48(tVest.clf(id)), uint48(tVest.bgn(id)), usr))); + // Set DssVestTrasferrable awards slot n. 1 (clf, bgn, usr) to override awards(id).usr with address(this) + hevm.store(address(tVest), keccak256(abi.encode(uint256(id), uint256(1))), bytesToBytes32(abi.encodePacked(uint48(tVest.clf(id)), uint48(tVest.bgn(id)), usr))); assert(tVest.usr(id) == usr); } - function mutcap(uint256 bump) public clock(90 days) { + function mutcap(uint256 bump) private clock(90 days) { bump %= MAX; if (bump == 0) return; uint256 data = bump > MIN ? bump * WAD / TIME : MIN * WAD / TIME; - // Set DssVestTransferrable cap slot n. 4 to override cap with data - hevm.store(address(tVest), bytes32(uint256(4)), bytes32(uint256(data))); + // Set DssVestTransferrable cap slot n. 2 to override cap with data + hevm.store(address(tVest), bytes32(uint256(2)), bytes32(uint256(data))); assert(tVest.cap() == data); } } diff --git a/src/DssVest.sol b/src/DssVest.sol index e4969449..59e52d34 100644 --- a/src/DssVest.sol +++ b/src/DssVest.sol @@ -34,6 +34,7 @@ interface DaiJoinLike { interface VatLike { function hope(address) external; function suck(address, address, uint256) external; + function live() external view returns (uint256); } interface TokenLike { @@ -41,38 +42,8 @@ interface TokenLike { } abstract contract DssVest { - - uint256 public constant TWENTY_YEARS = 20 * 365 days; - - uint256 internal locked; - - event Rely(address indexed usr); - event Deny(address indexed usr); - event Init(uint256 indexed id, address indexed usr); - event Vest(uint256 indexed id, uint256 amt); - event Move(uint256 indexed id, address indexed dst); - event File(bytes32 indexed what, uint256 data); - event Yank(uint256 indexed id, uint256 end); - event Restrict(uint256 indexed id); - event Unrestrict(uint256 indexed id); - - - // --- Auth --- + // --- Data --- mapping (address => uint256) public wards; - function rely(address usr) external auth { wards[usr] = 1; emit Rely(usr); } - function deny(address usr) external auth { wards[usr] = 0; emit Deny(usr); } - modifier auth { - require(wards[msg.sender] == 1, "DssVest/not-authorized"); - _; - } - - // --- Mutex --- - modifier lock { - require(locked == 0, "DssVest/system-locked"); - locked = 1; - _; - locked = 0; - } struct Award { address usr; // Vesting recipient @@ -85,10 +56,28 @@ abstract contract DssVest { uint128 rxd; // Amount of vest claimed } mapping (uint256 => Award) public awards; - uint256 public ids; uint256 public cap; // Maximum per-second issuance token rate + uint256 public ids; // Total vestings + + uint256 internal locked; + + uint256 public constant TWENTY_YEARS = 20 * 365 days; + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + + event File(bytes32 indexed what, uint256 data); + + event Init(uint256 indexed id, address indexed usr); + event Vest(uint256 indexed id, uint256 amt); + event Restrict(uint256 indexed id); + event Unrestrict(uint256 indexed id); + event Yank(uint256 indexed id, uint256 end); + event Move(uint256 indexed id, address indexed dst); + // Getters to access only to the value desired function usr(uint256 _id) external view returns (address) { return awards[_id].usr; @@ -130,12 +119,35 @@ abstract contract DssVest { emit Rely(msg.sender); } + // --- Mutex --- + modifier lock { + require(locked == 0, "DssVest/system-locked"); + locked = 1; + _; + locked = 0; + } + + // --- Auth --- + modifier auth { + require(wards[msg.sender] == 1, "DssVest/not-authorized"); + _; + } + + function rely(address _usr) external auth { + wards[_usr] = 1; + emit Rely(_usr); + } + function deny(address _usr) external auth { + wards[_usr] = 0; + emit Deny(_usr); + } + /** @dev (Required) Set the per-second token issuance rate. @param what The tag of the value to change (ex. bytes32("cap")) @param data The value to update (ex. cap of 1000 tokens/yr == 1000*WAD/365 days) */ - function file(bytes32 what, uint256 data) external auth lock { + function file(bytes32 what, uint256 data) external lock auth { if (what == "cap") cap = data; // The maximum amount of tokens that can be streamed per-second per vest else revert("DssVest/file-unrecognized-param"); emit File(what, data); @@ -170,7 +182,7 @@ abstract contract DssVest { @param _mgr An optional manager for the contract. Can yank if vesting ends prematurely. @return id The id of the vesting contract */ - function create(address _usr, uint256 _tot, uint256 _bgn, uint256 _tau, uint256 _eta, address _mgr) external auth lock returns (uint256 id) { + function create(address _usr, uint256 _tot, uint256 _bgn, uint256 _tau, uint256 _eta, address _mgr) external lock auth returns (uint256 id) { require(_usr != address(0), "DssVest/invalid-user"); require(_tot > 0, "DssVest/no-vest-total-amount"); require(_bgn < add(block.timestamp, TWENTY_YEARS), "DssVest/bgn-too-far"); @@ -396,7 +408,7 @@ contract DssVestMintable is DssVest { @param _gem The contract address of the mintable token */ constructor(address _gem) public DssVest() { - require(_gem != address(0), "DssVest/Invalid-token-address"); + require(_gem != address(0), "DssVestMintable/Invalid-token-address"); gem = MintLike(_gem); } @@ -423,7 +435,7 @@ contract DssVestSuckable is DssVest { @param _chainlog The contract address of the MCD chainlog */ constructor(address _chainlog) public DssVest() { - require(_chainlog != address(0), "DssVest/Invalid-chainlog-address"); + require(_chainlog != address(0), "DssVestSuckable/Invalid-chainlog-address"); ChainlogLike chainlog_ = chainlog = ChainlogLike(_chainlog); VatLike vat_ = vat = VatLike(chainlog_.getAddress("MCD_VAT")); DaiJoinLike daiJoin_ = daiJoin = DaiJoinLike(chainlog_.getAddress("MCD_JOIN_DAI")); @@ -437,6 +449,7 @@ contract DssVestSuckable is DssVest { @param _amt The amount of Dai to send to the _guy [WAD] */ function pay(address _guy, uint256 _amt) override internal { + require(vat.live() == 1, "DssVestSuckable/vat-not-live"); vat.suck(chainlog.getAddress("MCD_VOW"), address(this), mul(_amt, RAY)); daiJoin.exit(_guy, _amt); } @@ -458,8 +471,8 @@ contract DssVestTransferrable is DssVest { @param _gem The token to be distributed */ constructor(address _czar, address _gem) public DssVest() { - require(_czar != address(0), "DssVest/Invalid-distributor-address"); - require(_gem != address(0), "DssVest/Invalid-token-address"); + require(_czar != address(0), "DssVestTransferrable/Invalid-distributor-address"); + require(_gem != address(0), "DssVestTransferrable/Invalid-token-address"); czar = _czar; gem = TokenLike(_gem); } @@ -470,6 +483,6 @@ contract DssVestTransferrable is DssVest { @param _amt The amount of gem to send to the _guy (in native token units) */ function pay(address _guy, uint256 _amt) override internal { - require(gem.transferFrom(czar, _guy, _amt)); + require(gem.transferFrom(czar, _guy, _amt), "DssVestTransferrable/failed-transfer"); } } diff --git a/src/DssVest.t.sol b/src/DssVest.t.sol index 7a55c0b0..3d8d35dd 100644 --- a/src/DssVest.t.sol +++ b/src/DssVest.t.sol @@ -3,29 +3,46 @@ pragma solidity 0.6.12; import "ds-test/test.sol"; -import "./DssVest.sol"; +import {DssVest, DssVestMintable, DssVestSuckable, DssVestTransferrable} from "./DssVest.sol"; interface Hevm { function warp(uint256) external; - function store(address,bytes32,bytes32) external; - function load(address,bytes32) external; + function store(address, bytes32, bytes32) external; + function load(address, bytes32) external returns (bytes32); +} + +interface ChainlogLike { + function getAddress(bytes32) external view returns (address); +} + +interface EndLike { + function cage() external; + function thaw() external; + function wait() external returns (uint256); + function debt() external returns (uint256); } interface GemLike { function approve(address, uint256) external returns (bool); } -interface GovGuard { - function wards(address) external returns (uint256); +interface DaiLike is GemLike { + function balanceOf(address) external returns (uint256); } -interface Token { +interface DSTokenLike { function balanceOf(address) external returns (uint256); } -interface VatLikeTest { +interface MkrAuthorityLike { + function wards(address) external returns (uint256); +} + +interface VatLike { function wards(address) external view returns (uint256); function sin(address) external view returns (uint256); + function debt() external view returns (uint256); + function live() external view returns (uint256); } contract Manager { @@ -59,675 +76,739 @@ contract User { } contract DssVestTest is DSTest { - Hevm hevm; - DssVestMintable vest; - DssVestSuckable suckableVest; - - address constant MKR_TOKEN = 0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2; - address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; - address constant VAT = 0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B; - address constant DAI_JOIN = 0x9759A6Ac90977b93B58547b4A71c78317f391A28; - address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - address constant VOW = 0xA950524441892A31ebddF91d3cEEFa04Bf454466; + // --- Math --- uint256 constant WAD = 10**18; uint256 constant RAY = 10**27; uint256 constant days_vest = WAD; + // --- Hevm --- + Hevm hevm; + // CHEAT_CODE = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D bytes20 constant CHEAT_CODE = bytes20(uint160(uint256(keccak256('hevm cheat code')))); - address GOV_GUARD = 0x6eEB68B2C7A918f36B78E2DB80dcF279236DDFb8; + DssVestMintable mVest; + DssVestSuckable sVest; + DssVestTransferrable tVest; + Manager boss; + + ChainlogLike chainlog; + DSTokenLike gem; + MkrAuthorityLike authority; + VatLike vat; + DaiLike dai; + EndLike end; + + address VOW; function setUp() public { hevm = Hevm(address(CHEAT_CODE)); - vest = new DssVestMintable(MKR_TOKEN); - vest.file("cap", (2000 * WAD) / (4 * 365 days)); - suckableVest = new DssVestSuckable(CHAINLOG); - suckableVest.file("cap", (2000 * WAD) / (4 * 365 days)); + + chainlog = ChainlogLike(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F); + gem = DSTokenLike ( chainlog.getAddress("MCD_GOV")); + authority = MkrAuthorityLike( chainlog.getAddress("GOV_GUARD")); + vat = VatLike( chainlog.getAddress("MCD_VAT")); + dai = DaiLike( chainlog.getAddress("MCD_DAI")); + end = EndLike( chainlog.getAddress("MCD_END")); + VOW = chainlog.getAddress("MCD_VOW"); + + mVest = new DssVestMintable(address(gem)); + mVest.file("cap", (2000 * WAD) / (4 * 365 days)); + sVest = new DssVestSuckable(address(chainlog)); + sVest.file("cap", (2000 * WAD) / (4 * 365 days)); + boss = new Manager(); + tVest = new DssVestTransferrable(address(boss), address(dai)); + tVest.file("cap", (2000 * WAD) / (4 * 365 days)); + boss.gemApprove(address(dai), address(tVest)); + // Set testing contract as a MKR Auth hevm.store( - address(GOV_GUARD), - keccak256(abi.encode(address(vest), uint256(1))), + address(authority), + keccak256(abi.encode(address(mVest), uint256(1))), bytes32(uint256(1)) ); - assertEq(GovGuard(GOV_GUARD).wards(address(vest)), 1); + assertEq(authority.wards(address(mVest)), 1); // Give admin access to vat hevm.store( - address(VAT), - keccak256(abi.encode(address(suckableVest), uint256(0))), + address(vat), + keccak256(abi.encode(address(sVest), uint256(0))), bytes32(uint256(1)) ); - assertEq(VatLikeTest(VAT).wards(address(suckableVest)), 1); + assertEq(vat.wards(address(sVest)), 1); + + // Give boss 10000 DAI + hevm.store( + address(dai), + keccak256(abi.encode(address(boss), uint(2))), + bytes32(uint256(10000 * WAD)) + ); + assertEq(dai.balanceOf(address(boss)), 10000 * WAD); } function testCost() public { - new DssVestMintable(MKR_TOKEN); + new DssVestMintable(address(gem)); } function testInit() public { - vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(1)); - (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 amt, uint128 rxd) = vest.awards(1); + mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(1)); + (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 tot, uint128 rxd) = mVest.awards(1); assertEq(usr, address(this)); assertEq(uint256(bgn), now); assertEq(uint256(clf), now); assertEq(uint256(fin), now + 100 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); assertEq(mgr, address(1)); } function testVest() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); hevm.warp(now + 10 days); - (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 amt, uint128 rxd) = vest.awards(id); + (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 tot, uint128 rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); - vest.vest(id); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + mVest.vest(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 10 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 10 * days_vest); + assertEq(gem.balanceOf(address(this)), 10 * days_vest); hevm.warp(now + 70 days); - vest.vest(id, type(uint256).max); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + mVest.vest(id, type(uint256).max); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 80 days); assertEq(uint256(fin), now + 20 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 80 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 80 * days_vest); + assertEq(gem.balanceOf(address(this)), 80 * days_vest); } function testFailVestNonExistingAward() public { - vest.vest(9999); + mVest.vest(9999); } function testVestInsideCliff() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 50 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 50 days, address(0)); hevm.warp(now + 10 days); - vest.vest(id); // vest is inside cliff, no payout should happen - (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 amt, uint128 rxd) = vest.awards(id); + mVest.vest(id); // vest is inside cliff, no payout should happen + (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 tot, uint128 rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(clf), now + 40 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); assertEq(mgr, address(0)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); } function testVestAfterTimeout() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); hevm.warp(now + 200 days); - (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 amt, uint128 rxd) = vest.awards(id); + (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 tot, uint128 rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 200 days); assertEq(uint256(fin), now - 100 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); - vest.vest(id); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + mVest.vest(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); // After final payout, vesting information is removed assertEq(usr, address(this)); assertEq(uint256(bgn), now - 200 days); assertEq(uint256(fin), now - 100 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 100 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 100*days_vest); - assertTrue(!vest.valid(id)); + assertEq(gem.balanceOf(address(this)), 100*days_vest); + assertTrue(!mVest.valid(id)); } function testMove() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); - vest.move(id, address(3)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + mVest.move(id, address(3)); - (address usr,,,,,,,) = vest.awards(id); + (address usr,,,,,,,) = mVest.awards(id); assertEq(usr, address(3)); } function testFailMoveToZeroAddress() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); - vest.move(id, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + mVest.move(id, address(0)); } function testUnpaid() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); - assertTrue(vest.valid(id)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); + assertTrue(mVest.valid(id)); - assertEq(vest.unpaid(id), 0); + assertEq(mVest.unpaid(id), 0); hevm.warp(block.timestamp + 43200); - assertEq(vest.unpaid(id), 0); // inside cliff + assertEq(mVest.unpaid(id), 0); // inside cliff hevm.warp(block.timestamp + 36 hours); - assertEq(vest.unpaid(id), days_vest * 2); // past cliff + assertEq(mVest.unpaid(id), days_vest * 2); // past cliff hevm.warp(block.timestamp + 2 days); - assertEq(vest.unpaid(id), days_vest * 4); // past cliff - vest.vest(id); - assertEq(vest.unpaid(id), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), days_vest * 4); + assertEq(mVest.unpaid(id), days_vest * 4); // past cliff + mVest.vest(id); + assertEq(mVest.unpaid(id), 0); + assertEq(gem.balanceOf(address(this)), days_vest * 4); hevm.warp(block.timestamp + 10 days); - assertEq(vest.unpaid(id), days_vest * 10); - vest.vest(id); - assertEq(vest.unpaid(id), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), days_vest * 14); + assertEq(mVest.unpaid(id), days_vest * 10); + mVest.vest(id); + assertEq(mVest.unpaid(id), 0); + assertEq(gem.balanceOf(address(this)), days_vest * 14); hevm.warp(block.timestamp + 120 days); // vesting complete - assertEq(vest.unpaid(id), days_vest * 86); - vest.vest(id); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 100 * days_vest); + assertEq(mVest.unpaid(id), days_vest * 86); + mVest.vest(id); + assertEq(gem.balanceOf(address(this)), 100 * days_vest); } function testAccrued() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp + 10 days, 100 days, 0, address(0)); - assertTrue(vest.valid(id)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp + 10 days, 100 days, 0, address(0)); + assertTrue(mVest.valid(id)); - assertEq(vest.accrued(id), 0); + assertEq(mVest.accrued(id), 0); hevm.warp(block.timestamp + 43200); - assertEq(vest.unpaid(id), 0); // inside cliff - assertEq(vest.accrued(id), 0); + assertEq(mVest.unpaid(id), 0); // inside cliff + assertEq(mVest.accrued(id), 0); hevm.warp(block.timestamp + 12 hours + 11 days); - assertEq(vest.unpaid(id), days_vest * 2); // past cliff - assertEq(vest.accrued(id), days_vest * 2); + assertEq(mVest.unpaid(id), days_vest * 2); // past cliff + assertEq(mVest.accrued(id), days_vest * 2); hevm.warp(block.timestamp + 2 days); - assertEq(vest.unpaid(id), days_vest * 4); // past cliff - assertEq(vest.accrued(id), days_vest * 4); - vest.vest(id); - assertEq(vest.unpaid(id), 0); - assertEq(vest.accrued(id), days_vest * 4); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), days_vest * 4); + assertEq(mVest.unpaid(id), days_vest * 4); // past cliff + assertEq(mVest.accrued(id), days_vest * 4); + mVest.vest(id); + assertEq(mVest.unpaid(id), 0); + assertEq(mVest.accrued(id), days_vest * 4); + assertEq(gem.balanceOf(address(this)), days_vest * 4); hevm.warp(block.timestamp + 10 days); - assertEq(vest.unpaid(id), days_vest * 10); - assertEq(vest.accrued(id), days_vest * 14); - vest.vest(id); - assertEq(vest.unpaid(id), 0); - assertEq(vest.accrued(id), days_vest * 14); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), days_vest * 14); - hevm.warp(block.timestamp + 120 days); // vesting complete - assertEq(vest.unpaid(id), days_vest * 86); - assertEq(vest.accrued(id), days_vest * 100); - vest.vest(id); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 100 * days_vest); + assertEq(mVest.unpaid(id), days_vest * 10); + assertEq(mVest.accrued(id), days_vest * 14); + mVest.vest(id); + assertEq(mVest.unpaid(id), 0); + assertEq(mVest.accrued(id), days_vest * 14); + assertEq(gem.balanceOf(address(this)), days_vest * 14); + hevm.warp(block.timestamp + 120 days); // vesting complete + assertEq(mVest.unpaid(id), days_vest * 86); + assertEq(mVest.accrued(id), days_vest * 100); + mVest.vest(id); + assertEq(gem.balanceOf(address(this)), 100 * days_vest); } function testFutureAccrual() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp + 10 days, 100 days, 0, address(0)); - assertEq(vest.accrued(id), 0); // accrual starts in 10 days + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp + 10 days, 100 days, 0, address(0)); + assertEq(mVest.accrued(id), 0); // accrual starts in 10 days hevm.warp(block.timestamp + 9 days); - assertEq(vest.accrued(id), 0); // accrual starts in 1 days + assertEq(mVest.accrued(id), 0); // accrual starts in 1 days hevm.warp(block.timestamp + 2 days); - assertEq(vest.accrued(id), days_vest); // accrual started 1 day ago + assertEq(mVest.accrued(id), days_vest); // accrual started 1 day ago hevm.warp(block.timestamp + 999 days); - assertEq(vest.accrued(id), days_vest * 100); // accrual ended + assertEq(mVest.accrued(id), days_vest * 100); // accrual ended } function testYank() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); - assertTrue(vest.valid(id)); - vest.yank(id); // yank before cliff - assertTrue(!vest.valid(id)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); + assertTrue(mVest.valid(id)); + mVest.yank(id); // yank before cliff + assertTrue(!mVest.valid(id)); - id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); - assertTrue(vest.valid(id)); + id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 2 days); - vest.yank(id); // yank after cliff - (, uint48 bgn, uint48 clf, uint48 fin,,, uint128 tot,) = vest.awards(id); + mVest.yank(id); // yank after cliff + (, uint48 bgn, uint48 clf, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(bgn, block.timestamp - 2 days); assertEq(clf, block.timestamp - 1 days); assertEq(fin, block.timestamp); assertEq(uint256(tot), 100 * days_vest * 2 / 100); - assertTrue(vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); - vest.vest(id); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 2 * days_vest); - assertTrue(!vest.valid(id)); + assertTrue(mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 0); + mVest.vest(id); + assertEq(gem.balanceOf(address(this)), 2 * days_vest); + assertTrue(!mVest.valid(id)); } function testYankInsideCliff() public { Manager manager = new Manager(); - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 50 days, address(manager)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 50 days, address(manager)); hevm.warp(block.timestamp + 10 days); - manager.yank(address(vest), id); + manager.yank(address(mVest), id); - (, uint48 bgn, uint48 clf, uint48 fin,,, uint128 tot,) = vest.awards(id); + (, uint48 bgn, uint48 clf, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(bgn, block.timestamp - 10 days); assertEq(clf, block.timestamp); assertEq(fin, block.timestamp); assertEq(uint256(tot), 0); - assertTrue(!vest.valid(id)); + assertTrue(!mVest.valid(id)); } function testYankBeforeBgn() public { Manager manager = new Manager(); - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp + 10 days, 100 days, 50 days, address(manager)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp + 10 days, 100 days, 50 days, address(manager)); hevm.warp(block.timestamp + 5 days); - manager.yank(address(vest), id); + manager.yank(address(mVest), id); - (, uint48 bgn, uint48 clf, uint48 fin,,, uint128 tot,) = vest.awards(id); + (, uint48 bgn, uint48 clf, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(bgn, block.timestamp); assertEq(clf, block.timestamp); assertEq(fin, block.timestamp); assertEq(uint256(tot), 0); - assertTrue(!vest.valid(id)); + assertTrue(!mVest.valid(id)); } function testDoubleYank() public { // Test case where vest is yanked twice, say by manager and then governance - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); - assertTrue(vest.valid(id)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 2 days); - vest.yank(id); // accrued two days before yank - (,,, uint48 fin,,, uint128 amt,) = vest.awards(id); + mVest.yank(id); // accrued two days before yank + (,,, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(fin, block.timestamp); - assertEq(amt, 2 * days_vest); - assertTrue(vest.valid(id)); + assertEq(tot, 2 * days_vest); + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 2 days); - vest.yank(id); // yank again later - (,,, fin,,, amt,) = vest.awards(id); + mVest.yank(id); // yank again later + (,,, fin,,, tot,) = mVest.awards(id); assertEq(fin, block.timestamp - 2 days); // fin stays the same as the first yank - assertEq(amt, 2 * days_vest); // amt doesn't get updated on second yank - assertTrue(vest.valid(id)); + assertEq(tot, 2 * days_vest); // tot doesn't get updated on second yank + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 999 days); - vest.vest(id); // user collects at some future time - assertTrue(!vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 2 * days_vest); + mVest.vest(id); // user collects at some future time + assertTrue(!mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 2 * days_vest); } function testYankAfterVest() public { // Test case where yanked is called after a partial vest - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); - assertTrue(vest.valid(id)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(0)); + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 2 days); - assertEq(vest.unpaid(id), 2 * days_vest); - vest.vest(id); // collect some now - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 2 * days_vest); + assertEq(mVest.unpaid(id), 2 * days_vest); + mVest.vest(id); // collect some now + assertEq(gem.balanceOf(address(this)), 2 * days_vest); hevm.warp(block.timestamp + 2 days); - assertEq(vest.unpaid(id), 2 * days_vest); - assertEq(vest.accrued(id), 4 * days_vest); + assertEq(mVest.unpaid(id), 2 * days_vest); + assertEq(mVest.accrued(id), 4 * days_vest); - vest.yank(id); // yank 4 days after start - (,,, uint48 fin,,, uint128 amt,) = vest.awards(id); + mVest.yank(id); // yank 4 days after start + (,,, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(fin, block.timestamp); - assertEq(amt, 4 * days_vest); - assertTrue(vest.valid(id)); + assertEq(tot, 4 * days_vest); + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 999 days); - assertEq(vest.unpaid(id), 2 * days_vest); - assertEq(vest.accrued(id), 4 * days_vest); - vest.vest(id); // user collects at some future time - assertTrue(!vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 4 * days_vest); + assertEq(mVest.unpaid(id), 2 * days_vest); + assertEq(mVest.accrued(id), 4 * days_vest); + mVest.vest(id); // user collects at some future time + assertTrue(!mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 4 * days_vest); } function testYankSchedulePassed() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); hevm.warp(block.timestamp + 51 days); - vest.yank(id, now - 10 days); // Try to yank before cliff + mVest.yank(id, now - 10 days); // Try to yank before cliff - (,,, uint48 fin,,, uint128 amt,) = vest.awards(id); + (,,, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(fin, block.timestamp); - assertEq(amt, 51 * days_vest); // amt is total amount - assertTrue(vest.valid(id)); + assertEq(tot, 51 * days_vest); // tot is total amount + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 999 days); - assertEq(vest.unpaid(id), 51 * days_vest); - assertEq(vest.accrued(id), 51 * days_vest); - vest.vest(id); // user collects at some future time - assertTrue(!vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 51 * days_vest); + assertEq(mVest.unpaid(id), 51 * days_vest); + assertEq(mVest.accrued(id), 51 * days_vest); + mVest.vest(id); // user collects at some future time + assertTrue(!mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 51 * days_vest); } function testYankScheduleFutureAfterCliff() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); hevm.warp(block.timestamp + 11 days); - vest.yank(id, now + 10 days); // Schedule yank after cliff + mVest.yank(id, now + 10 days); // Schedule yank after cliff - (,,, uint48 fin,,, uint128 amt,) = vest.awards(id); + (,,, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(fin, block.timestamp + 10 days); - assertEq(uint256(amt), 21 * days_vest); // amt is total amount - assertTrue(vest.valid(id)); + assertEq(uint256(tot), 21 * days_vest); // tot is total amount + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 999 days); - assertEq(vest.unpaid(id), 21 * days_vest); - assertEq(vest.accrued(id), 21 * days_vest); - vest.vest(id); // user collects at some future time - assertTrue(!vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 21 * days_vest); + assertEq(mVest.unpaid(id), 21 * days_vest); + assertEq(mVest.accrued(id), 21 * days_vest); + mVest.vest(id); // user collects at some future time + assertTrue(!mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 21 * days_vest); } function testYankScheduleFutureBeforeCliff() public { // Test case where yank is scheduled but before the cliff - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); - vest.yank(id, now + 10 days); // Schedule yank before cliff + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); + mVest.yank(id, now + 10 days); // Schedule yank before cliff - (,,, uint48 fin,,, uint128 amt,) = vest.awards(id); + (,,, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(fin, block.timestamp + 10 days); - assertEq(uint256(amt), 0); // amt is total amount - assertTrue(!vest.valid(id)); + assertEq(uint256(tot), 0); // tot is total amount + assertTrue(!mVest.valid(id)); hevm.warp(block.timestamp + 999 days); - assertEq(vest.unpaid(id), 0); - assertEq(vest.accrued(id), 0); - vest.vest(id); // user collects at some future time - assertTrue(!vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(mVest.unpaid(id), 0); + assertEq(mVest.accrued(id), 0); + mVest.vest(id); // user collects at some future time + assertTrue(!mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 0); } function testYankScheduleFutureAfterCompletion() public { // When the sheduled yank takes place after the natural conclusion of the vest, // Pay out the remainder of the contract and no more. - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 20 days, address(0)); hevm.warp(block.timestamp + 11 days); - vest.yank(id, now + 999 days); // Schedule yank after completion + mVest.yank(id, now + 999 days); // Schedule yank after completion - (,,, uint48 fin,,, uint128 amt,) = vest.awards(id); + (,,, uint48 fin,,, uint128 tot,) = mVest.awards(id); assertEq(fin, block.timestamp + 89 days); - assertEq(uint256(amt), 100 * days_vest); // amt is total amount - assertTrue(vest.valid(id)); + assertEq(uint256(tot), 100 * days_vest); // tot is total amount + assertTrue(mVest.valid(id)); hevm.warp(block.timestamp + 999 days); - assertEq(vest.unpaid(id), 100 * days_vest); - assertEq(vest.accrued(id), 100 * days_vest); - vest.vest(id); // user collects at some future time - assertTrue(!vest.valid(id)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 100 * days_vest); + assertEq(mVest.unpaid(id), 100 * days_vest); + assertEq(mVest.accrued(id), 100 * days_vest); + mVest.vest(id); // user collects at some future time + assertTrue(!mVest.valid(id)); + assertEq(gem.balanceOf(address(this)), 100 * days_vest); } function testMgrYank() public { Manager manager = new Manager(); - uint256 id1 = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(manager)); + uint256 id1 = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 1 days, address(manager)); - assertTrue(vest.valid(id1)); + assertTrue(mVest.valid(id1)); hevm.warp(block.timestamp + 30 days); - manager.yank(address(vest), id1); - assertTrue(vest.valid(id1)); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); - vest.vest(id1); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 30 * days_vest); - assertTrue(!vest.valid(id1)); - - uint256 id2 = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 30 days, address(manager)); + manager.yank(address(mVest), id1); + assertTrue(mVest.valid(id1)); + assertEq(gem.balanceOf(address(this)), 0); + mVest.vest(id1); + assertEq(gem.balanceOf(address(this)), 30 * days_vest); + assertTrue(!mVest.valid(id1)); + + uint256 id2 = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 30 days, address(manager)); assertTrue(id1 != id2); - assertTrue(vest.valid(id2)); - manager.yank(address(vest), id2); - assertTrue(!vest.valid(id2)); + assertTrue(mVest.valid(id2)); + manager.yank(address(mVest), id2); + assertTrue(!mVest.valid(id2)); } function testUnRestrictedVest() public { ThirdPartyVest alice = new ThirdPartyVest(); - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); hevm.warp(now + 10 days); - (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 amt, uint128 rxd) = vest.awards(id); + (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 tot, uint128 rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); - alice.vest(address(vest), id); + alice.vest(address(mVest), id); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 10 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 10 * days_vest); + assertEq(gem.balanceOf(address(this)), 10 * days_vest); hevm.warp(now + 70 days); - alice.vest(address(vest), id); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + alice.vest(address(mVest), id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 80 days); assertEq(uint256(fin), now + 20 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 80 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 80 * days_vest); + assertEq(gem.balanceOf(address(this)), 80 * days_vest); } function testFailRestrictedVest() public { ThirdPartyVest alice = new ThirdPartyVest(); - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); hevm.warp(now + 10 days); - (address usr, uint48 bgn,, uint48 fin,,, uint128 amt, uint128 rxd) = vest.awards(id); + (address usr, uint48 bgn,, uint48 fin,,, uint128 tot, uint128 rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); - vest.restrict(id); + mVest.restrict(id); - alice.vest(address(vest), id); + alice.vest(address(mVest), id); } function testRestrictions() public { User bob = new User(); - uint256 id = vest.create(address(bob), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + uint256 id = mVest.create(address(bob), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); - assertEq(vest.res(id), 0); + assertEq(mVest.res(id), 0); - bob.restrict(address(vest), id); + bob.restrict(address(mVest), id); - assertEq(vest.res(id), 1); + assertEq(mVest.res(id), 1); - bob.vest(address(vest), id); + bob.vest(address(mVest), id); - bob.unrestrict(address(vest), id); + bob.unrestrict(address(mVest), id); - assertEq(vest.res(id), 0); + assertEq(mVest.res(id), 0); // also test auth ability - vest.restrict(id); + mVest.restrict(id); - assertEq(vest.res(id), 1); + assertEq(mVest.res(id), 1); - vest.unrestrict(id); + mVest.unrestrict(id); - assertEq(vest.res(id), 0); + assertEq(mVest.res(id), 0); } function testFailRestrictNonExistingAward() public { - vest.restrict(9999); + mVest.restrict(9999); } function testFailUnrestrictNonExistingAward() public { - vest.unrestrict(9999); + mVest.unrestrict(9999); } function testFailMgrYankUnauthed() public { Manager manager = new Manager(); - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(1)); - manager.yank(address(vest), id); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(1)); + manager.yank(address(mVest), id); } function testLive() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); - assertTrue(vest.valid(id)); - assertTrue(!vest.valid(5)); + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + assertTrue(mVest.valid(id)); + assertTrue(!mVest.valid(5)); } - function testFailAmtTooHigh() public { - vest.create(address(this), uint128(-1) + 1, block.timestamp, 100 days, 0 days, address(0)); + function testFailTotTooHigh() public { + mVest.create(address(this), uint128(-1) + 1, block.timestamp, 100 days, 0 days, address(0)); } function testFailZeroUser() public { - vest.create(address(0), 100 * days_vest + 1, block.timestamp, 100 days, 0 days, address(0)); + mVest.create(address(0), 100 * days_vest + 1, block.timestamp, 100 days, 0 days, address(0)); } function testFailStartTooFarInTheFuture() public { - vest.create(address(this), 100 * days_vest + 1, block.timestamp + (21 * 365 days), 100 days, 0 days, address(0)); + mVest.create(address(this), 100 * days_vest + 1, block.timestamp + (21 * 365 days), 100 days, 0 days, address(0)); } function testFailStartTooFarInThePast() public { - vest.create(address(this), 100 * days_vest + 1, block.timestamp - (21 * 365 days), 100 days, 0 days, address(0)); + mVest.create(address(this), 100 * days_vest + 1, block.timestamp - (21 * 365 days), 100 days, 0 days, address(0)); } function testFailStartTooLong() public { - vest.create(address(this), 100 * days_vest + 1, block.timestamp, 21 * 365 days, 0 days, address(0)); + mVest.create(address(this), 100 * days_vest + 1, block.timestamp, 21 * 365 days, 0 days, address(0)); } function testFailClfAfterTau() public { - vest.create(address(this), 100 * days_vest + 1, block.timestamp, 100 days, 101 days, address(0)); + mVest.create(address(this), 100 * days_vest + 1, block.timestamp, 100 days, 101 days, address(0)); } function testSuckableVest() public { - uint256 originalSin = VatLikeTest(VAT).sin(VOW); - uint256 id = suckableVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0)); - assertTrue(suckableVest.valid(id)); + uint256 originalSin = vat.sin(VOW); + uint256 id = sVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0)); + assertTrue(sVest.valid(id)); hevm.warp(block.timestamp + 1 days); - suckableVest.vest(id); - assertEq(Token(DAI).balanceOf(address(this)), 1 * days_vest); - assertEq(VatLikeTest(VAT).sin(VOW), originalSin + 1 * days_vest * RAY); + sVest.vest(id); + assertEq(dai.balanceOf(address(this)), 1 * days_vest); + assertEq(vat.sin(VOW), originalSin + 1 * days_vest * RAY); hevm.warp(block.timestamp + 9 days); - suckableVest.vest(id); - assertEq(Token(DAI).balanceOf(address(this)), 10 * days_vest); - assertEq(VatLikeTest(VAT).sin(VOW), originalSin + 10 * days_vest * RAY); + sVest.vest(id); + assertEq(dai.balanceOf(address(this)), 10 * days_vest); + assertEq(vat.sin(VOW), originalSin + 10 * days_vest * RAY); hevm.warp(block.timestamp + 365 days); - suckableVest.vest(id); - assertEq(Token(DAI).balanceOf(address(this)), 100 * days_vest); - assertEq(VatLikeTest(VAT).sin(VOW), originalSin + 100 * days_vest * RAY); + sVest.vest(id); + assertEq(dai.balanceOf(address(this)), 100 * days_vest); + assertEq(vat.sin(VOW), originalSin + 100 * days_vest * RAY); + } + + function testSuckableVestCaged() public { + uint256 originalSin = vat.sin(VOW); + uint256 id = sVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0)); + assertTrue(sVest.valid(id)); + + hevm.warp(block.timestamp + 1 days); + sVest.vest(id); + assertEq(dai.balanceOf(address(this)), 1 * days_vest); + assertEq(vat.sin(VOW), originalSin + 1 * days_vest * RAY); + + hevm.warp(block.timestamp + 9 days); + + // Get End auth to allow call `cage` + hevm.store( + address(end), + keccak256(abi.encode(address(this), uint256(0))), + bytes32(uint256(1)) + ); + end.cage(); + + uint256 when = block.timestamp; + + try sVest.vest(id) { + assertTrue(false); + } catch Error(string memory errmsg) { + assertTrue(vat.live() == 0 && cmpStr(errmsg, "DssVestSuckable/vat-not-live")); + assertEq(dai.balanceOf(address(this)), 1 * days_vest); + assertEq(vat.sin(VOW), 0); // true only if there is more surplus than debt in the system + } catch { + assertTrue(false); + } + + hevm.warp(when + end.wait()); + uint256 vatDebt = vat.debt(); + + // Coerce system surplus to zero to allow end `thaw` execution + hevm.store( + address(vat), + keccak256(abi.encode(address(VOW), uint256(5))), + bytes32(uint256(0)) + ); + + end.thaw(); + + uint256 endDebt = end.debt(); + assertEq(endDebt, vatDebt); } function testCap() public { // Test init at top limit - uint256 id = vest.create(address(this), 500 * WAD, block.timestamp, 365 days, 0, address(0)); + uint256 id = mVest.create(address(this), 500 * WAD, block.timestamp, 365 days, 0, address(0)); assertEq(id, 1); - vest.file("cap", (4000 * WAD) / (4 * 365 days)); + mVest.file("cap", (4000 * WAD) / (4 * 365 days)); - id = vest.create(address(this), 1000 * WAD, block.timestamp, 365 days, 0, address(0)); + id = mVest.create(address(this), 1000 * WAD, block.timestamp, 365 days, 0, address(0)); assertEq(id, 2); } function testFailCap() public { // Test failure at 1 over limit - vest.create(address(this), 501 * WAD, block.timestamp, 365 days, 0, address(0)); + mVest.create(address(this), 501 * WAD, block.timestamp, 365 days, 0, address(0)); } - function testVestPartialAmt() public { - uint256 id = vest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); + function testVestPartialTot() public { + uint256 id = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0 days, address(0)); // Partial vesting hevm.warp(now + 10 days); - (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 amt, uint128 rxd) = vest.awards(id); + (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr,, uint128 tot, uint128 rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 0); - assertEq(vest.unpaid(id), 10 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 0); + assertEq(mVest.unpaid(id), 10 * days_vest); + assertEq(gem.balanceOf(address(this)), 0); - vest.vest(id, 5 * days_vest); + mVest.vest(id, 5 * days_vest); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 5 * days_vest); - assertEq(vest.unpaid(id), 5 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 5 * days_vest); + assertEq(mVest.unpaid(id), 5 * days_vest); + assertEq(gem.balanceOf(address(this)), 5 * days_vest); // Additional partial vesting calls, up to the entire amount owed at this time - vest.vest(id, 3 * days_vest); + mVest.vest(id, 3 * days_vest); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 8 * days_vest); - assertEq(vest.unpaid(id), 2 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 8 * days_vest); + assertEq(mVest.unpaid(id), 2 * days_vest); + assertEq(gem.balanceOf(address(this)), 8 * days_vest); - vest.vest(id, 2 * days_vest); + mVest.vest(id, 2 * days_vest); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 10 days); assertEq(uint256(fin), now + 90 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 10 * days_vest); - assertEq(vest.unpaid(id), 0); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 10 * days_vest); + assertEq(mVest.unpaid(id), 0); + assertEq(gem.balanceOf(address(this)), 10 * days_vest); // Another partial vesting after subsequent elapsed time hevm.warp(now + 40 days); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 50 days); assertEq(uint256(fin), now + 50 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 10 * days_vest); - assertEq(vest.unpaid(id), 40 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 10 * days_vest); + assertEq(mVest.unpaid(id), 40 * days_vest); + assertEq(gem.balanceOf(address(this)), 10 * days_vest); - vest.vest(id, 20 * days_vest); + mVest.vest(id, 20 * days_vest); - (usr, bgn, clf, fin, mgr,, amt, rxd) = vest.awards(id); + (usr, bgn, clf, fin, mgr,, tot, rxd) = mVest.awards(id); assertEq(usr, address(this)); assertEq(uint256(bgn), now - 50 days); assertEq(uint256(fin), now + 50 days); - assertEq(uint256(amt), 100 * days_vest); + assertEq(uint256(tot), 100 * days_vest); assertEq(uint256(rxd), 30 * days_vest); - assertEq(vest.unpaid(id), 20 * days_vest); - assertEq(Token(address(vest.gem())).balanceOf(address(this)), 30 * days_vest); + assertEq(mVest.unpaid(id), 20 * days_vest); + assertEq(gem.balanceOf(address(this)), 30 * days_vest); } function testTransferrableVest() public { - DssVestTransferrable tVest; User usr = new User(); - Manager boss = new Manager(); - hevm.store( - address(DAI), - keccak256(abi.encode(address(boss), uint(2))), - bytes32(uint256(10000 * WAD)) - ); - assertEq(Token(DAI).balanceOf(address(boss)), 10000 * WAD); - - tVest = new DssVestTransferrable(address(boss), address(DAI)); - tVest.file("cap", (2000 * WAD) / (4 * 365 days)); - boss.gemApprove(address(DAI), address(tVest)); uint256 id = tVest.create( address(usr), @@ -741,19 +822,247 @@ contract DssVestTest is DSTest { assertTrue(tVest.valid(id)); hevm.warp(block.timestamp + 1 days); tVest.vest(id); - assertEq(Token(DAI).balanceOf(address(usr)), 1 * days_vest); - assertEq(Token(DAI).balanceOf(address(boss)), 10000 * WAD - 1 * days_vest); + assertEq(dai.balanceOf(address(usr)), 1 * days_vest); + assertEq(dai.balanceOf(address(boss)), 10000 * WAD - 1 * days_vest); hevm.warp(block.timestamp + 9 days); tVest.vest(id); - assertEq(Token(DAI).balanceOf(address(usr)), 10 * days_vest); - assertEq(Token(DAI).balanceOf(address(boss)), 10000 * WAD - 10 * days_vest); + assertEq(dai.balanceOf(address(usr)), 10 * days_vest); + assertEq(dai.balanceOf(address(boss)), 10000 * WAD - 10 * days_vest); hevm.warp(block.timestamp + 365 days); tVest.vest(id); - assertEq(Token(DAI).balanceOf(address(usr)), 100 * days_vest); - assertEq(Token(DAI).balanceOf(address(boss)), 10000 * WAD - 100 * days_vest); + assertEq(dai.balanceOf(address(usr)), 100 * days_vest); + assertEq(dai.balanceOf(address(boss)), 10000 * WAD - 100 * days_vest); hevm.warp(block.timestamp + 365 days); tVest.vest(id); - assertEq(Token(DAI).balanceOf(address(usr)), 100 * days_vest); - assertEq(Token(DAI).balanceOf(address(boss)), 10000 * WAD - 100 * days_vest); + assertEq(dai.balanceOf(address(usr)), 100 * days_vest); + assertEq(dai.balanceOf(address(boss)), 10000 * WAD - 100 * days_vest); + } + + function testWardsSlot0x0() public { + // Load memory slot 0x0 + bytes32 mWards = hevm.load(address(mVest), keccak256(abi.encode(address(this), uint256(0)))); + bytes32 sWards = hevm.load(address(sVest), keccak256(abi.encode(address(this), uint256(0)))); + bytes32 tWards = hevm.load(address(tVest), keccak256(abi.encode(address(this), uint256(0)))); + + // mVest wards + assertEq(mVest.wards(address(this)), uint256(mWards)); // Assert wards = slot wards + assertEq(uint256(mWards), 1); // Assert slot wards == 1 + + // sVest wards + assertEq(sVest.wards(address(this)), uint256(sWards)); // Assert wards = slot wards + assertEq(uint256(sWards), 1); // Assert slot wards == 1 + + // tVest wards + assertEq(tVest.wards(address(this)), uint256(tWards)); // Assert wards = slot wards + assertEq(uint256(tWards), 1); // Assert slot wards == 1 + } + + function testAwardSlot0x1() public { + uint256 mId = mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 10 days, address(0xdead)); + uint256 sId = sVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 10 days, address(0xdead)); + uint256 tId = tVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 10 days, address(0xdead)); + + mVest.restrict(mId); + sVest.restrict(sId); + tVest.restrict(tId); + + hevm.warp(now + 10 days); + + mVest.vest(mId, 5 * days_vest); + sVest.vest(sId, 5 * days_vest); + tVest.vest(tId, 5 * days_vest); + + DssVest.Award memory mmemaward = testUnpackAward(address(mVest), mId); + DssVest.Award memory smemaward = testUnpackAward(address(sVest), sId); + DssVest.Award memory tmemaward = testUnpackAward(address(tVest), tId); + + // Assert usr = slot 0x1 offset 0 awards.usr + assertEq(mVest.usr(mId), mmemaward.usr); + assertEq(sVest.usr(sId), smemaward.usr); + assertEq(tVest.usr(tId), tmemaward.usr); + + // Assert bgn = slot 0x1 offset 0 awards.bgn + assertEq(mVest.bgn(mId), uint256(mmemaward.bgn)); + assertEq(sVest.bgn(sId), uint256(smemaward.bgn)); + assertEq(tVest.bgn(tId), uint256(tmemaward.bgn)); + + // Assert clf = slot 0x1 offset 0 awards.clf + assertEq(mVest.clf(mId), uint256(mmemaward.clf)); + assertEq(sVest.clf(sId), uint256(smemaward.clf)); + assertEq(tVest.clf(tId), uint256(tmemaward.clf)); + + // Assert fin = slot 0x1 offset 1 awards.fin + assertEq(mVest.fin(mId), uint256(mmemaward.fin)); + assertEq(sVest.fin(sId), uint256(smemaward.fin)); + assertEq(tVest.fin(tId), uint256(tmemaward.fin)); + + // Assert mgr = slot 0x1 offset 1 awards.mgr + assertEq(mVest.mgr(mId), mmemaward.mgr); + assertEq(sVest.mgr(sId), smemaward.mgr); + assertEq(tVest.mgr(tId), tmemaward.mgr); + + // Assert res = slot 0x1 offset 1 awards.res + assertEq(mVest.res(mId), uint256(mmemaward.res)); + assertEq(sVest.res(sId), uint256(smemaward.res)); + assertEq(tVest.res(tId), uint256(tmemaward.res)); + + // Assert tot = slot 0x1 offset 2 awards.tot + assertEq(mVest.tot(mId), uint256(mmemaward.tot)); + assertEq(sVest.tot(sId), uint256(smemaward.tot)); + assertEq(tVest.tot(tId), uint256(tmemaward.tot)); + + // Assert rxd = slot 0x1 offset 2 awards.rxd + assertEq(mVest.rxd(mId), uint256(mmemaward.rxd)); + assertEq(sVest.rxd(sId), uint256(smemaward.rxd)); + assertEq(tVest.rxd(tId), uint256(tmemaward.rxd)); + } + + function testUnpackAward(address vest, uint256 id) internal returns (DssVest.Award memory award) { + // Load memory slot 0x1 offset 0 + bytes32 awardsPacked0x10 = hevm.load(address(vest), keccak256(abi.encode(uint256(id), uint256(1)))); + + // Load memory slot 0x1 offset 1 + bytes32 awardsPacked0x11 = hevm.load(address(vest), bytes32(uint256(1) + uint256(keccak256(abi.encode(uint256(id), uint256(1)))))); + + // Load memory slot 0x1 offset 2 + bytes32 awardsPacked0x12 = hevm.load(address(vest), bytes32(uint256(2) + uint256(keccak256(abi.encode(uint256(id), uint256(1)))))); + + // Unpack memory slot 0x1 offset 0 + bytes20 memusr; + bytes6 membgn; + bytes6 memclf; + assembly { + memclf := awardsPacked0x10 + membgn := shl(48, awardsPacked0x10) + memusr := shl(96, awardsPacked0x10) + } + + // Unpack memory slot 0x1 offset 1 + bytes6 memfin; + bytes20 memmgr; + bytes1 memres; + assembly { + memres := shl(40, awardsPacked0x11) + memmgr := shl(48, awardsPacked0x11) + memfin := shl(208, awardsPacked0x11) + } + + // Unpack memory slot 0x1 offset 2 + bytes16 memtot; + bytes16 memrxd; + assembly { + memrxd := awardsPacked0x12 + memtot := shl(128, awardsPacked0x12) + } + + // awards.usr + assertEq(address(uint160(memusr)), address(this)); // Assert slot awards.usr == address(this) + + // awards.bgn + assertEq(uint256(uint48(membgn)), block.timestamp - 10 days); // Assert slot awards.bgn == block.timestamp - 10 days + + // awards.clf + assertEq(uint256(uint48(memclf)), block.timestamp); // Assert slot awards.clf == bgn + eta + + // awards.fin + assertEq(uint256(uint48(memfin)), block.timestamp + 90 days); // Assert slot awards.fin == bgn + tau + + // awards.mgr + assertEq(address(uint160(memmgr)), address(0xdead)); // Assert slot awards.mgr == address(0xdead) + + // awards.res + assertEq(uint256(uint8(memres)), 1); // Assert slot awards.res == 1 + + // awards.tot + assertEq(uint256(uint128(memtot)), 100 * days_vest); // Assert slot awards.tot == 100 * days_vest + + // awards.rxd + assertEq(uint256(uint128(memrxd)), 5 * days_vest); // Assert slot awards.rxd == 5 * days_vest + + return ( + DssVest.Award({ + usr: address(uint160(memusr)), + bgn: uint48(membgn), + clf: uint48(memclf), + fin: uint48(memfin), + mgr: address(uint160(memmgr)), + res: uint8(memres), + tot: uint128(memtot), + rxd: uint128(memrxd) + }) + ); + } + + function testCapSlot0x2() public { + // Load memory slot 0x2 + bytes32 mCap = hevm.load(address(mVest), bytes32(uint256(2))); + bytes32 sCap = hevm.load(address(sVest), bytes32(uint256(2))); + bytes32 tCap = hevm.load(address(tVest), bytes32(uint256(2))); + + // mVest cap + assertEq(mVest.cap(), uint256(mCap)); // Assert cap = slot cap + assertEq(uint256(mCap), (2000 * WAD) / (4 * 365 days)); // Assert slot cap == (2000 * WAD) / (4 * 365 days) + + // sVest cap + assertEq(sVest.cap(), uint256(sCap)); // Assert cap = slot cap + assertEq(uint256(sCap), (2000 * WAD) / (4 * 365 days)); // Assert slot cap == (2000 * WAD) / (4 * 365 days) + + // tVest cap + assertEq(tVest.cap(), uint256(tCap)); // Assert cap = slot cap + assertEq(uint256(tCap), (2000 * WAD) / (4 * 365 days)); // Assert slot cap == (2000 * WAD) / (4 * 365 days) + } + + function testIdsSlot0x3() public { + mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0xdead)); + sVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0xdead)); + tVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0xdead)); + + // Load memory slot 0x3 + bytes32 mIds = hevm.load(address(mVest), bytes32(uint256(3))); + bytes32 sIds = hevm.load(address(sVest), bytes32(uint256(3))); + bytes32 tIds = hevm.load(address(tVest), bytes32(uint256(3))); + + // mVest ids + assertEq(mVest.ids(), uint256(mIds)); // Assert ids = slot ids + assertEq(uint256(mIds), 1); // Assert slot ids == 1 + + // sVest ids + assertEq(sVest.ids(), uint256(sIds)); // Assert ids = slot ids + assertEq(uint256(sIds), 1); // Assert slot ids == 1 + + // tVest ids + assertEq(tVest.ids(), uint256(tIds)); // Assert ids = slot ids + assertEq(uint256(tIds), 1); // Assert slot ids == 1 + } + + function cmpStr(string memory a, string memory b) internal pure returns (bool) { + return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); + } + function testLockedSlot0x4() public { + // Store memory slot 0x4 + hevm.store(address(mVest), bytes32(uint256(4)), bytes32(uint256(1))); + hevm.store(address(sVest), bytes32(uint256(4)), bytes32(uint256(1))); + hevm.store(address(tVest), bytes32(uint256(4)), bytes32(uint256(1))); + + // mVest locked + try mVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0xdead)) {} + catch Error(string memory errmsg) { + bytes32 mLocked = hevm.load(address(mVest), bytes32(uint256(4))); // Load memory slot 0x4 + assertTrue(uint256(mLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked")); // Assert slot locked == 1 and function reverts + } + + // sVest locked + try sVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0xdead)) {} + catch Error(string memory errmsg) { + bytes32 sLocked = hevm.load(address(sVest), bytes32(uint256(4))); // Load memory slot 0x4 + assertTrue(uint256(sLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked")); // Assert slot locked == 1 and function reverts + } + + // tVest locked + try tVest.create(address(this), 100 * days_vest, block.timestamp, 100 days, 0, address(0xdead)) {} + catch Error(string memory errmsg) { + bytes32 tLocked = hevm.load(address(tVest), bytes32(uint256(4))); // Load memory slot 0x4 + assertTrue(uint256(tLocked) == 1 && cmpStr(errmsg, "DssVest/system-locked")); // Assert slot locked == 1 and function reverts + } } }