Skip to content

Individual vesting schedule by start, end, and cliff #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: good-main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/hardhat/contracts/claim/PeriodicTranches.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

contract PeriodicTranches {

struct Tranche {
uint128 time; // Timestamp upon which the tranche vests
uint128 vestedFraction; // Fraction of tokens unlockable as basis points
}

// Generate an array of tranches based on input parameters
function generateTranches(uint256 start, uint256 end, uint256 cliff) public pure returns (Tranche[] memory) {
require(end > start, "End must be greater than start");
require(cliff >= start && cliff <= end, "Cliff must be between start and end");

uint256 vestingStart = cliff > start ? cliff : start;
uint256 vestingDuration = end - vestingStart;

// Calculate the number of periods as months between vestingStart and end
// 30.44 days per month, is this accurate?
uint256 secondsPerMonth = 30.44 days;
uint256 periods = vestingDuration / secondsPerMonth;

// Ensure there is at least one period
if (periods == 0) {
periods = 1;
}

Tranche[] memory tranches = new Tranche[](periods);
uint256 periodDuration = vestingDuration / periods;
uint256 totalVestedFraction = 0;

for (uint256 i = 0; i < periods; i++) {
uint256 trancheTime = vestingStart + (periodDuration * (i + 1));
// What value should we use instead of 10,000?
uint256 fractionPerPeriod = (i + 1) * (10000 / periods);
uint128 vestedFraction = uint128(fractionPerPeriod - totalVestedFraction);
totalVestedFraction += vestedFraction;

// Adjust the last tranche to ensure total vested fraction equals 10000
if (i == periods - 1 && totalVestedFraction != 10000) {
vestedFraction += (10000 - uint128(totalVestedFraction));
}

tranches[i] = Tranche(uint128(trancheTime), vestedFraction);
}

return tranches;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ abstract contract ContinuousVesting is AdvancedDistributor, IContinuousVesting {
cliff = _cliff;
end = _end;

// No longer need to initialize start, end, and cliff
// emit SetContinuousVesting(start, cliff, end);
emit SetContinuousVesting(start, cliff, end);
}

function getVestedFraction(
address beneficiary,
uint256 time // time is in seconds past the epoch (e.g. block.timestamp)
uint256 time // time is in seconds past the epoch (e.g. block.timestamp),
// start, cliff, end
) public view override returns (uint256) {
uint256 delayedTime = time- getFairDelayTime(beneficiary);
// no tokens are vested
Expand Down Expand Up @@ -75,6 +78,7 @@ abstract contract ContinuousVesting is AdvancedDistributor, IContinuousVesting {
start = _start;
cliff = _cliff;
end = _end;
// We probably don't need this function anymore, as it'll be per address
emit SetContinuousVesting(start, cliff, end);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import '@openzeppelin/contracts/access/Ownable.sol';
import { ERC20Votes, ERC20Permit, ERC20 } from '@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol';
import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';

import { PerAddressDistributor, PerAddressDistributionRecord, IERC20 } from './PerAddressDistributor.sol';
import { IAdjustable } from '../../interfaces/IAdjustable.sol';
import { IVoting } from '../../interfaces/IVoting.sol';
import { Sweepable } from '../../utilities/Sweepable.sol';
import { FairQueue } from '../../utilities/FairQueue.sol';

/**
* @title AdvancedDistributor
* @notice Distributes tokens to beneficiaries with voting-while-vesting and administrative controls.
* The contract owner can control these distribution parameters:
* - the merkle root determining all distribution details
* - adjustments to specific distributions
* - the token being distributed
* - the total amount to distribute
* - the metadata URI
* - the voting power of undistributed tokens
* - the recipient of swept funds
*
* This contract also allows owners to perform several other admin functions
* - updating the contract owner
* - sweeping tokens and native currency to a recipient
*
* This contract tracks beneficiary voting power through an internal ERC20Votes token that cannot be transferred. The
* beneficiary must delegate to an address to use this voting power. Their voting power decreases when the token is claimed.
*
* @dev Updates to the contract must follow these constraints:
* - If a merkle root update alters the total token quantity to distribute across all users, also adjust the total value.
* The DistributionRecord for each beneficiary updated in the merkle root will be incorrect until a claim is executed.
* - If the total changes, make sure to add or withdraw tokens being distributed to match the new total.
*/
abstract contract PerAddressAdvancedDistributor is
Ownable,
Sweepable,
ERC20Votes,
PerAddressDistributor,
IAdjustable,
IVoting,
FairQueue
{
using SafeERC20 for IERC20;

uint256 private voteFactor;

constructor(
IERC20 _token,
uint256 _total,
string memory _uri,
uint256 _voteFactor,
uint256 _fractionDenominator,
uint160 _maxDelayTime,
uint160 _salt
)
PerAddressDistributor(_token, _total, _uri, _fractionDenominator)
ERC20Permit('Internal vote tracker')
ERC20('Internal vote tracker', 'IVT')
Sweepable(payable(msg.sender))
FairQueue(_maxDelayTime, _salt)
{
voteFactor = _voteFactor;
emit SetVoteFactor(voteFactor);
}

/**
* convert a token quantity to a vote quantity
*/
function tokensToVotes(uint256 tokenAmount) private view returns (uint256) {
return (tokenAmount * voteFactor) / fractionDenominator;
}

// Update voting power based on distribution record initialization or claims
function _reconcileVotingPower(address beneficiary) private {
// current potential voting power
uint256 currentVotes = balanceOf(beneficiary);
// correct voting power after initialization, claim, or adjustment
PerAddressDistributionRecord memory record = records[beneficiary];
uint256 newVotes = record.claimed >= record.total ? 0 : tokensToVotes(record.total - record.claimed);

if (currentVotes > newVotes) {
// reduce voting power through ERC20Votes extension
_burn(beneficiary, currentVotes - newVotes);
} else if (currentVotes < newVotes) {
// increase voting power through ERC20Votes extension
_mint(beneficiary, newVotes - currentVotes);
}
}

function _initializeDistributionRecord(
address beneficiary,
uint256 totalAmount,
uint256 start,
uint256 end,
uint256 cliff
) internal virtual override {
super._initializeDistributionRecord(beneficiary, totalAmount, start, end, cliff);
_reconcileVotingPower(beneficiary);
}

function _executeClaim(
address beneficiary,
uint256 totalAmount,
uint256 start,
uint256 end,
uint256 cliff
) internal virtual override returns (uint256 _claimed) {
_claimed = super._executeClaim(beneficiary, totalAmount, start, end, cliff);
_reconcileVotingPower(beneficiary);
}

/**
* @dev Adjust the quantity claimable by a user, overriding the value in the distribution record.
*
* Note: If used in combination with merkle proofs, adjustments to a beneficiary's total could be
* reset by anyone to the value in the merkle leaf at any time. Update the merkle root instead.
*
* Amount is limited to type(uint120).max to allow each DistributionRecord to be packed into a single storage slot.
*/
function adjust(address beneficiary, int256 amount) external onlyOwner {
PerAddressDistributionRecord memory distributionRecord = records[beneficiary];
require(distributionRecord.initialized, 'must initialize before adjusting');

uint256 diff = uint256(amount > 0 ? amount : -amount);
require(diff < type(uint120).max, 'adjustment > max uint120');

if (amount < 0) {
// decreasing claimable tokens
require(total >= diff, 'decrease greater than distributor total');
require(distributionRecord.total >= diff, 'decrease greater than distributionRecord total');
total -= diff;
records[beneficiary].total -= uint120(diff);
token.safeTransfer(owner(), diff);
} else {
// increasing claimable tokens
total += diff;
records[beneficiary].total += uint120(diff);
}
_reconcileVotingPower(beneficiary);
emit Adjust(beneficiary, amount);
}


function _setToken(IERC20 _token) internal virtual {
require(address(_token) != address(0), 'token is address(0)');
token = _token;
emit SetToken(token);
}

// Set the token being distributed
function setToken(IERC20 _token) external virtual onlyOwner {
_setToken(_token);
}

function _setTotal(uint256 _total) internal virtual {
total = _total;
emit SetTotal(total);
}

// Set the total to distribute
function setTotal(uint256 _total) external virtual onlyOwner {
_setTotal(_total);
}

// Set the distributor metadata URI
function setUri(string memory _uri) external onlyOwner {
uri = _uri;
emit SetUri(uri);
}

// set the recipient of swept funds
function setSweepRecipient(address payable _recipient) external onlyOwner {
_setSweepRecipient(_recipient);
}

function getTotalVotes() external view returns (uint256) {
// supply of internal token used to track voting power
return totalSupply();
}

function getVoteFactor(address) external view returns (uint256) {
return voteFactor;
}

/**
* @notice Set the voting power of undistributed tokens
* @param _voteFactor The voting power multiplier as a fraction of fractionDenominator
* @dev The vote factor can be any integer. If voteFactor / fractionDenominator == 1,
* one unclaimed token provides one vote. If voteFactor / fractionDenominator == 2, one
* unclaimed token counts as two votes.
*/
function setVoteFactor(uint256 _voteFactor) external onlyOwner {
voteFactor = _voteFactor;
emit SetVoteFactor(voteFactor);
}

/**
* @dev the internal token used only for tracking voting power cannot be transferred
*/
function _approve(address, address, uint256) internal pure override {
revert('disabled for voting power');
}

/**
* @dev the internal token used only f or tracking voting power cannot be transferred
*/
function _transfer(address, address, uint256) internal pure override {
revert('disabled for voting power');
}
}
Loading