Skip to content

Commit

Permalink
feat: migrating to yearn vesting escrow (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xGorilla authored Apr 3, 2024
2 parents 431a704 + aa3fd60 commit b2d736f
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 151 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Connext Unlock Contract

The `Unlock` contract is meant to be used as a receiver of a LlamaPay stream. It allows the owner to unlock their vested tokens at a different rate than the vesting. The main goal of that would be to delay the unlocking of the tokens in case of an early cancellation of the vesting stream.
The `Unlock` contract is meant to be used as a receiver of a Yearn Vesting Escrow. It allows the owner to unlock their vested tokens at a different rate than the vesting. The main goal of that would be to delay the unlocking of the tokens in case of an early cancellation of the vesting stream.

<img src="unlock.svg" alt="vesting and unlocking" align="center" />

## Dependencies

This contract is designed to be integrated with Yearn's [Vesting Escrow v0.3.0](https://github.com/yearn/yearn-vesting-escrow) contract, ([audited](https://github.com/yearn/yearn-security/tree/master/audits/20231013_Mixbytes_yearn_vesting_escrow)). The `ConnextVestingWallet` contract is meant to be the receiver of the tokens that are being vested.

**NOTE**: This Vesting Escrow contract version is compatible with post-shangai EVM versions only (as it requires `PUSH0` compatibility).

## Setup

1. Copy the `.env.example` file to `.env` and fill in the variables.
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sort_imports = true

[profile.default]
solc_version = '0.8.20'
evm_version = 'shanghai'
src = 'solidity'
test = 'solidity/test'
out = 'out'
Expand Down
4 changes: 2 additions & 2 deletions solidity/contracts/ConnextVestingWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ contract ConnextVestingWallet is Ownable2Step, IConnextVestingWallet {
* @inheritdoc IConnextVestingWallet
* @dev This func is needed because only the recipients can claim
*/
function claim(IVestingEscrowSimple _llamaVest) external {
_llamaVest.claim(address(this));
function claim(IVestingEscrowSimple _vestingEscrow) external {
_vestingEscrow.claim(address(this));
}
}
6 changes: 3 additions & 3 deletions solidity/interfaces/IConnextVestingWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ interface IConnextVestingWallet {
//////////////////////////////////////////////////////////////*/

/**
* @notice Claim tokens from Llama Vesting contract
* @param _llamaVest The address of the Llama Vesting contract
* @notice Claim tokens from Yearn Vesting Escrow contract
* @param _vestingEscrow The address of the Yearn Vesting Escrow contract
*/
function claim(IVestingEscrowSimple _llamaVest) external;
function claim(IVestingEscrowSimple _vestingEscrow) external;

/**
* @notice Collect dust from the contract
Expand Down
208 changes: 93 additions & 115 deletions solidity/interfaces/IVestingEscrowSimple.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,190 +3,168 @@ pragma solidity 0.8.20;

interface IVestingEscrowSimple {
/**
* @notice Emits when a new owner confirms his ownership
* @param _admin Address of the new owner
* @notice Initialize the contract.
* @dev This function is seperate from `constructor` because of the factory pattern
* used in `VestingEscrowFactory.deploy_vesting_contract`. It may be called once per deployment.
* @param _owner Owner address
* @param _token Address of the ERC20 token being distributed
* @param _recipient Address to vest tokens for
* @param _amount Amount of tokens being vested for `recipient`
* @param _startTime Epoch time at which token distribution starts
* @param _endTime Time until everything should be vested
* @param _cliffLength Duration (in seconds) after which the first portion vests
* @param _openClaim Switch if anyone can claim for `recipient`
* @return _success Whether or not the initialization was successful
*/
event ApplyOwnership(address _admin);
function initialize(
address _owner,
address _token,
address _recipient,
uint256 _amount,
uint256 _startTime,
uint256 _endTime,
uint256 _cliffLength,
bool _openClaim
) external returns (bool _success);

/**
* @notice Emits when claim func is triggered
* @param _recipient Address to send tokens to
* @param _claimed Amount of claimed tokens
* @notice Get the number of unclaimed, vested tokens for recipient.
* @return _unclaimed The amount of unclaimed tokens.
*/
event Claim(address indexed _recipient, uint256 _claimed);
function unclaimed() external view returns (uint256 _unclaimed);

/**
* @notice Emits when the old owner transfers his ownership
* @param _admin Future admin address
* @notice Get the number of locked tokens for recipient.
* @return _locked The amount of locked tokens.
*/
event CommitOwnership(address _admin);
function locked() external view returns (uint256 _locked);

/**
* @notice Emits on initialization and when the vesting is funded
* @param _recipient Address of the recipient of the vesting tokens
* @param _amount Amount of tokens to be vested
* @notice Claim tokens which have vested for the caller.
* @return _claimed The amount of tokens claimed.
*/
event Fund(address indexed _recipient, uint256 _amount);
function claim() external returns (uint256 _claimed);

/**
* @notice Emits when rug_pull func is triggered
* @param _recipient Address to send tokens to
* @param _rugged Amount of rugged tokens
* @notice Claim tokens which have vested for a specified beneficiary.
* @param _beneficiary The address to transfer claimed tokens to.
* @return _claimed The amount of tokens claimed.
*/
event RugPull(address _recipient, uint256 _rugged);
function claim(address _beneficiary) external returns (uint256 _claimed);

/**
* @notice Admin of the contract
* @return _admin Address of the admin
* @notice Claim a specified amount of tokens which have vested for a specified beneficiary.
* @param _beneficiary The address to transfer claimed tokens to.
* @param _amount The amount of tokens to claim.
* @return _claimed The amount of tokens claimed.
*/
function admin() external view returns (address _admin);
function claim(address _beneficiary, uint256 _amount) external returns (uint256 _claimed);

/**
* @notice Apply pending ownership transfer
* @dev Only future admin can call this func
* @notice Disable further flow of tokens and renounce ownership.
*/
function apply_transfer_ownership() external;
function revoke() external;

/**
* @notice Claim tokens which have vested
* @dev Caller is the beneficiary, claim all available tokens
* @notice Disable further flow of tokens from a specific timestamp and renounce ownership.
* @param _timestamp The timestamp to disable further token flow.
*/
function claim() external;
function revoke(uint256 _timestamp) external;

/**
* @notice Claim tokens which have vested
* @dev Claim all available tokens
* @param _beneficiary Address to send tokens to
* @notice Disable further flow of tokens from a specific timestamp, transfer unvested tokens to a beneficiary, and renounce ownership.
* @param _timestamp The timestamp to disable further token flow.
* @param _beneficiary The address to transfer unvested tokens to.
*/
function claim(address _beneficiary) external;
function revoke(uint256 _timestamp, address _beneficiary) external;

/**
* @notice Claim tokens which have vested
* @dev Claim a specific amount of tokens to a specific address
* @param _beneficiary Address to send tokens to
* @param _amount Amount of tokens to claim
* @notice Renounce owner control of the escrow, effectively making it ownerless.
*/
function claim(address _beneficiary, uint256 _amount) external;
function disown() external;

/**
* @notice Duration after which the first portion vests
* @return _length Duration in seconds
* @notice Disallow or allow anyone to claim tokens for `recipient`.
* @param _openClaim True to allow anyone to claim, false to restrict to recipient only.
*/
function cliff_length() external view returns (uint256 _length);
function set_open_claim(bool _openClaim) external;

/**
* @notice Collect dust tokens
* @dev Collect tokens that are not the vesting token
* @param _token Address of the token to collect
* @notice Transfer any ERC20 tokens sent to the contract address to a beneficiary.
* @param _token The address of the ERC20 token to transfer.
*/
function collect_dust(address _token) external;

/**
* @notice Trigger ownership transfer
* @dev Only admin can call this func
* @param _addr Address of the future admin
* @notice Transfer any ERC20 tokens sent to the contract address to a specified beneficiary.
* @param _token The address of the ERC20 token to transfer.
* @param _beneficiary The address to transfer the tokens to.
*/
function commit_transfer_ownership(address _addr) external;
function collect_dust(address _token, address _beneficiary) external;

/**
* @notice Get time when the vesting will be disabled
* @dev Set to a vesting end date
* @return _timestamp Timestamp when the vesting will be disabled
* @notice Returns the address of the recipient.
* @return _recipient The address of the recipient.
*/
function disabled_at() external view returns (uint256 _timestamp);

/**
* @notice Time when the vesting will finish
* @dev Set to a vesting end date
* @return _timestamp Timestamp when the vesting will finish
*/
function end_time() external view returns (uint256 _timestamp);

/**
* @notice Pending admin address
* @return _futureAdmin Address of the future admin
*/
function future_admin() external view returns (address _futureAdmin);

/**
* @notice Initialize the contract.
* @dev This function is seperate from `constructor` because of the factory pattern
* used in `VestingEscrowFactory.deploy_vesting_contract`. It may be called
* once per deployment.
* @param _admin Admin address
* @param _token Address of the ERC20 token being distributed
* @param _recipient Address to vest tokens for
* @param _amount Amount of tokens being vested for `recipient`
* @param _startTime Epoch time at which token distribution starts
* @param _endTime Time until everything should be vested
* @param _cliffLength Duration after which the first portion vests
* @return _success Whether or not the initialization was successful
*/
function initialize(
address _admin,
address _token,
address _recipient,
uint256 _amount,
uint256 _startTime,
uint256 _endTime,
uint256 _cliffLength
) external returns (bool _success);
function recipient() external view returns (address _recipient);

/**
* @notice Check if the contract is initialized
* @return _isInitialized Whether or not the contract is initialized
* @notice Returns the address of the token being vested.
* @return _token The ERC20 token address.
*/
function initialized() external view returns (bool _isInitialized);
function token() external view returns (address _token);

/**
* @notice Get the number of locked tokens for recipient
* @return _amount Amount of locked tokens
* @notice Returns the start time of the vesting period.
* @return _startTime The start time as a UNIX timestamp.
*/
function locked() external view returns (uint256 _amount);
function start_time() external view returns (uint256 _startTime);

/**
* @notice Recipient of the vesting tokens.
* @return _recipient Address of the recipient
* @notice Returns the end time of the vesting period.
* @return _endTime The end time as a UNIX timestamp.
*/
function recipient() external view returns (address _recipient);
function end_time() external view returns (uint256 _endTime);

/**
* @notice Renounce admin control of the escrow
* @notice Returns the cliff length in seconds.
* @return _cliffLength The cliff length in seconds.
*/
function renounce_ownership() external;
function cliff_length() external view returns (uint256 _cliffLength);

/**
* @notice Disable further flow of tokens and clawback the unvested part to admin
* @notice Returns the total amount of tokens locked in the contract.
* @return _totalLocked The total amount of locked tokens.
*/
function rug_pull() external;
function total_locked() external view returns (uint256 _totalLocked);

/**
* @notice Vesting start time
* @return _startTime Timestamp when the vesting will start
* @notice Returns the total amount of tokens claimed by the recipient.
* @return _totalClaimed The total amount of claimed tokens.
*/
function start_time() external view returns (uint256 _startTime);
function total_claimed() external view returns (uint256 _totalClaimed);

/**
* @notice Vesting token
* @return _vestingToken Address of the vesting token
* @notice Returns the timestamp at which the token flow was disabled, if ever.
* @return _disabledAt The disable timestamp, or the end time if never disabled.
*/
function token() external view returns (address _vestingToken);
function disabled_at() external view returns (uint256 _disabledAt);

/**
* @notice Total amount of tokens that are already claimed
* @return _amount Amount of tokens
* @notice Indicates whether anyone can claim tokens on behalf of the recipient.
* @return _openClaim True if open claiming is enabled, false otherwise.
*/
function total_claimed() external view returns (uint256 _amount);
function open_claim() external view returns (bool _openClaim);

/**
* @notice Get the number of locked tokens for recipient
* @return _amount Amount of tokens
* @notice Indicates whether the contract has been initialized.
* @return _initialized True if the contract has been initialized, false otherwise.
*/
function total_locked() external view returns (uint256 _amount);
function initialized() external view returns (bool _initialized);

/**
* @notice Get the number of unclaimed, vested tokens for recipient
* @return _amount Amount of tokens
* @notice Returns the address of the owner.
* @return _owner The address of the owner.
*/
function unclaimed() external view returns (uint256 _amount);
function owner() external view returns (address _owner);
}
19 changes: 0 additions & 19 deletions solidity/test/integration/ConnextVestingWallet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {ConnextVestingWallet, IConnextVestingWallet} 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 {
Expand All @@ -24,26 +21,10 @@ contract UnitConnextVestingWallet is Test, Constants {
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);
Expand Down
Loading

0 comments on commit b2d736f

Please sign in to comment.