├── .env.example ├── .husky └── pre-commit ├── remappings.txt ├── .gitignore ├── src ├── interfaces │ ├── IMintable.sol │ ├── IMintVest.sol │ ├── ITransferVest.sol │ └── IVest.sol ├── MintVest.sol ├── TransferVest.sol └── Vest.sol ├── .vscode └── settings.json ├── .gitmodules ├── foundry.toml ├── package.json ├── scripts ├── DeployMintVest.s.sol └── DeployTransferVest.s.sol ├── .github └── workflows │ └── test.yml ├── test ├── helpers │ └── BaseTest.sol ├── MintVest.t.sol ├── TransferVest.t.sol └── Vest.t.sol ├── LICENSE ├── README.md └── .gas-snapshot /.env.example: -------------------------------------------------------------------------------- 1 | # This key will be used for the deployment 2 | DEPLOYER_KEY = "" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run format -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | solmate/=lib/solmate/src/ 2 | morpho-utils/=lib/morpho-utils/src/ 3 | forge-std/=lib/forge-std/src/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # npm 17 | package-lock.json 18 | node_modules -------------------------------------------------------------------------------- /src/interfaces/IMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title IMintable 5 | /// @author MerlinEgalite 6 | /// @notice Smallest interface for a mintable token. 7 | interface IMintable { 8 | function mint(address to, uint256 amount) external; 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[solidity]": { 3 | "editor.formatOnSave": false 4 | }, 5 | "emeraldwalk.runonsave": { 6 | "commands": [ 7 | { 8 | "match": ".sol", 9 | "isAsync": true, 10 | "cmd": "forge fmt ${file}" 11 | }, 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | branch = v1.5.2 5 | [submodule "lib/solmate"] 6 | path = lib/solmate 7 | url = https://github.com/transmissions11/solmate 8 | [submodule "lib/morpho-utils"] 9 | path = lib/morpho-utils 10 | url = https://github.com/morpho-dao/morpho-utils 11 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | [invariant] 7 | runs = 4 8 | depth = 64 9 | 10 | [fuzz] 11 | runs = 512 12 | 13 | [profile.ci.fuzz] 14 | runs = 512 15 | 16 | [profile.ci.invariant] 17 | runs = 8 18 | depth = 256 19 | 20 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /src/interfaces/IMintVest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | 4 | import {IVest} from "./IVest.sol"; 5 | 6 | /// @title IMintVest 7 | /// @author MerlinEgalite 8 | /// @notice Interface that the MintVest contract must implement. 9 | interface IMintVest is IVest { 10 | function getToken() external view returns (address); 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/ITransferVest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | 4 | import {IVest} from "./IVest.sol"; 5 | 6 | /// @title ITransferVest 7 | /// @author MerlinEgalite 8 | /// @notice Interface that the TransferVest contract must implement. 9 | interface ITransferVest is IVest { 10 | function getSender() external view returns (address); 11 | function getToken() external view returns (address); 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solvest", 3 | "version": "1.0.0", 4 | "description": "Easy to use vesting contracts written in Solidity", 5 | "directories": { 6 | "lib": "lib", 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "forge test", 11 | "format": "forge fmt" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/MerlinEgalite/solvest.git" 16 | }, 17 | "author": "Merlin Egalite", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/MerlinEgalite/solvest/issues" 21 | }, 22 | "homepage": "https://github.com/MerlinEgalite/solvest#readme", 23 | "devDependencies": { 24 | "husky": "^8.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/DeployMintVest.s.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.17; 2 | 3 | import "forge-std/Script.sol"; 4 | 5 | import {MintVest} from "../src/MintVest.sol"; 6 | 7 | contract DeployMintVest is Script{ 8 | 9 | /// @dev Change these values before running the script! 10 | address public owner = address(0xCAFE); 11 | address public token = address(0xBEEF); 12 | 13 | function run() public { 14 | uint256 deployerPrivateKey = vm.envUint("DEPLOYER_KEY"); 15 | vm.startBroadcast(deployerPrivateKey); 16 | address deployedInstance = address(new MintVest(owner,token)); 17 | console.log("MintVest contract deployed at: ",deployedInstance, " for the token: ", token); 18 | vm.stopBroadcast(); 19 | } 20 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: push 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /scripts/DeployTransferVest.s.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.17; 2 | 3 | import "forge-std/Script.sol"; 4 | 5 | import {TransferVest} from "../src/TransferVest.sol"; 6 | 7 | contract DeployTransferVest is Script{ 8 | 9 | /// @dev Change these values before running the script! 10 | address public owner = address(0xCAFE); 11 | address public token = address(0xBEEF); 12 | address public sender = address(0xFACE); 13 | 14 | function run() public { 15 | uint256 deployerPrivateKey = vm.envUint("DEPLOYER_KEY"); 16 | vm.startBroadcast(deployerPrivateKey); 17 | address deployedInstance = address(new TransferVest(owner, sender, token)); 18 | console.log("TransferVest contract deployed at: ", deployedInstance, " for the token: ", token); 19 | vm.stopBroadcast(); 20 | } 21 | } -------------------------------------------------------------------------------- /test/helpers/BaseTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | contract BaseTest is Test { 7 | uint256 internal constant OFFSET = 1_000; 8 | uint256 internal constant TOTAL = 1_000; 9 | uint256 internal constant TWENTY_YEARS = 20 * 365 days; 10 | uint256 internal constant DURATION = 3 * 365 days; 11 | 12 | uint256 internal immutable START; 13 | 14 | constructor() { 15 | START = block.timestamp + 30 days; 16 | } 17 | 18 | function _boundAddressNotZero(address input) internal view virtual returns (address) { 19 | return address(uint160(bound(uint256(uint160(input)), 1, type(uint160).max))); 20 | } 21 | 22 | function _boundStart(uint256 start) internal view virtual returns (uint256) { 23 | return bound(start, OFFSET + 1, block.timestamp + TWENTY_YEARS - 1); 24 | } 25 | 26 | function _boundId(uint256 id) internal pure returns (uint256) { 27 | return (id == 1) ? 0 : id; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Merlin Egalite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/MintVest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {IMintVest} from "./interfaces/IMintVest.sol"; 5 | import {IMintable} from "./interfaces/IMintable.sol"; 6 | 7 | import {Vest} from "./Vest.sol"; 8 | 9 | /// @title MintVest 10 | /// @author MerlinEgalite 11 | /// @notice MintVest contract allowing an owner to create and manage vestings. 12 | /// @dev Claimed tokens are directly minted on the token. 13 | contract MintVest is IMintVest, Vest { 14 | /* IMMUTABLES */ 15 | 16 | /// @dev The token being vested. 17 | address internal immutable _token; 18 | 19 | /* CONSTRUCTOR */ 20 | 21 | /// @notice Constructs the contract. 22 | /// @param owner The owner of the contract. 23 | /// @param token The token being vested. 24 | constructor(address owner, address token) Vest(owner) { 25 | if (token == address(0)) revert AddressIsZero(); 26 | _token = token; 27 | } 28 | 29 | /* EXTERNAL */ 30 | 31 | /// @notice Returns the token being vested. 32 | function getToken() external view returns (address) { 33 | return _token; 34 | } 35 | 36 | /* INTERNAL */ 37 | 38 | /// @dev Mints `amount` of tokens to the `receiver`. 39 | function _transfer(address receiver, uint256 amount) internal override { 40 | IMintable(_token).mint(receiver, amount); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/TransferVest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {ITransferVest} from "./interfaces/ITransferVest.sol"; 5 | 6 | import {ERC20, SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; 7 | 8 | import {Vest} from "./Vest.sol"; 9 | 10 | /// @title TransferVest 11 | /// @author MerlinEgalite 12 | /// @notice TransferVest contract allowing an owner to create and manage vestings. 13 | /// @dev Claimed tokens are directly transferred from a sender to the receivers. 14 | contract TransferVest is ITransferVest, Vest { 15 | using SafeTransferLib for ERC20; 16 | 17 | /* IMMUTABLES */ 18 | 19 | /// @dev The sender of the tokens. 20 | address internal immutable _sender; 21 | 22 | /// @dev The token being vested. 23 | address internal immutable _token; 24 | 25 | /* CONSTRUCTOR */ 26 | 27 | /// @notice Constructs the contract. 28 | /// @param owner The owner of the contract. 29 | /// @param sender The sender of the vested token. 30 | /// @param token The token being vested. 31 | constructor(address owner, address sender, address token) Vest(owner) { 32 | if (sender == address(0) || token == address(0)) revert AddressIsZero(); 33 | _sender = sender; 34 | _token = token; 35 | } 36 | 37 | /* EXTERNAL */ 38 | 39 | /// @notice Returns the sender of the tokens. 40 | function getSender() external view returns (address) { 41 | return _sender; 42 | } 43 | 44 | /// @notice Returns the token being vested. 45 | function getToken() external view returns (address) { 46 | return _token; 47 | } 48 | 49 | /* INTERNAL */ 50 | 51 | /// @dev Transfers `amount` of tokens from the `sender` to the `receiver`. 52 | function _transfer(address receiver, uint256 amount) internal override { 53 | ERC20(_token).safeTransferFrom(_sender, receiver, amount); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/MintVest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.0; 3 | 4 | import "../src/interfaces/IVest.sol"; 5 | 6 | import "solmate/test/utils/mocks/MockERC20.sol"; 7 | import "./helpers/BaseTest.sol"; 8 | import "../src/MintVest.sol"; 9 | 10 | contract MockMintVest is MintVest { 11 | constructor(address token) MintVest(msg.sender, token) {} 12 | } 13 | 14 | contract MintVestTest is BaseTest { 15 | MockMintVest internal vest; 16 | ERC20 internal token; 17 | 18 | function setUp() public { 19 | token = new MockERC20("Test", "TST", 18); 20 | vest = new MockMintVest(address(token)); 21 | 22 | vm.warp(TWENTY_YEARS + OFFSET); 23 | } 24 | 25 | function testMintVestDeploymentShouldFailWhenAddressIsZero() public { 26 | vm.expectRevert(IVest.AddressIsZero.selector); 27 | new MockMintVest(address(0)); 28 | } 29 | 30 | function testGetToken() public { 31 | assertEq(vest.getToken(), address(token)); 32 | } 33 | 34 | function testClaimAndMintTokensAfterCliff( 35 | address receiver, 36 | uint256 start, 37 | uint256 cliffDuration, 38 | uint256 duration, 39 | address manager, 40 | bool restricted, 41 | bool protected, 42 | uint256 total, 43 | uint256 claimTime 44 | ) public { 45 | receiver = _boundAddressNotZero(receiver); 46 | start = _boundStart(start); 47 | duration = bound(duration, OFFSET, TWENTY_YEARS); 48 | cliffDuration = bound(cliffDuration, 0, duration); 49 | claimTime = bound(claimTime, start + cliffDuration, type(uint128).max); 50 | total = bound(total, TOTAL, type(uint128).max); 51 | 52 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 53 | 54 | vm.warp(claimTime); 55 | 56 | vm.prank(receiver); 57 | vest.claim(id); 58 | uint256 accrued = vest.getAccrued(block.timestamp, start, start + duration, total); 59 | 60 | Vest.Vesting memory vesting = vest.getVesting(id); 61 | 62 | assertEq(vesting.claimed, accrued); 63 | assertEq(ERC20(token).balanceOf(receiver), accrued); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solvest 2 | 3 | Improved version of the [MakerDAO dss-vest contracts](https://github.com/makerdao/dss-vest/blob/master). Solvest allows to easily create multiple vesting plans with different parameters. 4 | 5 | Most noticeable differences compared to `dss-vest`: 6 | 7 | - Better naming. 8 | - There is only one single owner for a vesting contract instead of multiple owners. 9 | - The owner can protect/unprotect a vesting plan to be revoked by a manager. 10 | - The `cap` has been removed. 11 | 12 | ## [MintVest](./src/MintVest.sol) 13 | 14 | Pass the address of the vesting token to the constructor on deploy. This contract must be given authority to `mint()` tokens in the vesting contract. 15 | 16 | ## [TransferVest](./src/TransferVest.sol) 17 | 18 | Pass the authorized sender address and the address of the token contract to the constructor to set up the contract for streaming arbitrary ERC20 tokens. Note: this contract must be given approval by the sender to spend tokens on its behalf. 19 | 20 | ## Installation 21 | 22 | Download foundry: 23 | 24 | ```bash 25 | curl -L https://foundry.paradigm.xyz | bash 26 | ``` 27 | 28 | Install it: 29 | 30 | ```bash 31 | foundryup 32 | ``` 33 | 34 | Install dependencies: 35 | 36 | ```bash 37 | git submodule update --init --recursive 38 | ``` 39 | 40 | Now you can run tests, using forge: 41 | 42 | ```bash 43 | forge test 44 | ``` 45 | 46 | ## Deployment 47 | 48 | - Create a `.env` file and add the `DEPLOYER_KEY` to the env file. 49 | 50 | - To deploy the `MintVest` contract, run the following command. 51 | 52 | ```sh 53 | forge script scripts/DeployMintVest.s.sol:DeployMintVest --broadcast --rpc-url -vvvv 54 | ``` 55 | 56 | - To deploy the `TransferVest` contract, run the following command. 57 | 58 | ```sh 59 | forge script scripts/DeployTransferVest.s.sol:DeployTransferVest --broadcast --rpc-url -vvvv 60 | ``` 61 | 62 | ## DssVest <> Solvest translation 63 | 64 | | DssVest | Solvest | 65 | | ------- | ----------- | 66 | | wards | owner | 67 | | bgn | start | 68 | | clf | cliff | 69 | | fin | end | 70 | | mgr | manager | 71 | | res | restricted | 72 | | tot | total | 73 | | rxd | claimed | 74 | | awards | vestings | 75 | | vest | claim | 76 | | unpaid | unclaimed | 77 | | yank | revoke | 78 | | pay | transfer | 79 | | move | setReceiver | 80 | | czar | sender | 81 | | gem | token | 82 | -------------------------------------------------------------------------------- /test/TransferVest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.0; 3 | 4 | import "../src/interfaces/IVest.sol"; 5 | 6 | import "solmate/test/utils/mocks/MockERC20.sol"; 7 | import "./helpers/BaseTest.sol"; 8 | import "../src/TransferVest.sol"; 9 | 10 | contract MockTransferVest is TransferVest { 11 | constructor(address sender, address token) TransferVest(msg.sender, sender, token) {} 12 | } 13 | 14 | contract TransferVestTest is BaseTest { 15 | MockTransferVest internal vest; 16 | ERC20 internal token; 17 | address internal sender = address(0x1); 18 | 19 | function setUp() public { 20 | token = new MockERC20("Test", "TST", 18); 21 | deal(address(token), sender, type(uint128).max); 22 | 23 | vest = new MockTransferVest(sender, address(token)); 24 | 25 | vm.prank(sender); 26 | token.approve(address(vest), type(uint256).max); 27 | 28 | vm.warp(TWENTY_YEARS + OFFSET); 29 | } 30 | 31 | function testTransferVestDeploymentShouldFailWhenAddressIsZero() public { 32 | vm.expectRevert(IVest.AddressIsZero.selector); 33 | new MockTransferVest(address(0), address(token)); 34 | 35 | vm.expectRevert(IVest.AddressIsZero.selector); 36 | new MockTransferVest(sender, address(0)); 37 | } 38 | 39 | function testGetSender() public { 40 | assertEq(vest.getSender(), sender); 41 | } 42 | 43 | function testGetToken() public { 44 | assertEq(vest.getToken(), address(token)); 45 | } 46 | 47 | function testClaimAndTranfserTokensAfterCliff( 48 | address receiver, 49 | uint256 start, 50 | uint256 cliffDuration, 51 | uint256 duration, 52 | address manager, 53 | bool restricted, 54 | bool protected, 55 | uint256 total, 56 | uint256 claimTime 57 | ) public { 58 | receiver = _boundAddressNotZero(receiver); 59 | vm.assume(receiver != sender); 60 | start = _boundStart(start); 61 | duration = bound(duration, OFFSET, TWENTY_YEARS); 62 | cliffDuration = bound(cliffDuration, 0, duration); 63 | claimTime = bound(claimTime, start + cliffDuration, type(uint128).max); 64 | total = bound(total, TOTAL, type(uint128).max); 65 | 66 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 67 | 68 | vm.warp(claimTime); 69 | 70 | vm.prank(receiver); 71 | vest.claim(id); 72 | uint256 accrued = vest.getAccrued(block.timestamp, start, start + duration, total); 73 | 74 | Vest.Vesting memory vesting = vest.getVesting(id); 75 | 76 | assertEq(vesting.claimed, accrued); 77 | assertEq(ERC20(token).balanceOf(receiver), accrued); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/interfaces/IVest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | 4 | interface IVest { 5 | /* STRUCTS */ 6 | 7 | struct Vesting { 8 | address receiver; // The receiver of the vesting. 9 | uint48 start; // The start time of the vesting. 10 | uint48 cliff; // The end of the cliff. 11 | uint48 end; // The end of the vesting. 12 | address manager; // The manager of the vesting that can claim the tokens if the vesting is not restricted. 13 | bool restricted; // True if the manager cannot claim tokens on behalf of receiver. 14 | bool protected; // True if the vesting cannot be revoked. 15 | uint128 total; // The total amount of vested tokens. 16 | uint128 claimed; // The amount of tokens already claimed. 17 | } 18 | 19 | /* ERRORS */ 20 | 21 | /// @notice Thrown when the sender has not the permission to call the function. 22 | error PermissionDenied(); 23 | 24 | /// @notice Thrown when only the receiver can call the function. 25 | error OnlyReceiver(); 26 | 27 | /// @notice Thrown when the address passed as argument is the zero address. 28 | error AddressIsZero(); 29 | 30 | /// @notice Thrown when the total amount of tokens is zero for a new vesting. 31 | error TotalIsZero(); 32 | 33 | /// @notice Thrown when the start time is too far in the future for a new vesting. 34 | error StartTooFar(); 35 | 36 | /// @notice Thrown when the start time is too far in the past for a new vesting. 37 | error StartTooLongAgo(); 38 | 39 | /// @notice Thrown when the duration is zero for a new vesting. 40 | error DurationIsZero(); 41 | 42 | /// @notice Thrown when the duration is too long for a new vesting. 43 | error DurationTooLong(); 44 | 45 | /// @notice Thrown when the cliff duration is too long for a new vesting. 46 | error CliffDurationTooLong(); 47 | 48 | /// @notice Thrown when the vesting does not exist. 49 | error InvalidVestingId(); 50 | 51 | /// @notice Thrown when the vesting is not revokable. 52 | error VestingIsProtected(); 53 | 54 | /* EVENTS */ 55 | 56 | /// @notice Emitted when a vesting is created with `id` and `receiver`. 57 | event VestingCreated(uint256 id, address receiver); 58 | 59 | /// @notice Emitted when the vesting `id` is revoked at `end`. 60 | event VestingRevoked(uint256 id, uint256 end); 61 | 62 | /// @notice Emitted when an `amount` of tokens is claimed for the vesting `id`. 63 | event Claimed(uint256 id, uint256 amount); 64 | 65 | /// @notice Emitted when the vesting `id` is protected. 66 | event VestingProtected(uint256 id); 67 | 68 | /// @notice Emitted when the vesting `id` is unprotected. 69 | event VestingUnprotected(uint256 id); 70 | 71 | /// @notice Emitted when the vesting `id` is restricted. 72 | event VestingRestricted(uint256 id); 73 | 74 | /// @notice Emitted when the vesting `id` is unrestricted. 75 | event VestingUnrestricted(uint256 id); 76 | 77 | /// @notice Emitted when the receiver of vesting `id` is set to `receiver`. 78 | event ReceiverSet(uint256 id, address receiver); 79 | 80 | /* GETTERS */ 81 | 82 | function TWENTY_YEARS() external pure returns (uint256); 83 | function ids() external view returns (uint256); 84 | function getVesting(uint256 id) external view returns (Vesting memory); 85 | function getUnclaimed(uint256 id) external view returns (uint256); 86 | 87 | /* EXTERNAL */ 88 | 89 | function create( 90 | address receiver, 91 | uint256 start, 92 | uint256 cliff, 93 | uint256 duration, 94 | address manager, 95 | bool restricted, 96 | bool protected, 97 | uint256 total 98 | ) external returns (uint256 id); 99 | function revoke(uint256 id) external; 100 | function revoke(uint256 id, uint256 end) external; 101 | function claim(uint256 id) external; 102 | function protect(uint256 id) external; 103 | function unprotect(uint256 id) external; 104 | function restrict(uint256 id) external; 105 | function unrestrict(uint256 id) external; 106 | function setReceiver(uint256 id, address receiver) external; 107 | } 108 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | MintVestTest:testClaimAndMintTokensAfterCliff(address,uint256,uint256,uint256,address,bool,bool,uint256,uint256) (runs: 512, μ: 178748, ~: 179300) 2 | MintVestTest:testGetToken() (gas: 7708) 3 | MintVestTest:testMintVestDeploymentShouldFailWhenAddressIsZero() (gas: 60211) 4 | TransferVestTest:testClaimAndTranfserTokensAfterCliff(address,uint256,uint256,uint256,address,bool,bool,uint256,uint256) (runs: 512, μ: 166713, ~: 167144) 5 | TransferVestTest:testGetSender() (gas: 7718) 6 | TransferVestTest:testGetToken() (gas: 7752) 7 | TransferVestTest:testTransferVestDeploymentShouldFailWhenAddressIsZero() (gas: 121988) 8 | VestTest:invariantClaimed() (runs: 4, calls: 256, reverts: 200) 9 | VestTest:invariantDeadlines() (runs: 4, calls: 256, reverts: 200) 10 | VestTest:invariantUnclaimed() (runs: 4, calls: 256, reverts: 200) 11 | VestTest:testAccrued(uint256,uint256,uint256,uint256) (runs: 512, μ: 16829, ~: 16857) 12 | VestTest:testAccruedWithTimeAfterEnd(uint256,uint256,uint256,uint256) (runs: 512, μ: 13807, ~: 13690) 13 | VestTest:testAccruedWithTimeBeforeStart(uint256,uint256,uint256,uint256) (runs: 512, μ: 11831, ~: 11513) 14 | VestTest:testClaimAfterCliff(address,uint256,uint256,uint256,address,bool,bool,uint256,uint256) (runs: 512, μ: 246581, ~: 246604) 15 | VestTest:testClaimAfterCliffWithMaxAmount(address,uint256,uint256,uint256,address,bool,bool,uint256,uint256,uint256) (runs: 512, μ: 247719, ~: 247800) 16 | VestTest:testClaimBeforeCliff(address,uint256,uint256,uint256,address,bool,bool,uint256,uint256) (runs: 512, μ: 243701, ~: 243698) 17 | VestTest:testClaimBeforeCliffWithMaxAmount(address,uint256,uint256,uint256,address,bool,bool,uint256,uint256,uint256) (runs: 512, μ: 244827, ~: 244842) 18 | VestTest:testClaimCalledByManagerNotRestricted(address,uint256,uint256,uint256,address,bool,uint256) (runs: 512, μ: 236984, ~: 236948) 19 | VestTest:testClaimCalledByReceiver(address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 236769, ~: 236731) 20 | VestTest:testClaimShouldRevertWhenCalledByNotReceiverAndNoManagerSet(address,address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 117683, ~: 117763) 21 | VestTest:testClaimShouldRevertWhenCalledByNotReceiverAndNotManagerButManagerSet(address,address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 117686, ~: 117766) 22 | VestTest:testClaimShouldRevertWhenCalledByRestrictedManager(address,uint256,uint256,uint256,address,bool,uint256) (runs: 512, μ: 117000, ~: 117088) 23 | VestTest:testCreate(address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 120709, ~: 120840) 24 | VestTest:testCreateShouldRevertWhenCalledByNotOwner(address,address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 24525, ~: 24470) 25 | VestTest:testCreateShouldRevertWhenCliffIsTooLong(address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 24055, ~: 24114) 26 | VestTest:testCreateShouldRevertWhenDurationIsTooLong(address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 24044, ~: 24108) 27 | VestTest:testCreateShouldRevertWhenDurationIsZero(address,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 21834, ~: 21974) 28 | VestTest:testCreateShouldRevertWhenReceiverIsZero(uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 21848, ~: 21852) 29 | VestTest:testCreateShouldRevertWhenStartIsTooFar(address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 23819, ~: 23904) 30 | VestTest:testCreateShouldRevertWhenStartTooLongAgo(address,uint256,uint256,uint256,address,bool,bool,uint256) (runs: 512, μ: 23672, ~: 23762) 31 | VestTest:testCreateShouldRevertWhenTotalIsZero(address,uint256,uint256,uint256,address,bool,bool) (runs: 512, μ: 21774, ~: 21912) 32 | VestTest:testIds(uint8) (runs: 512, μ: 7328767, ~: 5075989) 33 | VestTest:testOwner(address) (runs: 512, μ: 983298, ~: 983298) 34 | VestTest:testProtect() (gas: 111902) 35 | VestTest:testProtectShouldRevertWhenCalledByNotOwner(address) (runs: 512, μ: 107923, ~: 107923) 36 | VestTest:testProtectShouldRevertWhenInvalidId(uint256) (runs: 512, μ: 108280, ~: 108281) 37 | VestTest:testRestrictCalledByOwner() (gas: 112106) 38 | VestTest:testRestrictCalledByReceiver() (gas: 112551) 39 | VestTest:testRestrictShouldRevertWhenCalledByNotOwnerNorReceiver(address) (runs: 512, μ: 108836, ~: 108836) 40 | VestTest:testRestrictShouldRevertWhenInvalidId(uint256) (runs: 512, μ: 108139, ~: 108140) 41 | VestTest:testRevokeAfterCliffAndBeforeEnd(uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 131729, ~: 131794) 42 | VestTest:testRevokeAfterEnd(uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 122885, ~: 122895) 43 | VestTest:testRevokeBeforeCliff(uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 106804, ~: 106913) 44 | VestTest:testRevokeBeforeStart(uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 106668, ~: 106738) 45 | VestTest:testRevokeEndShouldAtLeastBlockTimestamp(uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 105618, ~: 102211) 46 | VestTest:testRevokeShouldRevertWhenCalledByManagerAndVestingProtected() (gas: 108920) 47 | VestTest:testRevokeShouldRevertWhenCalledByNotOwnerAndNotManagerAndVestingNotProtected(address) (runs: 512, μ: 109911, ~: 109911) 48 | VestTest:testRevokeShouldRevertWhenCalledByNotOwnerAndVestingProtected(address) (runs: 512, μ: 107309, ~: 107309) 49 | VestTest:testRevokeShouldRevertWhenInvalidId(uint256) (runs: 512, μ: 11263, ~: 11264) 50 | VestTest:testRevokeWhenCalledByManagerAndNotVestingProtected() (gas: 108736) 51 | VestTest:testRevokeWhenCalledByOwnerAndVestingProtected() (gas: 108405) 52 | VestTest:testSetReceiver(address) (runs: 512, μ: 117154, ~: 117154) 53 | VestTest:testSetReceiverShouldRevertWhenAddressZero() (gas: 106426) 54 | VestTest:testSetReceiverShouldRevertWhenCalledByNotReceiver(address,address) (runs: 512, μ: 111225, ~: 111225) 55 | VestTest:testUnclaimedAfterCliff(uint256,uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 246779, ~: 246748) 56 | VestTest:testUnclaimedBeforeCliff(uint256,uint256,uint256,uint256,uint256) (runs: 512, μ: 118651, ~: 118651) 57 | VestTest:testUnprotect() (gas: 111913) 58 | VestTest:testUnprotectShouldRevertWhenInvalidId(uint256) (runs: 512, μ: 108270, ~: 108271) 59 | VestTest:testUnrestrictCalledByOwner() (gas: 112096) 60 | VestTest:testUnrestrictCalledByReceiver() (gas: 112475) 61 | VestTest:testUnrestrictShouldRevertWhenCalledByNotOwnerNorReceiver(address) (runs: 512, μ: 108748, ~: 108748) 62 | VestTest:testUnrestrictShouldRevertWhenInvalidId(uint256) (runs: 512, μ: 108072, ~: 108073) 63 | VestTest:testUnrotectShouldRevertWhenCalledByNotOwner(address) (runs: 512, μ: 107891, ~: 107891) 64 | VestTest:testValidateId() (gas: 102965) 65 | VestTest:testValidateIdShouldRevertWhenNotAVesting(uint256) (runs: 512, μ: 10770, ~: 10770) 66 | VestTest:testValidateIdWhenIdIsZero() (gas: 107880) -------------------------------------------------------------------------------- /src/Vest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {IVest} from "./interfaces/IVest.sol"; 5 | 6 | import {Owned} from "solmate/auth/Owned.sol"; 7 | import {Math} from "morpho-utils/math/Math.sol"; 8 | import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; 9 | 10 | /// @title Vest 11 | /// @author MerlinEgalite 12 | /// @notice Abstract contract allowing an owner to create and manage vestings. 13 | /// @dev Modified and improved version of https://github.com/makerdao/dss-vest. 14 | abstract contract Vest is IVest, Owned { 15 | using SafeCastLib for uint256; 16 | 17 | /* CONSTANTS */ 18 | 19 | /// @dev 20 years in seconds. 20 | uint256 internal constant _TWENTY_YEARS = 20 * 365 days; 21 | 22 | /* STORAGE */ 23 | 24 | /// @dev The total number of created vestings. 25 | uint256 internal _ids; 26 | 27 | /// @dev Maps an id to a vesting configuration. 28 | mapping(uint256 => Vesting) internal _vestings; 29 | 30 | /* CONSTRUCTOR */ 31 | 32 | /// @notice Constructs and sets `owner` as owner of the contract. 33 | constructor(address owner) Owned(owner) {} 34 | 35 | /* GETTERS */ 36 | 37 | /// @notice Returns the number of seconds in 20 years. 38 | function TWENTY_YEARS() external pure returns (uint256) { 39 | return _TWENTY_YEARS; 40 | } 41 | 42 | /// @notice Returns the number of created vestings. 43 | function ids() external view returns (uint256) { 44 | return _ids; 45 | } 46 | 47 | /// @notice Returns the vesting data of the vesting `id`. 48 | function getVesting(uint256 id) external view returns (Vesting memory) { 49 | return _vestings[id]; 50 | } 51 | 52 | /// @notice Returns the available unclaimed tokens of the vesting `id`. 53 | function getUnclaimed(uint256 id) external view returns (uint256) { 54 | return _unclaimed(id); 55 | } 56 | 57 | /// @dev Returns the amount of token accrued given the different parameters. 58 | /// @param time The time at which the accrual ends. 59 | /// @param start The start of the vesting. 60 | /// @param start The end of the vesting. 61 | /// @param start The total token amount of the vesting. 62 | function getAccrued(uint256 time, uint256 start, uint256 end, uint256 total) external pure returns (uint256) { 63 | return _accrued(time, start, end, total); 64 | } 65 | 66 | /* EXTERNAL */ 67 | 68 | /// @notice Creates a new vesting. 69 | /// @param receiver The receiver of the vesting. 70 | /// @param start The start time of the vesting. 71 | /// @param cliffDuration The cliff duration of the vesting. 72 | /// @param duration The total duration of the vesting. 73 | /// @param manager The manager of the vesting that can claim the tokens if the vesting is not restricted, and revoke the vesting if the vesting is not protected. 74 | /// @param restricted True if the manager cannot claim tokens on behalf of receiver. 75 | /// @param protected True if the vesting cannot be revoked. 76 | /// @param total The total amount of vested tokens. 77 | function create( 78 | address receiver, 79 | uint256 start, 80 | uint256 cliffDuration, 81 | uint256 duration, 82 | address manager, 83 | bool restricted, 84 | bool protected, 85 | uint256 total 86 | ) external onlyOwner returns (uint256 id) { 87 | if (receiver == address(0)) revert AddressIsZero(); 88 | if (total == 0) revert TotalIsZero(); 89 | if (start >= block.timestamp + _TWENTY_YEARS) revert StartTooFar(); 90 | if (start <= block.timestamp - _TWENTY_YEARS) revert StartTooLongAgo(); 91 | if (duration == 0) revert DurationIsZero(); 92 | if (duration > _TWENTY_YEARS) revert DurationTooLong(); 93 | if (cliffDuration > duration) revert CliffDurationTooLong(); 94 | 95 | id = ++_ids; 96 | _vestings[id] = Vesting({ 97 | receiver: receiver, 98 | start: start.safeCastTo48(), 99 | cliff: (start + cliffDuration).safeCastTo48(), 100 | end: (start + duration).safeCastTo48(), 101 | manager: manager, 102 | restricted: restricted, 103 | protected: protected, 104 | total: total.safeCastTo128(), 105 | claimed: 0 106 | }); 107 | 108 | emit VestingCreated(id, receiver); 109 | } 110 | 111 | /// @notice Revokes the vesting `id` at `block.timestamp` time. 112 | /// @dev Callable if the vesting is not protected. 113 | function revoke(uint256 id) external { 114 | _revoke(id, block.timestamp); 115 | } 116 | 117 | /// @notice Revokes the vesting `id` at `end` time. 118 | /// @dev Callable if the vesting is not protected. 119 | function revoke(uint256 id, uint256 end) external { 120 | _revoke(id, end); 121 | } 122 | 123 | /// @notice Claims the available tokens of the vesting `id` and sends them to the receiver. 124 | /// @dev Callable by the receiver or the manager if set and the vesting is not restricted. 125 | function claim(uint256 id) external { 126 | _claim(id, type(uint256).max); 127 | } 128 | 129 | /// @notice Claims the available tokens of the vesting `id` and sends them to the receiver. 130 | /// @dev Callable by the receiver or the manager if set and the vesting is not restricted. 131 | function claim(uint256 id, uint256 maxAmount) external { 132 | _claim(id, maxAmount); 133 | } 134 | 135 | /// @notice Protects vesting `id` to be revoked. 136 | function protect(uint256 id) external onlyOwner { 137 | _validateId(id); 138 | _vestings[id].protected = true; 139 | emit VestingProtected(id); 140 | } 141 | 142 | /// @notice Unprotects vesting `id` to be revoked. 143 | function unprotect(uint256 id) external onlyOwner { 144 | _validateId(id); 145 | _vestings[id].protected = false; 146 | emit VestingUnprotected(id); 147 | } 148 | 149 | /// @notice Restricts the vesting `id` to be claimed by the receiver of the vesting only. 150 | /// @dev Callable by the receiver of the vesting or the owner only. 151 | function restrict(uint256 id) external { 152 | _validateId(id); 153 | Vesting storage vesting = _vestings[id]; 154 | if (msg.sender != vesting.receiver && msg.sender != owner) revert PermissionDenied(); 155 | vesting.restricted = true; 156 | emit VestingRestricted(id); 157 | } 158 | 159 | /// @notice Unrestricts the vesting `id` to be claimed by the receiver and the manager if set. 160 | /// @dev Callable by the receiver of the vesting or the owner only. 161 | function unrestrict(uint256 id) external { 162 | _validateId(id); 163 | Vesting storage vesting = _vestings[id]; 164 | if (msg.sender != vesting.receiver && msg.sender != owner) revert PermissionDenied(); 165 | vesting.restricted = false; 166 | emit VestingUnrestricted(id); 167 | } 168 | 169 | /// @notice Sets a new `receiver` to the vesting `id`. 170 | /// @dev Callable by the receiver of the vesting only. 171 | function setReceiver(uint256 id, address receiver) external { 172 | if (receiver == address(0)) revert AddressIsZero(); 173 | Vesting storage vesting = _vestings[id]; 174 | if (msg.sender != vesting.receiver) revert OnlyReceiver(); 175 | vesting.receiver = receiver; 176 | emit ReceiverSet(id, receiver); 177 | } 178 | 179 | /* INTERNAL */ 180 | 181 | /// @dev Validates that the `id` is correct. 182 | function _validateId(uint256 id) internal view { 183 | if (_vestings[id].receiver == address(0)) revert InvalidVestingId(); 184 | } 185 | 186 | /// @dev Returns the unclaimed amount of tokens related to the vesting `id`. 187 | function _unclaimed(uint256 id) internal view returns (uint256) { 188 | Vesting storage vesting = _vestings[id]; 189 | uint256 accrued = 190 | block.timestamp < vesting.cliff ? 0 : _accrued(block.timestamp, vesting.start, vesting.end, vesting.total); 191 | return accrued - vesting.claimed; 192 | } 193 | 194 | /// @dev Returns the amount of token accrued given the different parameters. 195 | /// @param time The time at which the accrual ends. 196 | /// @param start The start of the vesting. 197 | /// @param start The end of the vesting. 198 | /// @param start The total token amount of the vesting. 199 | function _accrued(uint256 time, uint256 start, uint256 end, uint256 total) internal pure returns (uint256) { 200 | if (time < start) return 0; 201 | if (time >= end) return total; 202 | uint256 delta = time - start; 203 | return (total * delta) / (end - start); 204 | } 205 | 206 | /// @notice Claims the available tokens of the vesting `id` and sends them to the receiver. 207 | /// @param id The id of the vesting. 208 | /// @param maxAmount The maximum amount of tokens to claim. 209 | function _claim(uint256 id, uint256 maxAmount) internal { 210 | Vesting storage vesting = _vestings[id]; 211 | if (msg.sender != vesting.receiver && (vesting.restricted || msg.sender != vesting.manager)) { 212 | revert PermissionDenied(); 213 | } 214 | uint256 amount = Math.min(_unclaimed(id), maxAmount); 215 | vesting.claimed += amount.safeCastTo128(); 216 | _transfer(vesting.receiver, amount); 217 | emit Claimed(id, amount); 218 | } 219 | 220 | /// @dev Revokes a vesting if it is not protected. 221 | /// @dev The new `end` cannot be earlier than the current `block.timestamp`. 222 | /// @param id The id of the vesting. 223 | /// @param end The new end of the vesting. 224 | function _revoke(uint256 id, uint256 end) internal { 225 | _validateId(id); 226 | Vesting storage vesting = _vestings[id]; 227 | if (msg.sender != owner && (vesting.protected || msg.sender != vesting.manager)) { 228 | revert PermissionDenied(); 229 | } 230 | 231 | if (end < block.timestamp) end = block.timestamp; 232 | 233 | if (end < vesting.end) { 234 | uint48 castedEnd = end.safeCastTo48(); 235 | vesting.end = castedEnd; 236 | 237 | if (castedEnd < vesting.start) { 238 | vesting.start = castedEnd; 239 | vesting.cliff = castedEnd; 240 | vesting.total = 0; 241 | } else if (castedEnd < vesting.cliff) { 242 | vesting.cliff = castedEnd; 243 | vesting.total = 0; 244 | } else { 245 | vesting.total = _accrued(end, vesting.start, vesting.end, vesting.total).safeCastTo128(); 246 | } 247 | 248 | emit VestingRevoked(id, end); 249 | } 250 | } 251 | 252 | /// @dev Transfers the `amount` of tokens to `receiver`. 253 | /// @dev Must be overriden to implement the logic. 254 | function _transfer(address receiver, uint256 amount) internal virtual; 255 | } 256 | -------------------------------------------------------------------------------- /test/Vest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.0; 3 | 4 | import "solmate/test/utils/mocks/MockERC20.sol"; 5 | import "forge-std/StdStorage.sol"; 6 | import "./helpers/BaseTest.sol"; 7 | import "../src/Vest.sol"; 8 | 9 | contract SimpleMockVest is Vest { 10 | constructor(address owner) Vest(owner) {} 11 | 12 | function _transfer(address receiver, uint256 amount) internal override {} 13 | } 14 | 15 | contract MockVest is Vest, Test { 16 | using stdStorage for StdStorage; 17 | 18 | address internal immutable _token; 19 | 20 | constructor(address token) Vest(msg.sender) { 21 | _token = token; 22 | } 23 | 24 | function validateId(uint256 id) external view { 25 | _validateId(id); 26 | } 27 | 28 | function unclaimed(uint256 id) external view returns (uint256) { 29 | return _unclaimed(id); 30 | } 31 | 32 | function _transfer(address receiver, uint256 amount) internal override { 33 | stdstore.target(_token).sig("balanceOf(address)").with_key(receiver).checked_write(amount); 34 | } 35 | } 36 | 37 | contract VestTest is BaseTest { 38 | using stdStorage for StdStorage; 39 | 40 | event VestingCreated(uint256 id, address receiver); 41 | event VestingRevoked(uint256 id, uint256 end); 42 | event Claimed(uint256 id, uint256 amount); 43 | event VestingProtected(uint256 id); 44 | event VestingUnprotected(uint256 id); 45 | event VestingRestricted(uint256 id); 46 | event VestingUnrestricted(uint256 id); 47 | event ReceiverSet(uint256 id, address receiver); 48 | 49 | MockVest internal vest; 50 | ERC20 internal token; 51 | 52 | address internal alice = address(0x1); 53 | address internal bob = address(0x2); 54 | 55 | function setUp() public { 56 | token = new MockERC20("Test", "TST", 18); 57 | vest = new MockVest(address(token)); 58 | 59 | vm.warp(TWENTY_YEARS + OFFSET); 60 | } 61 | 62 | function testOwner(address owner) public { 63 | Vest newVest = new SimpleMockVest(owner); 64 | assertEq(newVest.owner(), owner); 65 | } 66 | 67 | function testCreate( 68 | address receiver, 69 | uint256 start, 70 | uint256 cliffDuration, 71 | uint256 duration, 72 | address manager, 73 | bool restricted, 74 | bool protected, 75 | uint256 total 76 | ) public { 77 | receiver = _boundAddressNotZero(receiver); 78 | start = _boundStart(start); 79 | duration = bound(duration, 1, TWENTY_YEARS); 80 | cliffDuration = bound(cliffDuration, 0, duration); 81 | total = bound(total, 1, type(uint128).max); 82 | 83 | vm.expectEmit(true, true, true, true); 84 | emit VestingCreated(1, receiver); 85 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 86 | 87 | Vest.Vesting memory vesting = vest.getVesting(id); 88 | 89 | assertEq(id, 1); 90 | assertEq(vesting.receiver, receiver); 91 | assertEq(vesting.start, start); 92 | assertEq(vesting.cliff, start + cliffDuration); 93 | assertEq(vesting.end, start + duration); 94 | assertEq(vesting.manager, manager); 95 | assertEq(vesting.restricted, restricted); 96 | assertEq(vesting.protected, protected); 97 | assertEq(vesting.total, total); 98 | assertEq(vesting.claimed, 0); 99 | } 100 | 101 | function testCreateShouldRevertWhenCalledByNotOwner( 102 | address caller, 103 | address receiver, 104 | uint256 start, 105 | uint256 cliffDuration, 106 | uint256 duration, 107 | address manager, 108 | bool restricted, 109 | bool protected, 110 | uint256 total 111 | ) public { 112 | vm.assume(caller != address(this)); 113 | receiver = _boundAddressNotZero(receiver); 114 | start = _boundStart(start); 115 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 116 | duration = bound(duration, 1, TWENTY_YEARS); 117 | total = bound(total, 1, type(uint128).max); 118 | 119 | vm.prank(caller); 120 | vm.expectRevert("UNAUTHORIZED"); 121 | vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 122 | } 123 | 124 | function testCreateShouldRevertWhenReceiverIsZero( 125 | uint256 start, 126 | uint256 cliffDuration, 127 | uint256 duration, 128 | address manager, 129 | bool restricted, 130 | bool protected, 131 | uint256 total 132 | ) public { 133 | start = _boundStart(start); 134 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 135 | duration = bound(duration, 1, TWENTY_YEARS); 136 | total = bound(total, 1, type(uint128).max); 137 | 138 | vm.expectRevert(IVest.AddressIsZero.selector); 139 | vest.create(address(0), start, cliffDuration, duration, manager, restricted, protected, total); 140 | } 141 | 142 | function testCreateShouldRevertWhenTotalIsZero( 143 | address receiver, 144 | uint256 start, 145 | uint256 cliffDuration, 146 | uint256 duration, 147 | address manager, 148 | bool restricted, 149 | bool protected 150 | ) public { 151 | receiver = _boundAddressNotZero(receiver); 152 | start = _boundStart(start); 153 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 154 | duration = bound(duration, 1, TWENTY_YEARS); 155 | 156 | vm.expectRevert(IVest.TotalIsZero.selector); 157 | vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, 0); 158 | } 159 | 160 | function testCreateShouldRevertWhenStartIsTooFar( 161 | address receiver, 162 | uint256 start, 163 | uint256 cliffDuration, 164 | uint256 duration, 165 | address manager, 166 | bool restricted, 167 | bool protected, 168 | uint256 total 169 | ) public { 170 | receiver = _boundAddressNotZero(receiver); 171 | start = bound(start, block.timestamp + TWENTY_YEARS + OFFSET - 1, type(uint48).max); 172 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 173 | duration = bound(duration, 1, TWENTY_YEARS); 174 | total = bound(total, 1, type(uint128).max); 175 | 176 | vm.expectRevert(IVest.StartTooFar.selector); 177 | vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 178 | } 179 | 180 | function testCreateShouldRevertWhenStartTooLongAgo( 181 | address receiver, 182 | uint256 start, 183 | uint256 cliffDuration, 184 | uint256 duration, 185 | address manager, 186 | bool restricted, 187 | bool protected, 188 | uint256 total 189 | ) public { 190 | receiver = _boundAddressNotZero(receiver); 191 | start = bound(start, 0, block.timestamp - TWENTY_YEARS); 192 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 193 | duration = bound(duration, 1, TWENTY_YEARS); 194 | total = bound(total, 1, type(uint128).max); 195 | 196 | vm.expectRevert(IVest.StartTooLongAgo.selector); 197 | vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 198 | } 199 | 200 | function testCreateShouldRevertWhenDurationIsZero( 201 | address receiver, 202 | uint256 start, 203 | uint256 cliffDuration, 204 | address manager, 205 | bool restricted, 206 | bool protected, 207 | uint256 total 208 | ) public { 209 | receiver = _boundAddressNotZero(receiver); 210 | start = _boundStart(start); 211 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 212 | total = bound(total, 1, type(uint128).max); 213 | vm.expectRevert(IVest.DurationIsZero.selector); 214 | vest.create(receiver, start, cliffDuration, 0, manager, restricted, protected, total); 215 | } 216 | 217 | function testCreateShouldRevertWhenDurationIsTooLong( 218 | address receiver, 219 | uint256 start, 220 | uint256 cliffDuration, 221 | uint256 duration, 222 | address manager, 223 | bool restricted, 224 | bool protected, 225 | uint256 total 226 | ) public { 227 | receiver = _boundAddressNotZero(receiver); 228 | start = _boundStart(start); 229 | cliffDuration = bound(cliffDuration, 0, TWENTY_YEARS); 230 | duration = bound(duration, TWENTY_YEARS + 1, type(uint48).max); 231 | total = bound(total, 1, type(uint128).max); 232 | 233 | vm.expectRevert(IVest.DurationTooLong.selector); 234 | vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 235 | } 236 | 237 | function testCreateShouldRevertWhenCliffIsTooLong( 238 | address receiver, 239 | uint256 start, 240 | uint256 cliffDuration, 241 | uint256 duration, 242 | address manager, 243 | bool restricted, 244 | bool protected, 245 | uint256 total 246 | ) public { 247 | receiver = _boundAddressNotZero(receiver); 248 | start = _boundStart(start); 249 | cliffDuration = bound(cliffDuration, TWENTY_YEARS + 1, type(uint48).max); 250 | duration = bound(duration, 1, TWENTY_YEARS); 251 | total = bound(total, 1, type(uint128).max); 252 | 253 | vm.expectRevert(IVest.CliffDurationTooLong.selector); 254 | vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 255 | } 256 | 257 | function testValidateId() public { 258 | uint256 id = vest.create(alice, START, 0, DURATION, address(0), false, false, TOTAL); 259 | 260 | vest.validateId(id); 261 | } 262 | 263 | function testValidateIdWhenIdIsZero() public { 264 | vest.create(alice, START, 0, DURATION, address(0), false, false, TOTAL); 265 | vm.expectRevert(IVest.InvalidVestingId.selector); 266 | vest.validateId(0); 267 | } 268 | 269 | function testValidateIdShouldRevertWhenNotAVesting(uint256 id) public { 270 | vm.expectRevert(IVest.InvalidVestingId.selector); 271 | vest.validateId(id); 272 | } 273 | 274 | function testClaimShouldRevertWhenCalledByNotReceiverAndNoManagerSet( 275 | address caller, 276 | address receiver, 277 | uint256 start, 278 | uint256 cliffDuration, 279 | uint256 duration, 280 | address manager, 281 | bool restricted, 282 | bool protected, 283 | uint256 total 284 | ) public { 285 | receiver = _boundAddressNotZero(receiver); 286 | vm.assume(caller != receiver); 287 | vm.assume(caller != manager); 288 | start = _boundStart(start); 289 | duration = bound(duration, 1, TWENTY_YEARS); 290 | cliffDuration = bound(cliffDuration, 0, duration); 291 | total = bound(total, 1, type(uint128).max); 292 | 293 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 294 | 295 | vm.prank(caller); 296 | vm.expectRevert(IVest.PermissionDenied.selector); 297 | vest.claim(id); 298 | } 299 | 300 | function testClaimShouldRevertWhenCalledByNotReceiverAndNotManagerButManagerSet( 301 | address caller, 302 | address receiver, 303 | uint256 start, 304 | uint256 cliffDuration, 305 | uint256 duration, 306 | address manager, 307 | bool restricted, 308 | bool protected, 309 | uint256 total 310 | ) public { 311 | receiver = _boundAddressNotZero(receiver); 312 | vm.assume(caller != receiver); 313 | vm.assume(caller != manager); 314 | start = _boundStart(start); 315 | duration = bound(duration, 1, TWENTY_YEARS); 316 | cliffDuration = bound(cliffDuration, 0, duration); 317 | total = bound(total, 1, type(uint128).max); 318 | 319 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 320 | 321 | vm.prank(caller); 322 | vm.expectRevert(IVest.PermissionDenied.selector); 323 | vest.claim(id); 324 | } 325 | 326 | function testClaimShouldRevertWhenCalledByRestrictedManager( 327 | address receiver, 328 | uint256 start, 329 | uint256 cliffDuration, 330 | uint256 duration, 331 | address manager, 332 | bool protected, 333 | uint256 total 334 | ) public { 335 | receiver = _boundAddressNotZero(receiver); 336 | vm.assume(manager != receiver); 337 | start = _boundStart(start); 338 | duration = bound(duration, 1, TWENTY_YEARS); 339 | cliffDuration = bound(cliffDuration, 0, duration); 340 | total = bound(total, 1, type(uint128).max); 341 | 342 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, true, protected, total); 343 | 344 | vm.prank(manager); 345 | vm.expectRevert(IVest.PermissionDenied.selector); 346 | vest.claim(id); 347 | } 348 | 349 | function testClaimCalledByReceiver( 350 | address receiver, 351 | uint256 start, 352 | uint256 cliffDuration, 353 | uint256 duration, 354 | address manager, 355 | bool restricted, 356 | bool protected, 357 | uint256 total 358 | ) public { 359 | receiver = _boundAddressNotZero(receiver); 360 | 361 | start = _boundStart(start); 362 | duration = bound(duration, 1, TWENTY_YEARS); 363 | cliffDuration = bound(cliffDuration, 0, duration); 364 | total = bound(total, 1, type(uint128).max); 365 | 366 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 367 | 368 | vm.prank(receiver); 369 | vest.claim(id); 370 | } 371 | 372 | function testClaimCalledByManagerNotRestricted( 373 | address receiver, 374 | uint256 start, 375 | uint256 cliffDuration, 376 | uint256 duration, 377 | address manager, 378 | bool protected, 379 | uint256 total 380 | ) public { 381 | receiver = _boundAddressNotZero(receiver); 382 | 383 | start = _boundStart(start); 384 | duration = bound(duration, 1, TWENTY_YEARS); 385 | cliffDuration = bound(cliffDuration, 0, duration); 386 | total = bound(total, 1, type(uint128).max); 387 | 388 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, false, protected, total); 389 | 390 | vm.prank(manager); 391 | vest.claim(id); 392 | } 393 | 394 | function testSetReceiver(address receiver) public { 395 | receiver = _boundAddressNotZero(receiver); 396 | 397 | uint256 id = _createVest(); 398 | 399 | vm.expectEmit(true, true, true, true); 400 | emit ReceiverSet(id, receiver); 401 | 402 | vm.prank(alice); 403 | vest.setReceiver(id, receiver); 404 | 405 | Vest.Vesting memory vesting = vest.getVesting(id); 406 | 407 | assertEq(vesting.receiver, receiver); 408 | } 409 | 410 | function testSetReceiverShouldRevertWhenAddressZero() public { 411 | uint256 id = _createVest(); 412 | 413 | vm.prank(alice); 414 | vm.expectRevert(IVest.AddressIsZero.selector); 415 | vest.setReceiver(id, address(0)); 416 | } 417 | 418 | function testSetReceiverShouldRevertWhenCalledByNotReceiver(address caller, address receiver) public { 419 | vm.assume(caller != alice); 420 | receiver = _boundAddressNotZero(receiver); 421 | 422 | uint256 id = _createVest(); 423 | 424 | vm.prank(caller); 425 | vm.expectRevert(IVest.OnlyReceiver.selector); 426 | vest.setReceiver(id, receiver); 427 | } 428 | 429 | function testProtect() public { 430 | uint256 id = _createVest(); 431 | 432 | vm.expectEmit(true, true, true, true); 433 | emit VestingProtected(id); 434 | 435 | vest.protect(id); 436 | 437 | Vest.Vesting memory vesting = vest.getVesting(id); 438 | 439 | assertTrue(vesting.protected); 440 | } 441 | 442 | function testProtectShouldRevertWhenCalledByNotOwner(address caller) public { 443 | vm.assume(caller != vest.owner()); 444 | 445 | uint256 id = _createVest(); 446 | 447 | vm.prank(caller); 448 | vm.expectRevert("UNAUTHORIZED"); 449 | vest.protect(id); 450 | } 451 | 452 | function testProtectShouldRevertWhenInvalidId(uint256 id) public { 453 | id = _boundId(id); 454 | 455 | _createVest(); 456 | 457 | vm.expectRevert(IVest.InvalidVestingId.selector); 458 | vest.protect(id); 459 | } 460 | 461 | function testUnprotect() public { 462 | uint256 id = _createVest(); 463 | 464 | vm.expectEmit(true, true, true, true); 465 | emit VestingUnprotected(id); 466 | 467 | vest.unprotect(id); 468 | 469 | Vest.Vesting memory vesting = vest.getVesting(id); 470 | 471 | assertFalse(vesting.protected); 472 | } 473 | 474 | function testUnrotectShouldRevertWhenCalledByNotOwner(address caller) public { 475 | vm.assume(caller != vest.owner()); 476 | 477 | uint256 id = _createVest(); 478 | 479 | vm.prank(caller); 480 | vm.expectRevert("UNAUTHORIZED"); 481 | vest.unprotect(id); 482 | } 483 | 484 | function testUnprotectShouldRevertWhenInvalidId(uint256 id) public { 485 | id = _boundId(id); 486 | 487 | _createVest(); 488 | 489 | vm.expectRevert(IVest.InvalidVestingId.selector); 490 | vest.unprotect(id); 491 | } 492 | 493 | function testRestrictCalledByOwner() public { 494 | uint256 id = _createVest(); 495 | 496 | vm.expectEmit(true, true, true, true); 497 | emit VestingRestricted(id); 498 | 499 | vest.restrict(id); 500 | 501 | Vest.Vesting memory vesting = vest.getVesting(id); 502 | 503 | assertTrue(vesting.restricted); 504 | } 505 | 506 | function testRestrictCalledByReceiver() public { 507 | uint256 id = _createVest(); 508 | 509 | vm.expectEmit(true, true, true, true); 510 | emit VestingRestricted(id); 511 | 512 | vm.prank(alice); 513 | vest.restrict(id); 514 | 515 | Vest.Vesting memory vesting = vest.getVesting(id); 516 | 517 | assertTrue(vesting.restricted); 518 | } 519 | 520 | function testRestrictShouldRevertWhenCalledByNotOwnerNorReceiver(address caller) public { 521 | vm.assume(caller != vest.owner()); 522 | vm.assume(caller != alice); 523 | 524 | uint256 id = _createVest(); 525 | 526 | vm.prank(caller); 527 | vm.expectRevert(IVest.PermissionDenied.selector); 528 | vest.restrict(id); 529 | } 530 | 531 | function testRestrictShouldRevertWhenInvalidId(uint256 id) public { 532 | id = _boundId(id); 533 | 534 | _createVest(); 535 | 536 | vm.expectRevert(IVest.InvalidVestingId.selector); 537 | vest.restrict(id); 538 | } 539 | 540 | function testUnrestrictCalledByOwner() public { 541 | uint256 id = _createVest(); 542 | 543 | vm.expectEmit(true, true, true, true); 544 | emit VestingUnrestricted(id); 545 | 546 | vest.unrestrict(id); 547 | 548 | Vest.Vesting memory vesting = vest.getVesting(id); 549 | 550 | assertFalse(vesting.restricted); 551 | } 552 | 553 | function testUnrestrictCalledByReceiver() public { 554 | uint256 id = _createVest(); 555 | 556 | vm.expectEmit(true, true, true, true); 557 | emit VestingUnrestricted(id); 558 | 559 | vm.prank(alice); 560 | vest.unrestrict(id); 561 | 562 | Vest.Vesting memory vesting = vest.getVesting(id); 563 | 564 | assertFalse(vesting.restricted); 565 | } 566 | 567 | function testUnrestrictShouldRevertWhenCalledByNotOwnerNorReceiver(address caller) public { 568 | vm.assume(caller != vest.owner()); 569 | vm.assume(caller != alice); 570 | 571 | uint256 id = _createVest(); 572 | 573 | vm.prank(caller); 574 | vm.expectRevert(IVest.PermissionDenied.selector); 575 | vest.unrestrict(id); 576 | } 577 | 578 | function testUnrestrictShouldRevertWhenInvalidId(uint256 id) public { 579 | id = _boundId(id); 580 | 581 | _createVest(); 582 | 583 | vm.expectRevert(IVest.InvalidVestingId.selector); 584 | vest.unrestrict(id); 585 | } 586 | 587 | function testClaimBeforeCliff( 588 | address receiver, 589 | uint256 start, 590 | uint256 cliffDuration, 591 | uint256 duration, 592 | address manager, 593 | bool restricted, 594 | bool protected, 595 | uint256 total, 596 | uint256 claimTime 597 | ) public { 598 | receiver = _boundAddressNotZero(receiver); 599 | start = _boundStart(start); 600 | duration = bound(duration, 1, TWENTY_YEARS); 601 | cliffDuration = bound(cliffDuration, 0, duration); 602 | claimTime = bound(claimTime, 0, start + cliffDuration - 1); 603 | total = bound(total, 1, type(uint128).max); 604 | 605 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 606 | 607 | vm.warp(claimTime); 608 | 609 | vm.prank(receiver); 610 | vest.claim(id); 611 | 612 | Vest.Vesting memory vesting = vest.getVesting(id); 613 | 614 | assertEq(vesting.claimed, 0); 615 | assertEq(ERC20(token).balanceOf(receiver), 0); 616 | } 617 | 618 | function testClaimBeforeCliffWithMaxAmount( 619 | address receiver, 620 | uint256 start, 621 | uint256 cliff, 622 | uint256 duration, 623 | address manager, 624 | bool restricted, 625 | bool protected, 626 | uint256 total, 627 | uint256 claimTime, 628 | uint256 maxAmount 629 | ) public { 630 | receiver = _boundAddressNotZero(receiver); 631 | start = _boundStart(start); 632 | duration = bound(duration, 1, TWENTY_YEARS); 633 | cliff = bound(cliff, 0, duration); 634 | claimTime = bound(claimTime, 0, start + cliff - 1); 635 | total = bound(total, 1, type(uint128).max); 636 | maxAmount = bound(maxAmount, 0, type(uint256).max); 637 | 638 | uint256 id = vest.create(receiver, start, cliff, duration, manager, restricted, protected, total); 639 | 640 | vm.warp(claimTime); 641 | 642 | vm.prank(receiver); 643 | vest.claim(id, maxAmount); 644 | 645 | assertEq(vest.getVesting(id).claimed, 0); 646 | assertEq(ERC20(token).balanceOf(receiver), 0); 647 | } 648 | 649 | function testClaimAfterCliff( 650 | address receiver, 651 | uint256 start, 652 | uint256 cliffDuration, 653 | uint256 duration, 654 | address manager, 655 | bool restricted, 656 | bool protected, 657 | uint256 total, 658 | uint256 claimTime 659 | ) public { 660 | receiver = _boundAddressNotZero(receiver); 661 | start = _boundStart(start); 662 | duration = bound(duration, OFFSET, TWENTY_YEARS); 663 | cliffDuration = bound(cliffDuration, 0, duration); 664 | claimTime = bound(claimTime, start + cliffDuration, type(uint128).max); 665 | total = bound(total, TOTAL, type(uint128).max); 666 | 667 | uint256 id = vest.create(receiver, start, cliffDuration, duration, manager, restricted, protected, total); 668 | 669 | vm.warp(claimTime); 670 | 671 | uint256 accrued = vest.getAccrued(block.timestamp, start, start + duration, total); 672 | 673 | vm.expectEmit(true, true, true, true); 674 | emit Claimed(id, accrued); 675 | 676 | vm.prank(receiver); 677 | vest.claim(id); 678 | 679 | assertEq(vest.getVesting(id).claimed, accrued); 680 | assertEq(ERC20(token).balanceOf(receiver), accrued); 681 | } 682 | 683 | function testClaimAfterCliffWithMaxAmount( 684 | address receiver, 685 | uint256 start, 686 | uint256 cliff, 687 | uint256 duration, 688 | address manager, 689 | bool restricted, 690 | bool protected, 691 | uint256 total, 692 | uint256 claimTime, 693 | uint256 maxAmount 694 | ) public { 695 | receiver = _boundAddressNotZero(receiver); 696 | start = _boundStart(start); 697 | duration = bound(duration, OFFSET, TWENTY_YEARS); 698 | cliff = bound(cliff, 0, duration); 699 | claimTime = bound(claimTime, start + cliff, type(uint128).max); 700 | total = bound(total, TOTAL, type(uint128).max); 701 | maxAmount = bound(maxAmount, 0, type(uint256).max); 702 | 703 | uint256 id = vest.create(receiver, start, cliff, duration, manager, restricted, protected, total); 704 | 705 | vm.warp(claimTime); 706 | 707 | uint256 accrued = vest.getAccrued(block.timestamp, start, start + duration, total); 708 | uint256 expectedClaimed = Math.min(accrued, maxAmount); 709 | 710 | vm.expectEmit(true, true, true, true); 711 | emit Claimed(id, expectedClaimed); 712 | 713 | vm.prank(receiver); 714 | vest.claim(id, maxAmount); 715 | 716 | assertEq(vest.getVesting(id).claimed, expectedClaimed); 717 | assertEq(ERC20(token).balanceOf(receiver), expectedClaimed); 718 | } 719 | 720 | function testAccruedWithTimeBeforeStart(uint256 time, uint256 start, uint256 end, uint256 total) public { 721 | start = bound(start, 1, type(uint256).max); 722 | time = bound(time, 0, start - 1); 723 | 724 | assertEq(vest.getAccrued(time, start, end, total), 0); 725 | } 726 | 727 | function testAccruedWithTimeAfterEnd(uint256 time, uint256 start, uint256 end, uint256 total) public { 728 | start = bound(start, 0, type(uint256).max - 1); 729 | end = bound(end, start + 1, type(uint256).max); 730 | time = bound(time, end, type(uint256).max); 731 | 732 | assertEq(vest.getAccrued(time, start, end, total), total); 733 | } 734 | 735 | function testAccrued(uint256 time, uint256 start, uint256 end, uint256 total) public { 736 | start = bound(start, 0, type(uint128).max - 1); 737 | end = bound(end, start + 1, type(uint128).max); 738 | time = bound(time, start + 1, end); 739 | total = bound(total, 0, type(uint128).max); 740 | 741 | uint256 expected = total * (time - start) / (end - start); 742 | 743 | assertEq(vest.getAccrued(time, start, end, total), expected); 744 | } 745 | 746 | function testUnclaimedBeforeCliff( 747 | uint256 time, 748 | uint256 start, 749 | uint256 cliffDuration, 750 | uint256 duration, 751 | uint256 total 752 | ) public { 753 | total = bound(total, 1, type(uint128).max); 754 | start = _boundStart(start); 755 | duration = bound(duration, 1, TWENTY_YEARS); 756 | cliffDuration = bound(cliffDuration, 0, duration); 757 | time = bound(time, 0, start + cliffDuration - 1); 758 | 759 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 760 | 761 | vm.warp(time); 762 | 763 | uint256 unclaimed = vest.getUnclaimed(id); 764 | 765 | assertEq(unclaimed, 0); 766 | } 767 | 768 | function testUnclaimedAfterCliff( 769 | uint256 timeClaim, 770 | uint256 timeUnclaimed, 771 | uint256 start, 772 | uint256 cliffDuration, 773 | uint256 duration, 774 | uint256 total 775 | ) public { 776 | total = bound(total, TOTAL, type(uint128).max); 777 | start = _boundStart(start); 778 | duration = bound(duration, OFFSET, TWENTY_YEARS); 779 | cliffDuration = bound(cliffDuration, 0, duration); 780 | timeClaim = bound(timeClaim, start + cliffDuration, type(uint96).max); 781 | timeUnclaimed = bound(timeUnclaimed, timeClaim + 1, type(uint128).max); 782 | 783 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 784 | 785 | vm.warp(timeClaim); 786 | 787 | vm.prank(alice); 788 | vest.claim(id); 789 | uint256 claimedBefore = vest.getVesting(id).claimed; 790 | 791 | vm.warp(timeUnclaimed); 792 | 793 | uint256 accrued = vest.getAccrued(block.timestamp, start, start + duration, total); 794 | uint256 expectedUnclaimed = accrued - claimedBefore; 795 | uint256 unclaimed = vest.getUnclaimed(id); 796 | 797 | assertApproxEqAbs(unclaimed, expectedUnclaimed, 1); 798 | } 799 | 800 | function testRevokeShouldRevertWhenInvalidId(uint256 id) public { 801 | id = _boundId(id); 802 | 803 | vm.expectRevert(IVest.InvalidVestingId.selector); 804 | vm.prank(address(this)); 805 | vest.revoke(id); 806 | } 807 | 808 | function testRevokeShouldRevertWhenCalledByNotOwnerAndVestingProtected(address caller) public { 809 | vm.assume(caller != address(this)); 810 | 811 | uint256 id = vest.create(alice, START, 0, DURATION, address(0), false, true, TOTAL); 812 | 813 | vm.expectRevert(IVest.PermissionDenied.selector); 814 | vm.prank(caller); 815 | vest.revoke(id); 816 | } 817 | 818 | function testRevokeShouldRevertWhenCalledByNotOwnerAndNotManagerAndVestingNotProtected(address caller) public { 819 | vm.assume(caller != address(this)); 820 | vm.assume(caller != bob); 821 | 822 | uint256 id = vest.create(alice, START, 0, DURATION, bob, false, true, TOTAL); 823 | 824 | vm.expectRevert(IVest.PermissionDenied.selector); 825 | vm.prank(caller); 826 | vest.revoke(id); 827 | } 828 | 829 | function testRevokeShouldRevertWhenCalledByManagerAndVestingProtected() public { 830 | uint256 id = vest.create(alice, START, 0, DURATION, bob, false, true, TOTAL); 831 | 832 | vm.expectRevert(IVest.PermissionDenied.selector); 833 | vm.prank(bob); 834 | vest.revoke(id); 835 | } 836 | 837 | function testRevokeWhenCalledByManagerAndNotVestingProtected() public { 838 | uint256 id = vest.create(alice, START, 0, DURATION, bob, false, false, TOTAL); 839 | 840 | vm.prank(bob); 841 | vest.revoke(id); 842 | } 843 | 844 | function testRevokeWhenCalledByOwnerAndVestingProtected() public { 845 | uint256 id = vest.create(alice, START, 0, DURATION, bob, false, false, TOTAL); 846 | 847 | vm.prank(address(this)); 848 | vest.revoke(id); 849 | } 850 | 851 | function testRevokeBeforeStart(uint256 end, uint256 start, uint256 cliffDuration, uint256 duration, uint256 total) 852 | public 853 | { 854 | total = bound(total, TOTAL, type(uint128).max); 855 | start = bound(start, block.timestamp + 1, block.timestamp + TWENTY_YEARS - 1); 856 | duration = bound(duration, OFFSET, TWENTY_YEARS); 857 | cliffDuration = bound(cliffDuration, 0, duration); 858 | end = bound(end, block.timestamp, start - 1); 859 | 860 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 861 | 862 | uint256 expectedEnd = end < block.timestamp ? block.timestamp : end; 863 | 864 | vm.expectEmit(true, true, true, true); 865 | emit VestingRevoked(id, expectedEnd); 866 | 867 | vest.revoke(id, end); 868 | 869 | Vest.Vesting memory vesting = vest.getVesting(id); 870 | 871 | assertEq(vesting.start, expectedEnd, "start"); 872 | assertEq(vesting.end, expectedEnd, "end"); 873 | assertEq(vesting.total, 0, "total"); 874 | } 875 | 876 | function testRevokeBeforeCliff(uint256 end, uint256 start, uint256 cliffDuration, uint256 duration, uint256 total) 877 | public 878 | { 879 | total = bound(total, TOTAL, type(uint128).max); 880 | start = bound(start, block.timestamp, block.timestamp + TWENTY_YEARS - 1); 881 | duration = bound(duration, OFFSET, TWENTY_YEARS); 882 | cliffDuration = bound(cliffDuration, OFFSET, duration); 883 | end = bound(end, start, start + cliffDuration - 1); 884 | 885 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 886 | 887 | uint256 expectedEnd = end < block.timestamp ? block.timestamp : end; 888 | 889 | vm.expectEmit(true, true, true, true); 890 | emit VestingRevoked(id, expectedEnd); 891 | 892 | vest.revoke(id, end); 893 | 894 | Vest.Vesting memory vesting = vest.getVesting(id); 895 | 896 | assertEq(vesting.start, start, "start"); 897 | assertEq(vesting.end, expectedEnd, "end"); 898 | assertEq(vesting.total, 0, "total"); 899 | } 900 | 901 | function testRevokeAfterEnd(uint256 end, uint256 start, uint256 cliffDuration, uint256 duration, uint256 total) 902 | public 903 | { 904 | total = bound(total, TOTAL, type(uint128).max); 905 | start = bound(start, block.timestamp, block.timestamp + TWENTY_YEARS - 1); 906 | duration = bound(duration, OFFSET, TWENTY_YEARS); 907 | cliffDuration = bound(cliffDuration, 0, duration); 908 | end = bound(end, start + duration, type(uint256).max); 909 | 910 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 911 | 912 | Vest.Vesting memory vestingBefore = vest.getVesting(id); 913 | 914 | vest.revoke(id, end); 915 | 916 | end = end < block.timestamp ? block.timestamp : end; 917 | 918 | Vest.Vesting memory vestingAfter = vest.getVesting(id); 919 | 920 | assertEq(vestingAfter.start, vestingBefore.start, "start"); 921 | assertEq(vestingAfter.end, vestingBefore.end, "end"); 922 | assertEq(vestingAfter.total, vestingBefore.total, "total"); 923 | } 924 | 925 | function testRevokeAfterCliffAndBeforeEnd( 926 | uint256 end, 927 | uint256 start, 928 | uint256 cliffDuration, 929 | uint256 duration, 930 | uint256 total 931 | ) public { 932 | total = bound(total, TOTAL, type(uint128).max); 933 | start = bound(start, block.timestamp, block.timestamp + TWENTY_YEARS - 1); 934 | duration = bound(duration, OFFSET, TWENTY_YEARS); 935 | cliffDuration = bound(cliffDuration, 0, duration - 1); 936 | end = bound(end, start + cliffDuration, start + duration - 1); 937 | 938 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 939 | 940 | Vest.Vesting memory vestingBefore = vest.getVesting(id); 941 | 942 | uint256 expectedEnd = end < block.timestamp ? block.timestamp : end; 943 | 944 | vm.expectEmit(true, true, true, true); 945 | emit VestingRevoked(id, expectedEnd); 946 | 947 | vest.revoke(id, end); 948 | 949 | uint256 expectedTotal = vest.getAccrued(vestingBefore.end, start, end, total); 950 | 951 | Vest.Vesting memory vestingAfter = vest.getVesting(id); 952 | 953 | assertEq(vestingAfter.start, vestingBefore.start, "start"); 954 | assertEq(vestingAfter.end, expectedEnd, "end"); 955 | assertEq(vestingAfter.total, expectedTotal, "total"); 956 | } 957 | 958 | function testRevokeEndShouldAtLeastBlockTimestamp( 959 | uint256 end, 960 | uint256 start, 961 | uint256 cliffDuration, 962 | uint256 duration, 963 | uint256 total 964 | ) public { 965 | total = bound(total, TOTAL, type(uint128).max); 966 | start = bound(start, block.timestamp, block.timestamp + TWENTY_YEARS - 1); 967 | duration = bound(duration, OFFSET, TWENTY_YEARS); 968 | cliffDuration = bound(cliffDuration, 0, duration - 1); 969 | end = bound(end, 0, start + duration - 1); 970 | 971 | uint256 id = vest.create(alice, start, cliffDuration, duration, address(0), false, false, total); 972 | 973 | vest.revoke(id, end); 974 | 975 | Vest.Vesting memory vesting = vest.getVesting(id); 976 | 977 | assertEq(vesting.end, end < block.timestamp ? block.timestamp : end, "end"); 978 | } 979 | 980 | function testIds(uint8 nbOfVestings) public { 981 | for (uint256 i; i < nbOfVestings; ++i) { 982 | uint256 id = _createVest(); 983 | assertEq(id, i + 1); 984 | } 985 | 986 | assertEq(vest.ids(), nbOfVestings); 987 | } 988 | 989 | function invariantDeadlines() public { 990 | Vest.Vesting memory vesting = vest.getVesting(0); 991 | assertLe(vesting.start, vesting.cliff); 992 | assertLe(vesting.start, vesting.end); 993 | } 994 | 995 | function invariantClaimed() public { 996 | Vest.Vesting memory vesting = vest.getVesting(0); 997 | assertLe(vesting.claimed, vesting.total); 998 | } 999 | 1000 | function invariantUnclaimed() public { 1001 | Vest.Vesting memory vesting = vest.getVesting(0); 1002 | uint256 unclaimed = vest.getUnclaimed(0); 1003 | assertLe(unclaimed, vesting.total); 1004 | } 1005 | 1006 | function _createVest() internal returns (uint256 id) { 1007 | id = vest.create(alice, START, 0, DURATION, address(0), false, false, TOTAL); 1008 | } 1009 | } 1010 | --------------------------------------------------------------------------------