From e6ae14524e25d6a4263008459eb98f2fcc4b98df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 16 Jan 2024 09:07:19 -0300 Subject: [PATCH] feat: inheriting cliffed OZ Vesting Wallet and using constants approach (#5) According to [Connext Forum](https://forum.connext.network/t/rfc-partnership-token-agreements/938), the agreement that Wonderland, Veil and Bootload (other partners have), unlocks at the same time, for every partner, one year after launch, a date that we all know (sept 5th 2023), and have the same 1/13ths schedule. So, inputting `startTime` at constructor makes no logic, since that's sth that can be shared accross all partners, this PR aims to reduce that unnecessary constructor argument. In the process of crafting this PR, a test failed hinting me that we missed a big issue, we need to use Llamapay V2. Luckily, this means no contract changes, and only to the test suite, but because of this test, we've realized that we almost failed BIG time. Pushing this PoC with some TODOs in the codebase, the constructor args are gonna be only beneficiary and amount (only thing that differs between partners), and in the testing suite, we need to be using LlamaPay v2. Let's do this approach: - import the Deploy script in the Integration tests - generically test `ConnextVestingWallet` using portions of `TOTAL_AMOUNT()` (and not precise numbers) - specify test `LlamaVesting` showing the expected limiting behaviours: - launch date - launch + 1yr - 1 - launch + 1yr (e.g. `assertEq(.., 1_920_000 ether)`) - launch + 1yr + 1mth - launch + 1yr + 1yr ![image](https://github.com/defi-wonderland/connext-vesting/assets/84595958/13d698a9-7c29-459b-9121-dd3f7d93d712) The way to do this test is probably run it, see the failing expectations, and input what's expected to receive in the tests, right? like, too lazy to calc, but that's a good thing!! the fact that the dev KNOWS and inputs the expected amount at each date, means that there are no doubts about what's expected to happen, please declare those expectations as `x ether` numbers (having a max delta of `1 ether`) --------- Co-authored-by: Dristpunk --- .github/workflows/ci.yml | 1 + remappings.txt | 1 + solidity/contracts/ConnextVestingWallet.sol | 134 ++ solidity/contracts/Unlock.sol | 95 -- solidity/contracts/VestingWalletWithCliff.sol | 41 + solidity/interfaces/IUnlock.sol | 108 -- solidity/interfaces/IVestingEscrowSimple.sol | 41 + solidity/scripts/Deploy.sol | 24 +- .../integration/ConnextVestingWallet.t.sol | 230 +++ solidity/test/integration/IntegrationBase.sol | 37 +- solidity/test/integration/LlamaVesting.t.sol | 65 +- solidity/test/integration/Unlock.t.sol | 188 --- solidity/test/utils/Constants.sol | 13 +- solidity/test/utils/ILlamaPay.sol | 16 - solidity/test/utils/ILlamaPayFactory.sol | 6 - solidity/test/utils/IVestingEscrowFactory.sol | 43 + unlock.svg | 1274 ++++++++++++++++- 17 files changed, 1826 insertions(+), 491 deletions(-) create mode 100644 solidity/contracts/ConnextVestingWallet.sol delete mode 100644 solidity/contracts/Unlock.sol create mode 100644 solidity/contracts/VestingWalletWithCliff.sol delete mode 100644 solidity/interfaces/IUnlock.sol create mode 100644 solidity/interfaces/IVestingEscrowSimple.sol create mode 100644 solidity/test/integration/ConnextVestingWallet.t.sol delete mode 100644 solidity/test/integration/Unlock.t.sol delete mode 100644 solidity/test/utils/ILlamaPay.sol delete mode 100644 solidity/test/utils/ILlamaPayFactory.sol create mode 100644 solidity/test/utils/IVestingEscrowFactory.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dd44ae..206ce0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ concurrency: env: MAINNET_RPC: ${{ secrets.MAINNET_RPC }} + DEPLOYER_PRIVATE_KEY: '0x115241e9f8d2246550d50641f38ee4170b937f25dfb983f7e34960f9670fc41d' jobs: integration-tests: diff --git a/remappings.txt b/remappings.txt index 33e3f0e..9e3a9a2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ isolmate/=node_modules/isolmate/src contracts/=solidity/contracts interfaces/=solidity/interfaces +scripts/=solidity/scripts test/=solidity/test diff --git a/solidity/contracts/ConnextVestingWallet.sol b/solidity/contracts/ConnextVestingWallet.sol new file mode 100644 index 0000000..5319558 --- /dev/null +++ b/solidity/contracts/ConnextVestingWallet.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +// solhint-disable-next-line no-unused-import +import {VestingWallet, VestingWalletWithCliff} from './VestingWalletWithCliff.sol'; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IVestingEscrowSimple} from 'interfaces/IVestingEscrowSimple.sol'; + +/** + * @title ConnextVestingWallet + * NOTE: The NEXT tokens will be subject to a twenty-four (24) months unlock schedule as follows: + * 1/13 (~7.69% of the token grant) unlocks at the 1 year mark from NEXT token launch, + * and 1/13 unlocks every month thereafter for 12 months. All tokens are unlocked after 24 months. + * https://forum.connext.network/t/rfc-partnership-token-agreements/938 + */ +contract ConnextVestingWallet is VestingWalletWithCliff, Ownable2Step { + /** + * --- Constants --- + */ + uint64 public constant ONE_YEAR = 365 days; + uint64 public constant ONE_MONTH = ONE_YEAR / 12; + uint64 public constant SEPT_05_2023 = 1_693_872_000; + uint64 public constant NEXT_TOKEN_LAUNCH = SEPT_05_2023; + address public constant NEXT_TOKEN = 0xFE67A4450907459c3e1FFf623aA927dD4e28c67a; // Mainnet NEXT token + + /** + * NOTE: The equivalent vesting schedule has a 13 months duration, with a 1 month cliff, + * offsetted to start from `Sept 5th 2024 - 1 month`: At Sept 5th 2024 the cliff + * is triggered unlocking 1/13 of the tokens, and then 1/13 of the tokens will + * be linearly unlocked every month after that. + */ + uint64 public constant VESTING_DURATION = ONE_YEAR + ONE_MONTH; // 13 months duration + uint64 public constant VESTING_CLIFF_DURATION = ONE_MONTH; // 1 month cliff + uint64 public constant VESTING_OFFSET = ONE_YEAR - ONE_MONTH; // 11 months offset + uint64 public constant VESTING_START_DATE = NEXT_TOKEN_LAUNCH + VESTING_OFFSET; // Sept 5th 2024 - 1 month + + /** + * --- Settable Storage --- + */ + uint256 public immutable TOTAL_AMOUNT; + + constructor( + address _beneficiary, + uint256 _totalAmount + ) VestingWalletWithCliff(_beneficiary, VESTING_START_DATE, VESTING_DURATION, VESTING_CLIFF_DURATION) { + TOTAL_AMOUNT = _totalAmount; + } + + /** + * --- Errors --- + */ + error NotAllowed(); + error ZeroAddress(); + + /** + * --- Overrides --- + */ + + /** + * @inheritdoc VestingWallet + * @notice This contract is only meant to vest NEXT tokens + */ + function vestedAmount(uint64) public view virtual override returns (uint256 _amount) { + return 0; + } + + /** + * @inheritdoc VestingWallet + * @notice This contract is only meant to vest NEXT tokens + */ + function vestedAmount(address _token, uint64 _timestamp) public view virtual override returns (uint256 _amount) { + if (_token != NEXT_TOKEN) return 0; + + return _vestingSchedule(TOTAL_AMOUNT, _timestamp); + } + + /** + * @inheritdoc VestingWallet + * @notice This contract is only meant to vest NEXT tokens + */ + function releasable(address _token) public view virtual override returns (uint256 _amount) { + _amount = vestedAmount(_token, uint64(block.timestamp)) - released(_token); + uint256 _balance = IERC20(_token).balanceOf(address(this)); + _amount = _balance < _amount ? _balance : _amount; + } + + /** + * @inheritdoc Ownable2Step + * @dev Override needed by linearization + */ + function _transferOwnership(address _newOwner) internal virtual override(Ownable2Step, Ownable) { + super._transferOwnership(_newOwner); + } + + /** + * @inheritdoc Ownable2Step + * @dev Override needed by linearization + */ + function transferOwnership(address _newOwner) public virtual override(Ownable2Step, Ownable) { + super.transferOwnership(_newOwner); + } + + /** + * --- Dust Collector --- + * @notice Collect dust from the contract + * @dev This contract allows to withdraw any token, with the exception of vested NEXT tokens + */ + function sendDust(IERC20 _token, uint256 _amount, address _to) external onlyOwner { + if (_to == address(0)) revert ZeroAddress(); + if (_token == IERC20(NEXT_TOKEN) && released(NEXT_TOKEN) != TOTAL_AMOUNT) { + revert NotAllowed(); + } + + if (_token == IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + // Sending ETH + payable(_to).transfer(_amount); + } else { + // Sending ERC20s + _token.transfer(_to, _amount); + } + } + + /** + * --- Claim --- + * @notice Claim tokens from Llama Vesting contract + * @dev This func is needed because only the recipients can claim + */ + function claim(address _llamaVestAddress) external { + IVestingEscrowSimple(_llamaVestAddress).claim(address(this)); + } +} diff --git a/solidity/contracts/Unlock.sol b/solidity/contracts/Unlock.sol deleted file mode 100644 index f00a0fd..0000000 --- a/solidity/contracts/Unlock.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; - -import {IUnlock} from 'interfaces/IUnlock.sol'; - -contract Unlock is Ownable2Step, IUnlock { - using SafeERC20 for IERC20; - - /// @inheritdoc IUnlock - uint256 public constant SECONDS_UNTIL_FIRST_MILESTONE = 365 days; - - /// @inheritdoc IUnlock - IERC20 public immutable VESTING_TOKEN; - - /// @inheritdoc IUnlock - uint256 public immutable TOTAL_AMOUNT; - /// @inheritdoc IUnlock - uint256 public immutable START_TIME; - /// @inheritdoc IUnlock - uint256 public immutable FIRST_MILESTONE_TIMESTAMP; - /// @inheritdoc IUnlock - uint256 public immutable UNLOCKED_AT_FIRST_MILESTONE; - /// @inheritdoc IUnlock - uint256 public immutable UNLOCKED_AFTER_FIRST_MILESTONE; - - /// @inheritdoc IUnlock - uint256 public withdrawnAmount; - - constructor(uint256 _startTime, address _owner, IERC20 _vestingToken, uint256 _totalAmount) Ownable(_owner) { - START_TIME = _startTime; - TOTAL_AMOUNT = _totalAmount; - VESTING_TOKEN = _vestingToken; - FIRST_MILESTONE_TIMESTAMP = START_TIME + SECONDS_UNTIL_FIRST_MILESTONE; - UNLOCKED_AT_FIRST_MILESTONE = TOTAL_AMOUNT / 13; - UNLOCKED_AFTER_FIRST_MILESTONE = TOTAL_AMOUNT - UNLOCKED_AT_FIRST_MILESTONE; - } - - /// @inheritdoc IUnlock - function withdrawableAmount() public view returns (uint256 _withdrawableAmount) { - _withdrawableAmount = _unlockedAmountAt(block.timestamp) - withdrawnAmount; - } - - /// @inheritdoc IUnlock - function unlockedAtTimestamp(uint256 _timestamp) external view returns (uint256 _unlockedAtTimestamp) { - _unlockedAtTimestamp = _unlockedAmountAt(_timestamp); - } - - /// @inheritdoc IUnlock - function withdraw(address _receiver) external onlyOwner { - uint256 _amount = withdrawableAmount(); - uint256 _balance = VESTING_TOKEN.balanceOf(address(this)); - - if (_amount > _balance) _amount = _balance; - - withdrawnAmount += _amount; - VESTING_TOKEN.safeTransfer(_receiver, _amount); - } - - /// @inheritdoc IUnlock - function sendDust(IERC20 _token, uint256 _amount, address _to) external onlyOwner { - if (_to == address(0)) revert ZeroAddress(); - - if (_token == IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - // Sending ETH - payable(_to).transfer(_amount); - } else if (_token != VESTING_TOKEN || withdrawnAmount == TOTAL_AMOUNT) { - // Sending ERC20s - _token.safeTransfer(_to, _amount); - } - } - - /** - * @notice Returns the amount of unlocked tokens at a given timestamp - * - * @dev f(x) = ax + b, where - * x = time since the first milestone - * a = remaining amount / totalTime - * b = total amount / 13 - * @param _timestamp The timestamp to query - * @return _unlockedAmount The amount of unlocked tokens at the given timestamp - */ - function _unlockedAmountAt(uint256 _timestamp) internal view returns (uint256 _unlockedAmount) { - // 0 if the first milestone has not been reached yet - if (_timestamp < FIRST_MILESTONE_TIMESTAMP) return _unlockedAmount; - - uint256 _timeSinceFirstMilestone = _timestamp - FIRST_MILESTONE_TIMESTAMP; - _unlockedAmount = UNLOCKED_AT_FIRST_MILESTONE - + (UNLOCKED_AFTER_FIRST_MILESTONE * _timeSinceFirstMilestone) / SECONDS_UNTIL_FIRST_MILESTONE; - if (_unlockedAmount > TOTAL_AMOUNT) _unlockedAmount = TOTAL_AMOUNT; - } -} diff --git a/solidity/contracts/VestingWalletWithCliff.sol b/solidity/contracts/VestingWalletWithCliff.sol new file mode 100644 index 0000000..1ae53e1 --- /dev/null +++ b/solidity/contracts/VestingWalletWithCliff.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {VestingWallet} from '@openzeppelin/contracts/finance/VestingWallet.sol'; + +contract VestingWalletWithCliff is VestingWallet { + uint64 private immutable _CLIFF; + + constructor( + address _beneficiary, + uint64 _vestingStartTimestamp, + uint64 _durationSeconds, + uint64 _cliffDurationSeconds + ) VestingWallet(_beneficiary, _vestingStartTimestamp, _durationSeconds) { + _CLIFF = _vestingStartTimestamp + _cliffDurationSeconds; + } + + /** + * @dev Getter for the cliff timestamp. + */ + function cliff() public view virtual returns (uint256 _timestamp) { + return _CLIFF; + } + + /** + * @dev Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for + * an asset given its total historical allocation. + */ + function _vestingSchedule( + uint256 _totalAllocation, + uint64 _timestamp + ) internal view virtual override returns (uint256 _amount) { + if (_timestamp < cliff()) { + return 0; + } else if (_timestamp >= end()) { + return _totalAllocation; + } else { + return (_totalAllocation * (_timestamp - start())) / duration(); + } + } +} diff --git a/solidity/interfaces/IUnlock.sol b/solidity/interfaces/IUnlock.sol deleted file mode 100644 index 32850fd..0000000 --- a/solidity/interfaces/IUnlock.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; - -interface IUnlock { - /*/////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - /** - * @notice Thrown when somebody is trying to send dust to the zero address - */ - error ZeroAddress(); - - /*/////////////////////////////////////////////////////////////// - VARIABLES - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Returns the timestamp of the beginning of the vesting period - * - * @return _startTime The timestamp of the beginning of the vesting period - */ - function START_TIME() external view returns (uint256 _startTime); - - /** - * @notice Returns the total amount of vested tokens - * - * @return _totalAmount The total amount of vested tokens - */ - function TOTAL_AMOUNT() external view returns (uint256 _totalAmount); - - /** - * @notice Returns the vesting token - * - * @return _vestingToken The vesting token - */ - function VESTING_TOKEN() external view returns (IERC20 _vestingToken); - - /** - * @notice Returns the timestamp of the first milestone, at which a part of the vested tokens will be unlocked - * - * @return _firstMilestoneTimestamp The timestamp of the first milestone - */ - function FIRST_MILESTONE_TIMESTAMP() external view returns (uint256 _firstMilestoneTimestamp); - - /** - * @notice Returns the amount of the vested tokens that will be unlocked at the first milestone - * - * @return _unlockedAtFirstMilestone The amount of the vested tokens that will be unlocked at the first milestone - */ - function UNLOCKED_AT_FIRST_MILESTONE() external view returns (uint256 _unlockedAtFirstMilestone); - - /** - * @notice Returns the amount of the vested tokens that will be gradually unlocked after the first milestone - * - * @return _unlockedAfterFirstMilestone The amount of the vested tokens that will be unlocked at the first milestone - */ - function UNLOCKED_AFTER_FIRST_MILESTONE() external view returns (uint256 _unlockedAfterFirstMilestone); - - /** - * @notice Returns the number of seconds that should pass before the first milestone is reached - * - * @return _secondsUntilFirstMilestone The number of seconds before the first milestone - */ - function SECONDS_UNTIL_FIRST_MILESTONE() external view returns (uint256 _secondsUntilFirstMilestone); - - /** - * @notice Returns the amount of tokens that have been withdrawn - * - * @return _withdrawnAmount The amount of withdrawn tokens - */ - function withdrawnAmount() external view returns (uint256 _withdrawnAmount); - - /** - * @notice Returns the amount of tokens that can be withdrawn - * - * @return _withdrawableAmount The amount of withdrawable tokens - */ - function withdrawableAmount() external view returns (uint256 _withdrawableAmount); - - /** - * @notice Returns the amount of tokens that can be withdrawn at a given timestamp - * - * @return _unlockedAtTimestamp The amount of tokens that can be withdrawn at the given timestamp - */ - function unlockedAtTimestamp(uint256 _timestamp) external view returns (uint256 _unlockedAtTimestamp); - - /*/////////////////////////////////////////////////////////////// - LOGIC - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Withdraws the tokens that have been unlocked - * - * @param _receiver The address of the receiver - */ - function withdraw(address _receiver) external; - - /** - * @notice Sends dust to the owner, including ETH and non-vesting ERC20s - * - * @param _token The token to collect - * @param _amount The amount to collect - * @param _to The address to send the dust to - */ - function sendDust(IERC20 _token, uint256 _amount, address _to) external; -} diff --git a/solidity/interfaces/IVestingEscrowSimple.sol b/solidity/interfaces/IVestingEscrowSimple.sol new file mode 100644 index 0000000..24800e1 --- /dev/null +++ b/solidity/interfaces/IVestingEscrowSimple.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +interface IVestingEscrowSimple { + event ApplyOwnership(address _admin); + event Claim(address indexed _recipient, uint256 _claimed); + event CommitOwnership(address _admin); + event Fund(address indexed _recipient, uint256 _amount); + event RugPull(address _recipient, uint256 _rugged); + + function admin() external view returns (address _admin); + function apply_transfer_ownership() external; + function claim() external; + function claim(address _beneficiary) external; + function claim(address _beneficiary, uint256 _amount) external; + function cliff_length() external view returns (uint256 _length); + function collect_dust(address _token) external; + function commit_transfer_ownership(address _addr) external; + function disabled_at() external view returns (uint256 _timestamp); + function end_time() external view returns (uint256 _timestamp); + function future_admin() external view returns (address _futureAdmin); + function initialize( + address _admin, + address _token, + address _recipient, + uint256 _amount, + uint256 _startTime, + uint256 _endTime, + uint256 _cliffLength + ) external returns (bool _success); + function initialized() external view returns (bool _isInitialized); + function locked() external view returns (uint256 _amount); + function recipient() external view returns (address _recipient); + function renounce_ownership() external; + function rug_pull() external; + function start_time() external view returns (uint256 _startTime); + function token() external view returns (address _vestingToken); + function total_claimed() external view returns (uint256 _amount); + function total_locked() external view returns (uint256 _amount); + function unclaimed() external view returns (uint256 _amount); +} diff --git a/solidity/scripts/Deploy.sol b/solidity/scripts/Deploy.sol index 4075483..c326b57 100644 --- a/solidity/scripts/Deploy.sol +++ b/solidity/scripts/Deploy.sol @@ -1,34 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; -import {Unlock} from '../contracts/Unlock.sol'; +import {ConnextVestingWallet} from 'contracts/ConnextVestingWallet.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {Script, console} from 'forge-std/Script.sol'; contract Deploy is Script { - Unlock public unlock; + ConnextVestingWallet internal _connextVestingWallet; - IERC20 public constant NEXT = IERC20(0xFE67A4450907459c3e1FFf623aA927dD4e28c67a); - uint256 public constant START_TIME = 1_693_872_000; - uint256 public constant TOTAL_AMOUNT = 24_960_000 ether; - address public constant OWNER = 0x74fEa3FB0eD030e9228026E7F413D66186d3D107; + uint256 internal constant _TOTAL_AMOUNT = 24_960_000 ether; + address internal constant _OWNER = 0x74fEa3FB0eD030e9228026E7F413D66186d3D107; function run() public { address deployer = vm.rememberKey(vm.envUint('DEPLOYER_PRIVATE_KEY')); - require(START_TIME > 0, 'START_TIME'); - require(TOTAL_AMOUNT > 0, 'TOTAL_AMOUNT'); - require(OWNER != address(0), 'OWNER'); - require(address(NEXT) != address(0), 'VESTING_TOKEN'); + require(_TOTAL_AMOUNT > 0, 'TOTAL_AMOUNT'); + require(_OWNER != address(0), 'OWNER'); vm.startBroadcast(deployer); - unlock = new Unlock(START_TIME, OWNER, NEXT, TOTAL_AMOUNT); + _connextVestingWallet = new ConnextVestingWallet(_OWNER, _TOTAL_AMOUNT); vm.stopBroadcast(); - require(unlock.owner() == OWNER, 'owner'); - require(unlock.START_TIME() == START_TIME, 'START_TIME'); - require(unlock.TOTAL_AMOUNT() == TOTAL_AMOUNT, 'TOTAL_AMOUNT'); - require(unlock.VESTING_TOKEN() == NEXT, 'VESTING_TOKEN'); + require(_connextVestingWallet.owner() == _OWNER, 'owner'); + require(_connextVestingWallet.TOTAL_AMOUNT() == _TOTAL_AMOUNT, 'TOTAL_AMOUNT'); } } diff --git a/solidity/test/integration/ConnextVestingWallet.t.sol b/solidity/test/integration/ConnextVestingWallet.t.sol new file mode 100644 index 0000000..ce5a52e --- /dev/null +++ b/solidity/test/integration/ConnextVestingWallet.t.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {ConnextVestingWallet} from 'contracts/ConnextVestingWallet.sol'; +import {Constants} from 'test/utils/Constants.sol'; + +import {IVestingEscrowSimple} from 'interfaces/IVestingEscrowSimple.sol'; +import {IVestingEscrowFactory} from 'test/utils/IVestingEscrowFactory.sol'; + +import {Test} from 'forge-std/Test.sol'; + +contract UnitConnextVestingWallet is Test, Constants { + address public receiver = makeAddr('receiver'); + + ConnextVestingWallet internal _connextVestingWallet; + address internal _connextVestingWalletAddress; + uint64 internal _firstMilestoneTimestamp; + uint64 internal _connextTokenLaunch; + + address public owner = makeAddr('owner'); + address public payer = makeAddr('payer'); + + IERC20 internal _nextToken = IERC20(NEXT_TOKEN_ADDRESS); + IVestingEscrowFactory internal _llamaVestFactory = IVestingEscrowFactory(LLAMA_FACTORY_ADDRESS); + IVestingEscrowSimple internal _llamaVest; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), FORK_BLOCK); + + deal(NEXT_TOKEN_ADDRESS, payer, TOTAL_AMOUNT); + + // approve before deployment + vm.prank(payer); + _nextToken.approve(address(_llamaVestFactory), TOTAL_AMOUNT); + + // deploy vesting contract + vm.prank(payer); + _llamaVest = IVestingEscrowSimple( + _llamaVestFactory.deploy_vesting_contract( + NEXT_TOKEN_ADDRESS, address(_connextVestingWallet), TOTAL_AMOUNT, VESTING_DURATION, AUG_01_2022, 0 + ) + ); + + // set total amount as 13 ether + _connextVestingWallet = new ConnextVestingWallet(owner, 13 ether); + _connextVestingWalletAddress = address(_connextVestingWallet); + _connextTokenLaunch = uint64(_connextVestingWallet.NEXT_TOKEN_LAUNCH()); + _firstMilestoneTimestamp = uint64(_connextVestingWallet.cliff()); + } + + /** + * @notice Testing the constructor logic, it should set the owner and the start time + */ + function test_Constructor() public { + assertEq(Ownable2Step(_connextVestingWalletAddress).owner(), owner); + assertEq(_connextVestingWallet.TOTAL_AMOUNT(), 13 ether); + } + + /** + * @notice The unlocked amount should be different at various points in time. + * At the beginning of the unlocking period: 0 tokens + * Just before the first milestone: 0 token + * At the first milestone: 1 ether tokens + * 1 month after the first milestone: 2 ether tokens + * 2 month after the first milestone: 3 ether tokens + * At the end of the unlocking period: 13 ether tokens + * After the end of the unlocking period: 13 ether tokens + */ + function test_UnlockedAtTimestamp() public { + assertEq(_connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _connextTokenLaunch), 0); + assertEq(_connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _firstMilestoneTimestamp - 1), 0); + + assertEq(_connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _firstMilestoneTimestamp), 1 ether); + + assertApproxEqAbs( + _connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _firstMilestoneTimestamp + MONTH), 2 ether, MAX_DELTA + ); + + assertApproxEqAbs( + _connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _firstMilestoneTimestamp + MONTH * 2), 3 ether, MAX_DELTA + ); + + assertApproxEqAbs( + _connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _firstMilestoneTimestamp + YEAR), 13 ether, MAX_DELTA + ); + + assertApproxEqAbs( + _connextVestingWallet.vestedAmount(NEXT_TOKEN_ADDRESS, _firstMilestoneTimestamp + YEAR + 10 days), + 13 ether, + MAX_DELTA + ); + } + + /** + * @notice The withdrawable amount should be different at various points in time, the same way as the unlocked amount. + * It should take into account already withdrawn tokens. + */ + function test_WithdrawableAmount() public { + deal(NEXT_TOKEN_ADDRESS, _connextVestingWalletAddress, 15 ether); + + assertEq(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 0); + + vm.warp(_connextTokenLaunch + YEAR - 1); + assertEq(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 0); + + vm.warp(_firstMilestoneTimestamp); + assertEq(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 1 ether); + + vm.warp(_firstMilestoneTimestamp + MONTH); + assertApproxEqAbs(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 2 ether, MAX_DELTA); + + _connextVestingWallet.release(NEXT_TOKEN_ADDRESS); + assertEq(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 0 ether); + + // 2 ether have been withdrawn + vm.warp(_firstMilestoneTimestamp + MONTH * 2); + assertApproxEqAbs(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 3 ether - 2 ether, MAX_DELTA); + + vm.warp(_firstMilestoneTimestamp + YEAR); + assertApproxEqAbs(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 13 ether - 2 ether, MAX_DELTA); + + vm.warp(_firstMilestoneTimestamp + YEAR + 10 days); + assertApproxEqAbs(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 13 ether - 2 ether, MAX_DELTA); + } + + /** + * @notice Testing the withdrawal logic. The unlocking rate should not depend on the balance of the contract. + */ + function test_Withdraw() public { + // Deal more tokens that will be locked + deal(NEXT_TOKEN_ADDRESS, _connextVestingWalletAddress, 2 ether); + vm.warp(_firstMilestoneTimestamp); + + vm.startPrank(owner); + _connextVestingWallet.release(NEXT_TOKEN_ADDRESS); + + // Even though the contract has more tokens, the unlocked amount should be the same + assertEq(_connextVestingWallet.released(NEXT_TOKEN_ADDRESS), 1 ether); + assertEq(_nextToken.balanceOf(owner), 1 ether); + + // Try again and expect no changes + _connextVestingWallet.release(NEXT_TOKEN_ADDRESS); + assertEq(_connextVestingWallet.released(NEXT_TOKEN_ADDRESS), 1 ether); + assertEq(_nextToken.balanceOf(owner), 1 ether); + + vm.stopPrank(); + } + + /** + * @notice Shouldn't revert if there is nothing to withdraw + */ + function test_Withdraw_NoSupply() public { + _connextVestingWallet.release(NEXT_TOKEN_ADDRESS); + + assertEq(_connextVestingWallet.releasable(NEXT_TOKEN_ADDRESS), 0); + assertEq(_nextToken.balanceOf(owner), 0); + } + + /** + * @notice 2-step ownership transfer + */ + function test_transferOwnership() public { + address _newOwner = makeAddr('newOwner'); + Ownable2Step _unlockOwnable = Ownable2Step(_connextVestingWalletAddress); + + vm.prank(owner); + _unlockOwnable.transferOwnership(_newOwner); + + assertEq(_unlockOwnable.pendingOwner(), _newOwner); + assertEq(_unlockOwnable.owner(), owner); + + address _bob = makeAddr('bob'); + vm.prank(_bob); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _bob)); + _unlockOwnable.acceptOwnership(); + + vm.prank(_newOwner); + _unlockOwnable.acceptOwnership(); + assertEq(_unlockOwnable.owner(), _newOwner); + } + + /** + * @notice The dust collector should allow the owner to send ETH and ERC20s to any address + */ + function test_SendDust() public { + IERC20 _dai = IERC20(DAI_ADDRESS); + address _randomAddress = makeAddr('randomAddress'); + uint256 _dustAmount = 1000; + + vm.deal(_connextVestingWalletAddress, _dustAmount); + deal(DAI_ADDRESS, _connextVestingWalletAddress, _dustAmount); + deal(NEXT_TOKEN_ADDRESS, _connextVestingWalletAddress, _dustAmount + _connextVestingWallet.TOTAL_AMOUNT()); + + // Random dude cannot collect dust + address _bob = makeAddr('bob'); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _bob)); + vm.prank(_bob); + _connextVestingWallet.sendDust(_dai, _dustAmount, _randomAddress); + + // Can't collect the vesting token + assertEq(_nextToken.balanceOf(_randomAddress), 0); + vm.expectRevert(abi.encodeWithSelector(ConnextVestingWallet.NotAllowed.selector)); + vm.prank(owner); + _connextVestingWallet.sendDust(_nextToken, _dustAmount, _randomAddress); + assertEq(_nextToken.balanceOf(_randomAddress), 0); + + // Collect an ERC20 token + assertEq(_dai.balanceOf(_randomAddress), 0); + vm.prank(owner); + _connextVestingWallet.sendDust(_dai, _dustAmount, _randomAddress); + assertEq(_dai.balanceOf(_randomAddress), _dustAmount); + + // Collect ETH + assertEq(_randomAddress.balance, 0); + vm.prank(owner); + _connextVestingWallet.sendDust(IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), _dustAmount, _randomAddress); + assertEq(_randomAddress.balance, _dustAmount); + + // Collect vesting token after the vesting period has ended + vm.warp(_firstMilestoneTimestamp + 365 days * 3 + 10 days); + assertEq(_nextToken.balanceOf(_randomAddress), 0); + _connextVestingWallet.release(NEXT_TOKEN_ADDRESS); + vm.prank(owner); + _connextVestingWallet.sendDust(_nextToken, _dustAmount, _randomAddress); + assertEq(_nextToken.balanceOf(_randomAddress), _dustAmount); + } +} diff --git a/solidity/test/integration/IntegrationBase.sol b/solidity/test/integration/IntegrationBase.sol index e234151..b83a17a 100644 --- a/solidity/test/integration/IntegrationBase.sol +++ b/solidity/test/integration/IntegrationBase.sol @@ -4,33 +4,38 @@ pragma solidity 0.8.20; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {Test} from 'forge-std/Test.sol'; -import {IUnlock, Unlock} from 'contracts/Unlock.sol'; - +import {Deploy} from 'scripts/Deploy.sol'; import {Constants} from 'test/utils/Constants.sol'; -import {ILlamaPay} from 'test/utils/ILlamaPay.sol'; -import {ILlamaPayFactory} from 'test/utils/ILlamaPayFactory.sol'; -contract IntegrationBase is Test, Constants { - address public owner = makeAddr('owner'); +import {IVestingEscrowSimple} from 'interfaces/IVestingEscrowSimple.sol'; +import {IVestingEscrowFactory} from 'test/utils/IVestingEscrowFactory.sol'; + +contract IntegrationBase is Test, Constants, Deploy { + address public owner = _OWNER; address public payer = makeAddr('payer'); IERC20 internal _nextToken = IERC20(NEXT_TOKEN_ADDRESS); - ILlamaPayFactory internal _llamaPayFactory = ILlamaPayFactory(LLAMA_FACTORY_ADDRESS); - - IUnlock internal _unlock; - ILlamaPay internal _llamaPay; - uint256 internal _unlockStartTime; + IVestingEscrowFactory internal _llamaVestFactory = IVestingEscrowFactory(LLAMA_FACTORY_ADDRESS); + IVestingEscrowSimple internal _llamaVest; function setUp() public virtual { vm.createSelectFork(vm.rpcUrl('mainnet'), FORK_BLOCK); - _unlockStartTime = block.timestamp + 10 minutes; - - _unlock = new Unlock(_unlockStartTime, owner, _nextToken, TOTAL_AMOUNT); - _llamaPay = ILlamaPay(_llamaPayFactory.createLlamaPayContract(NEXT_TOKEN_ADDRESS)); + // deploy + run(); deal(NEXT_TOKEN_ADDRESS, payer, TOTAL_AMOUNT); + + // approve before deployment + vm.prank(payer); + _nextToken.approve(address(_llamaVestFactory), TOTAL_AMOUNT); + + // deploy vesting contract vm.prank(payer); - _nextToken.approve(address(_llamaPay), TOTAL_AMOUNT); + _llamaVest = IVestingEscrowSimple( + _llamaVestFactory.deploy_vesting_contract( + NEXT_TOKEN_ADDRESS, address(_connextVestingWallet), TOTAL_AMOUNT, VESTING_DURATION, AUG_01_2022, 0 + ) + ); } } diff --git a/solidity/test/integration/LlamaVesting.t.sol b/solidity/test/integration/LlamaVesting.t.sol index 4552535..896ff2b 100644 --- a/solidity/test/integration/LlamaVesting.t.sol +++ b/solidity/test/integration/LlamaVesting.t.sol @@ -8,33 +8,45 @@ contract IntegrationLlamaVesting is IntegrationBase { function setUp() public override { super.setUp(); - _vestingStartTime = block.timestamp; + _vestingStartTime = _connextVestingWallet.start(); } function test_VestAndUnlock() public { - vm.prank(payer); - _llamaPay.depositAndCreate(TOTAL_AMOUNT, address(_unlock), PAY_PER_SECOND); + // At launch date + uint256 _timestamp = SEP_05_2023; + _warpAndWithdraw(_timestamp); + _assertWalletBalance(6_838_356 ether); + _assertOwnerBalance(0 ether); - // Before the 1st milestone - uint256 _timestamp = _unlock.FIRST_MILESTONE_TIMESTAMP() - 1; - uint256 _vestedAmount = (_timestamp - _vestingStartTime) * PAY_PER_SECOND / 1e2; + // Launch date + 1 year - 1 second + _timestamp = SEP_05_2023 + YEAR - 1; + _warpAndWithdraw(_timestamp); + _assertWalletBalance(13_078_355 ether); + _assertOwnerBalance(0 ether); - // The unlocking contract holds the tokens + // Launch date + 1 year + _timestamp = SEP_05_2023 + YEAR; _warpAndWithdraw(_timestamp); - _assertBalances(0); - assertEq(_nextToken.balanceOf(address(_unlock)), _vestedAmount); + _assertWalletBalance(11_158_356 ether); + _assertOwnerBalance(1_920_000 ether); - // After the 1st milestone - _warpAndWithdraw(_unlock.FIRST_MILESTONE_TIMESTAMP() + 10 days); - _assertBalances(2_551_232 ether); + // Launch date + 1 year + 1 month + _timestamp = SEP_05_2023 + YEAR + MONTH; + _warpAndWithdraw(_timestamp); + _assertWalletBalance(9_758_356 ether); + _assertOwnerBalance(3_840_000 ether); - // Linear unlock after the 1st milestone - _warpAndWithdraw(_unlock.FIRST_MILESTONE_TIMESTAMP() + 365 days); - _assertBalances(12_480_118 ether); + // Launch date + 2 years + _timestamp = SEP_05_2023 + 2 * YEAR; + _warpAndWithdraw(_timestamp); + _assertWalletBalance(0 ether); + _assertOwnerBalance(19_318_356 ether); - // After the unlocking period has ended - _warpAndWithdraw(_unlock.FIRST_MILESTONE_TIMESTAMP() + 365 days * 3 + 10 days); - _assertBalances(24_960_000 ether); + // Vesting start date + 4 years + _timestamp = AUG_01_2022 + 4 * YEAR; + _warpAndWithdraw(_timestamp); + _assertWalletBalance(0 ether); + _assertOwnerBalance(24_960_000 ether); } /** @@ -42,17 +54,22 @@ contract IntegrationLlamaVesting is IntegrationBase { */ function _warpAndWithdraw(uint256 _timestamp) internal { vm.warp(_timestamp); - _llamaPay.withdraw(payer, address(_unlock), PAY_PER_SECOND); - - vm.prank(owner); - _unlock.withdraw(owner); + _connextVestingWallet.claim(address(_llamaVest)); + _connextVestingWallet.release(NEXT_TOKEN_ADDRESS); } /** * @notice Each withdrawal should equally increase the withdrawn amount and the owner's balance */ - function _assertBalances(uint256 _balance) internal { - assertApproxEqAbs(_unlock.withdrawnAmount(), _balance, MAX_DELTA); + function _assertOwnerBalance(uint256 _balance) internal { + assertApproxEqAbs(_connextVestingWallet.released(NEXT_TOKEN_ADDRESS), _balance, MAX_DELTA); assertApproxEqAbs(_nextToken.balanceOf(owner), _balance, MAX_DELTA); } + + /** + * @notice Assert the connext vesting wallet balance is equal to the given amount + */ + function _assertWalletBalance(uint256 _balance) internal { + assertApproxEqAbs(_nextToken.balanceOf(address(_connextVestingWallet)), _balance, MAX_DELTA); + } } diff --git a/solidity/test/integration/Unlock.t.sol b/solidity/test/integration/Unlock.t.sol deleted file mode 100644 index b034f64..0000000 --- a/solidity/test/integration/Unlock.t.sol +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import {IntegrationBase} from 'test/integration/IntegrationBase.sol'; - -contract IntegrationUnlock is IntegrationBase { - address public receiver = makeAddr('receiver'); - - address internal _unlockAddress; - uint256 internal _firstMilestoneTimestamp; - - function setUp() public override { - super.setUp(); - - _unlockAddress = address(_unlock); - _firstMilestoneTimestamp = _unlock.FIRST_MILESTONE_TIMESTAMP(); - } - - /** - * @notice Testing the constructor logic, it should set the owner and the start time - */ - function test_Constructor() public { - assertEq(Ownable2Step(_unlockAddress).owner(), owner); - assertEq(_unlock.START_TIME(), block.timestamp + 10 minutes); - } - - /** - * @notice The unlocked amount should be different at various points in time. - * At the beginning of the unlocking period: 0 tokens - * Just before the first milestone: 0 token - * At the first milestone: 1,920,000 tokens - * 10 days after the first milestone: 2,551,232 tokens - * 100 days after the first milestone: 8,232,328 tokens - * At the end of the unlocking period: 24,960,000 tokens - * After the end of the unlocking period: 24,960,000 tokens - */ - function test_UnlockedAtTimestamp() public { - assertEq(_unlock.unlockedAtTimestamp(_unlockStartTime), 0); - assertEq(_unlock.unlockedAtTimestamp(_firstMilestoneTimestamp - 1), 0); - - assertEq(_unlock.unlockedAtTimestamp(_firstMilestoneTimestamp), 1_920_000 ether); - - assertApproxEqAbs(_unlock.unlockedAtTimestamp(_firstMilestoneTimestamp + 10 days), 2_551_232 ether, MAX_DELTA); - assertApproxEqAbs(_unlock.unlockedAtTimestamp(_firstMilestoneTimestamp + 100 days), 8_232_328 ether, MAX_DELTA); - - assertEq(_unlock.unlockedAtTimestamp(_firstMilestoneTimestamp + 365 days), 24_960_000 ether); - assertEq(_unlock.unlockedAtTimestamp(_firstMilestoneTimestamp + 365 days + 10 days), 24_960_000 ether); - } - - /** - * @notice The withdrawable amount should be different at various points in time, the same way as the unlocked amount. - * It should take into account already withdrawn tokens. - */ - function test_WithdrawableAmount() public { - deal(NEXT_TOKEN_ADDRESS, _unlockAddress, 25_000_000 ether); - - assertEq(_unlock.withdrawableAmount(), 0); - - vm.warp(_unlockStartTime + 364 days); - assertEq(_unlock.withdrawableAmount(), 0); - - vm.warp(_firstMilestoneTimestamp); - assertEq(_unlock.withdrawableAmount(), 1_920_000 ether); - - vm.warp(_firstMilestoneTimestamp + 10 days); - assertApproxEqAbs(_unlock.withdrawableAmount(), 2_551_232 ether, MAX_DELTA); - - vm.prank(owner); - _unlock.withdraw(receiver); - assertEq(_unlock.withdrawableAmount(), 0 ether); - - // 2,551,232 tokens have been withdrawn - vm.warp(_firstMilestoneTimestamp + 100 days); - assertApproxEqAbs(_unlock.withdrawableAmount(), 8_232_328 ether - 2_551_232 ether, MAX_DELTA); - - vm.warp(_firstMilestoneTimestamp + 365 days); - assertApproxEqAbs(_unlock.withdrawableAmount(), 24_960_000 ether - 2_551_232 ether, MAX_DELTA); - - vm.warp(_firstMilestoneTimestamp + 365 days + 10 days); - assertApproxEqAbs(_unlock.withdrawableAmount(), 24_960_000 ether - 2_551_232 ether, MAX_DELTA); - } - - /** - * @notice Testing the withdrawal logic. The unlocking rate should not depend on the balance of the contract. - */ - function test_Withdraw() public { - // Deal more tokens that will be locked - deal(NEXT_TOKEN_ADDRESS, _unlockAddress, 2_000_000 ether); - vm.warp(_firstMilestoneTimestamp); - - vm.startPrank(owner); - _unlock.withdraw(receiver); - - // Even though the contract has more tokens, the unlocked amount should be the same - assertEq(_unlock.withdrawnAmount(), 1_920_000 ether); - assertEq(_nextToken.balanceOf(receiver), 1_920_000 ether); - - // Try again and expect no changes - _unlock.withdraw(receiver); - assertEq(_unlock.withdrawnAmount(), 1_920_000 ether); - assertEq(_nextToken.balanceOf(receiver), 1_920_000 ether); - - vm.stopPrank(); - } - - /** - * @notice Shouldn't revert if there is nothing to withdraw - */ - function test_Withdraw_NoSupply() public { - vm.prank(owner); - _unlock.withdraw(receiver); - - assertEq(_unlock.withdrawnAmount(), 0); - assertEq(_nextToken.balanceOf(receiver), 0); - } - - /** - * @notice Shouldn't allow anyone but the owner to initiate the withdrawal - */ - function test_Withdraw_Unauthorized() public { - address _randomAddress = makeAddr('randomAddress'); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _randomAddress)); - vm.prank(_randomAddress); - _unlock.withdraw(receiver); - } - - /** - * @notice 2-step ownership transfer - */ - function test_transferOwnership() public { - address _newOwner = makeAddr('newOwner'); - Ownable2Step _unlockOwnable = Ownable2Step(_unlockAddress); - - vm.prank(owner); - _unlockOwnable.transferOwnership(_newOwner); - - assertEq(_unlockOwnable.pendingOwner(), _newOwner); - assertEq(_unlockOwnable.owner(), owner); - - address _bob = makeAddr('bob'); - vm.prank(_bob); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _bob)); - _unlockOwnable.acceptOwnership(); - - vm.prank(_newOwner); - _unlockOwnable.acceptOwnership(); - assertEq(_unlockOwnable.owner(), _newOwner); - } - - /** - * @notice The dust collector should allow the owner to send ETH and ERC20s to any address - */ - function test_SendDust() public { - IERC20 _dai = IERC20(DAI_ADDRESS); - address _randomAddress = makeAddr('randomAddress'); - uint256 _dustAmount = 1000; - - vm.deal(_unlockAddress, _dustAmount); - deal(DAI_ADDRESS, _unlockAddress, _dustAmount); - deal(NEXT_TOKEN_ADDRESS, _unlockAddress, _dustAmount); - - // Random dude cannot collect dust - address _bob = makeAddr('bob'); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _bob)); - vm.prank(_bob); - _unlock.sendDust(_dai, _dustAmount, _randomAddress); - - // Can't collect the vesting token - assertEq(_nextToken.balanceOf(_randomAddress), 0); - vm.prank(owner); - _unlock.sendDust(_nextToken, _dustAmount, _randomAddress); - assertEq(_nextToken.balanceOf(_randomAddress), 0); - - // Collect an ERC20 token - assertEq(_dai.balanceOf(_randomAddress), 0); - vm.prank(owner); - _unlock.sendDust(_dai, _dustAmount, _randomAddress); - assertEq(_dai.balanceOf(_randomAddress), _dustAmount); - - // Collect ETH - assertEq(_randomAddress.balance, 0); - vm.prank(owner); - _unlock.sendDust(IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), _dustAmount, _randomAddress); - assertEq(_randomAddress.balance, _dustAmount); - } -} diff --git a/solidity/test/utils/Constants.sol b/solidity/test/utils/Constants.sol index 822f39b..43e10ce 100644 --- a/solidity/test/utils/Constants.sol +++ b/solidity/test/utils/Constants.sol @@ -6,7 +6,7 @@ contract Constants { uint256 public constant MAX_DELTA = 1 ether; // The block to use for integration tests - uint256 public constant FORK_BLOCK = 18_927_563; + uint256 public constant FORK_BLOCK = 19_012_745; // The total amount of tokens to be vested uint256 public constant TOTAL_AMOUNT = 24_960_000 ether; @@ -16,5 +16,14 @@ contract Constants { address public constant DAI_ADDRESS = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address public constant NEXT_TOKEN_ADDRESS = 0xFE67A4450907459c3e1FFf623aA927dD4e28c67a; - address public constant LLAMA_FACTORY_ADDRESS = 0xde1C04855c2828431ba637675B6929A684f84C7F; + address public constant LLAMA_FACTORY_ADDRESS = 0xB93427b83573C8F27a08A909045c3e809610411a; + + // Vesting info + uint64 public constant AUG_01_2022 = 1_659_312_000; // vesting start date + uint64 public constant SEP_05_2023 = 1_693_872_000; // launch date + uint64 public constant VESTING_DURATION = 365 days * 4; + + // Time + uint64 public constant YEAR = 365 days; + uint64 public constant MONTH = 365 days / 12; } diff --git a/solidity/test/utils/ILlamaPay.sol b/solidity/test/utils/ILlamaPay.sol deleted file mode 100644 index 46f6a9b..0000000 --- a/solidity/test/utils/ILlamaPay.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -interface ILlamaPay { - function token() external view returns (address _token); - function payers(address _payer) external view returns (uint40 _lastPayerUpdate, uint216 _totalPaidPerSec); - function balances(address _payer) external view returns (uint256 _balance); - function getPayerBalance(address _payer) external view returns (uint256 _balance); - - function createStream(address _to, uint216 _amountPerSec) external; - function depositAndCreate(uint256 _amountToDeposit, address _to, uint216 _amountPerSec) external; - function deposit(uint256 _amount) external; - function withdraw(address _from, address _to, uint216 _amountPerSec) external; - function modifyStream(address _oldTo, uint216 _oldAmountPerSec, address _to, uint216 _amountPerSec) external; - function pauseStream(address _to, uint216 _amountPerSec) external; -} diff --git a/solidity/test/utils/ILlamaPayFactory.sol b/solidity/test/utils/ILlamaPayFactory.sol deleted file mode 100644 index bcd9618..0000000 --- a/solidity/test/utils/ILlamaPayFactory.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -interface ILlamaPayFactory { - function createLlamaPayContract(address _token) external returns (address _contractAddress); -} diff --git a/solidity/test/utils/IVestingEscrowFactory.sol b/solidity/test/utils/IVestingEscrowFactory.sol new file mode 100644 index 0000000..5037a08 --- /dev/null +++ b/solidity/test/utils/IVestingEscrowFactory.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +interface IVestingEscrowFactory { + event VestingEscrowCreated( + address indexed _funder, + address indexed _token, + address indexed _recipient, + address _escrow, + uint256 _amount, + uint256 _vestingStart, + uint256 _vestingDuration, + uint256 _cliffLength + ); + + function deploy_vesting_contract( + address _token, + address _recipient, + uint256 _amount, + uint256 _vestingDuration + ) external returns (address _vestingContract); + + function deploy_vesting_contract( + address _token, + address _recipient, + uint256 _amount, + uint256 _vestingDuration, + uint256 _vestingStart + ) external returns (address _vestingContract); + + function deploy_vesting_contract( + address _token, + address _recipient, + uint256 _amount, + uint256 _vestingDuration, + uint256 _vestingStart, + uint256 _cliffLength + ) external returns (address _vestingContract); + + function escrows(uint256 _id) external view returns (address _escrow); + function escrows_length() external view returns (uint256 _id); + function target() external view returns (address _vestingEscrowSimpleTemplate); +} diff --git a/unlock.svg b/unlock.svg index c9337e9..b4ec649 100644 --- a/unlock.svg +++ b/unlock.svg @@ -1,21 +1,1253 @@ - - - - - - - - Streaming S/year1234yearstokens01613Unlocking U/year \ No newline at end of file + + + + + + + + 2024-01-12T18:07:50.212415 + image/svg+xml + + + Matplotlib v3.7.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +