Skip to content

Commit

Permalink
feat: inheriting cliffed OZ Vesting Wallet and using constants approa…
Browse files Browse the repository at this point in the history
…ch (#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 <[email protected]>
  • Loading branch information
wei3erHase and dristpunk authored Jan 16, 2024
1 parent 6322983 commit e6ae145
Show file tree
Hide file tree
Showing 17 changed files with 1,826 additions and 491 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ concurrency:

env:
MAINNET_RPC: ${{ secrets.MAINNET_RPC }}
DEPLOYER_PRIVATE_KEY: '0x115241e9f8d2246550d50641f38ee4170b937f25dfb983f7e34960f9670fc41d'

jobs:
integration-tests:
Expand Down
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ isolmate/=node_modules/isolmate/src

contracts/=solidity/contracts
interfaces/=solidity/interfaces
scripts/=solidity/scripts
test/=solidity/test
134 changes: 134 additions & 0 deletions solidity/contracts/ConnextVestingWallet.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
95 changes: 0 additions & 95 deletions solidity/contracts/Unlock.sol

This file was deleted.

41 changes: 41 additions & 0 deletions solidity/contracts/VestingWalletWithCliff.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
108 changes: 0 additions & 108 deletions solidity/interfaces/IUnlock.sol

This file was deleted.

Loading

0 comments on commit e6ae145

Please sign in to comment.