From 777312e038320637ca35fecf50c275db8656acff Mon Sep 17 00:00:00 2001 From: randall Date: Thu, 22 Feb 2024 08:29:42 -0800 Subject: [PATCH 1/7] Add WIP for per address vesting config --- .../claim/abstract/ContinuousVesting.sol | 21 ++++++++++++------- .../claim/abstract/TrancheVesting.sol | 6 +++++- .../factory/DistributorInitializable.sol | 2 ++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol b/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol index 9f9a252d..2f9a9b63 100644 --- a/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol +++ b/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol @@ -24,20 +24,23 @@ abstract contract ContinuousVesting is AdvancedDistributor, IContinuousVesting { // use a large fraction denominator to provide the highest resolution on continuous vesting. AdvancedDistributor(_token, _total, _uri, _voteFactor, 10**18, _maxDelayTime, _salt) { - require(_start <= _cliff, "vesting cliff before start"); - require(_cliff <= _end, "vesting end before cliff"); - require(_end <= 4102444800, "vesting ends after 4102444800 (Jan 1 2100)"); + // require(_start <= _cliff, "vesting cliff before start"); + // require(_cliff <= _end, "vesting end before cliff"); + // require(_end <= 4102444800, "vesting ends after 4102444800 (Jan 1 2100)"); - start = _start; - cliff = _cliff; - end = _end; + // start = _start; + // cliff = _cliff; + // end = _end; - emit SetContinuousVesting(start, cliff, end); + // No longer need to initialize start, end, and cliff + // emit SetContinuousVesting(start, cliff, end); + emit SetContinuousVesting(); } 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 @@ -63,6 +66,7 @@ abstract contract ContinuousVesting is AdvancedDistributor, IContinuousVesting { uint256 ) { + // We probably need to update this so that it takes in an address return (start, cliff, end); } @@ -75,6 +79,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); } } diff --git a/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol b/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol index f10ed9d8..accbec24 100644 --- a/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol +++ b/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol @@ -35,8 +35,12 @@ abstract contract TrancheVesting is AdvancedDistributor, ITrancheVesting { */ function getVestedFraction( address beneficiary, - uint256 time + uint256 time, + // start, end, cliff ) public view override returns (uint256) { + // tranches = calculate the the tranche from start, end, cliff from record, record.start, record.end, record.cliff, + // calculateTranche(beneficiary, record); + uint256 delay = getFairDelayTime(beneficiary); for (uint256 i = tranches.length; i != 0; ) { unchecked { diff --git a/packages/hardhat/contracts/claim/factory/DistributorInitializable.sol b/packages/hardhat/contracts/claim/factory/DistributorInitializable.sol index dae28cc7..21bf5cef 100644 --- a/packages/hardhat/contracts/claim/factory/DistributorInitializable.sol +++ b/packages/hardhat/contracts/claim/factory/DistributorInitializable.sol @@ -110,6 +110,8 @@ abstract contract DistributorInitializable is Initializable, IDistributor, Reent DistributionRecord memory record = records[beneficiary]; + // Pass in start, end, cliff from record + // uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, record.start, record.end, record.cliff)) / fractionDenominator; uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp)) / fractionDenominator; return record.claimed >= claimable ? 0 // no more tokens to claim From fbdfabe5bc3d6d1a8c3459e75de15325caa9cbf0 Mon Sep 17 00:00:00 2001 From: Jack Dishman Date: Fri, 23 Feb 2024 16:04:09 -0500 Subject: [PATCH 2/7] setup --- .../PerAddressAdvancedDistributor.sol | 214 ++++++++++++++++++ .../claim/abstract/PerAddressDistributor.sol | 141 ++++++++++++ .../abstract/PerAddressTrancheVesting.sol | 110 +++++++++ .../claim/abstract/TrancheVesting.sol | 2 +- ...ddressAdvancedDistributorInitializable.sol | 213 +++++++++++++++++ .../PerAddressDistributorInitializable.sol | 127 +++++++++++ .../PerAddressTrancheVestingInitializable.sol | 104 +++++++++ ...AddressTrancheVestingMerkleDistributor.sol | 73 ++++++ .../interfaces/IPerAddressDistributor.sol | 82 +++++++ .../interfaces/IPerAddressTrancheVesting.sol | 19 ++ 10 files changed, 1084 insertions(+), 1 deletion(-) create mode 100644 packages/hardhat/contracts/claim/abstract/PerAddressAdvancedDistributor.sol create mode 100644 packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol create mode 100644 packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol create mode 100644 packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol create mode 100644 packages/hardhat/contracts/claim/factory/PerAddressDistributorInitializable.sol create mode 100644 packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol create mode 100644 packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol create mode 100644 packages/hardhat/contracts/interfaces/IPerAddressDistributor.sol create mode 100644 packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol diff --git a/packages/hardhat/contracts/claim/abstract/PerAddressAdvancedDistributor.sol b/packages/hardhat/contracts/claim/abstract/PerAddressAdvancedDistributor.sol new file mode 100644 index 00000000..587dd027 --- /dev/null +++ b/packages/hardhat/contracts/claim/abstract/PerAddressAdvancedDistributor.sol @@ -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'); + } +} diff --git a/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol b/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol new file mode 100644 index 00000000..bfe5bf58 --- /dev/null +++ b/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { ReentrancyGuard } from '@openzeppelin/contracts/security/ReentrancyGuard.sol'; + +import { IPerAddressDistributor, PerAddressDistributionRecord } from '../../interfaces/IPerAddressDistributor.sol'; + +/** + * @title Distributor + * @notice Distributes funds to beneficiaries and tracks distribution status + */ +abstract contract PerAddressDistributor is IPerAddressDistributor, ReentrancyGuard { + using SafeERC20 for IERC20; + + mapping(address => PerAddressDistributionRecord) internal records; // track distribution records per user + IERC20 public token; // the token being claimed + uint256 public total; // total tokens allocated for claims + uint256 public claimed; // tokens already claimed + string public uri; // ipfs link on distributor info + uint256 immutable fractionDenominator; // denominator for vesting fraction (e.g. if vested fraction is 100 and fractionDenominator is 10000, 1% of tokens have vested) + + // provide context on the contract name and version + function NAME() external view virtual returns (string memory); + + function VERSION() external view virtual returns (uint256); + + constructor(IERC20 _token, uint256 _total, string memory _uri, uint256 _fractionDenominator) { + require(address(_token) != address(0), 'Distributor: token is address(0)'); + require(_total > 0, 'Distributor: total is 0'); + + token = _token; + total = _total; + uri = _uri; + fractionDenominator = _fractionDenominator; + emit InitializeDistributor(token, total, uri, fractionDenominator); + } + + /** + * @dev Set up the distribution record for a user. Permissions are not checked in this function. + * Amount is limited to type(uint120).max to allow each DistributionRecord to be packed into a single storage slot. + * + * @param beneficiary The address of the beneficiary + * @param _totalAmount The total amount of tokens to be distributed to the beneficiary + */ + function _initializeDistributionRecord( + address beneficiary, + uint256 _totalAmount, + uint256 _start, + uint256 _end, + uint256 _cliff + ) internal virtual { + + // Checks + require(_totalAmount <= type(uint120).max, 'Distributor: totalAmount > type(uint120).max'); + uint120 totalAmount = uint120(_totalAmount); + + // Effects - note that the existing claimed quantity is re-used during re-initialization + records[beneficiary] = PerAddressDistributionRecord(true, totalAmount, records[beneficiary].claimed, _start, _end, _cliff); + emit InitializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); + } + + /** + * @notice Record the claim internally: + * @dev This function does not check permissions: caller must verify the claim is valid! + * this function should not call any untrusted external contracts to avoid reentrancy + */ + function _executeClaim( + address beneficiary, + uint256 _totalAmount, + uint256 _start, + uint256 _end, + uint256 _cliff + ) internal virtual returns (uint256) { + uint120 totalAmount = uint120(_totalAmount); + + // effects + if (records[beneficiary].total != totalAmount) { + // re-initialize if the total has been updated + _initializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); + } + if(records[beneficiary].start != _start || records[beneficiary].end != _end || records[beneficiary].cliff != _cliff) { + _initializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); + } + + uint120 claimableAmount = uint120(getClaimableAmount(beneficiary)); + require(claimableAmount > 0, 'Distributor: no more tokens claimable right now'); + + + records[beneficiary].claimed += claimableAmount; + claimed += claimableAmount; + + return claimableAmount; + } + + /** + * @dev Move tokens associated with the claim to the recipient. This function should be called + * after the claim has been executed internally to avoid reentrancy issues. + * @param _recipient The address of the recipient + * @param _amount The amount of tokens to be transferred during this claim + */ + function _settleClaim(address _recipient, uint256 _amount) internal virtual { + token.safeTransfer(_recipient, _amount); + emit Claim(_recipient, _amount); + } + + /// @notice return a distribution record + function getDistributionRecord( + address beneficiary + ) external view virtual returns (PerAddressDistributionRecord memory) { + return records[beneficiary]; + } + + // Get tokens vested as fraction of fractionDenominator + function getVestedFraction( + address beneficiary, + uint256 time, + uint256 start, + uint256 end, + uint256 cliff + ) public view virtual returns (uint256); + + function getFractionDenominator() public view returns (uint256) { + return fractionDenominator; + } + + // get the number of tokens currently claimable by a specific use + function getClaimableAmount(address beneficiary) public view virtual returns (uint256) { + require(records[beneficiary].initialized, 'Distributor: claim not initialized'); + + PerAddressDistributionRecord memory record = records[beneficiary]; + + uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, record.start, record.end, record.cliff)) / + fractionDenominator; + return + record.claimed >= claimable + ? 0 // no more tokens to claim + : claimable - record.claimed; // claim all available tokens + } +} diff --git a/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol b/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol new file mode 100644 index 00000000..bda64d1b --- /dev/null +++ b/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { PerAddressAdvancedDistributor } from './PerAddressAdvancedDistributor.sol'; +import { IPerAddressTrancheVesting, Tranche } from '../../interfaces/IPerAddressTrancheVesting.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import "hardhat/console.sol"; + +/** + * @title TrancheVesting + * @notice Distributes funds to beneficiaries over time in tranches. + */ +abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPerAddressTrancheVesting { + // time and vested fraction must monotonically increase in the tranche array + + constructor( + IERC20 _token, + uint256 _total, + string memory _uri, + uint256 _voteFactor, + uint160 _maxDelayTime, + uint160 _salt + ) PerAddressAdvancedDistributor(_token, _total, _uri, _voteFactor, 10000, _maxDelayTime, _salt) { + // tranches can be set in the constructor or in setTranches() + } + + /** + * @notice Get the vested fraction for a beneficiary at a given time. + * @dev Before the first tranche time, the vested fraction will be 0. At times between + * tranche_i and tranche_i+1, the vested fraction will be tranche_i+1's vested fraction. + * After the last tranche time, the vested fraction will be the fraction denominator. + */ + function getVestedFraction( + address beneficiary, + uint256 time, + uint256 start, + uint256 end, + uint256 cliff + ) public view override returns (uint256) { + // tranches = calculate the the tranche from start, end, cliff from record, record.start, record.end, record.cliff, + // calculateTranche(beneficiary, record); + + // TODO: calculate tranches with start, end, cliff + + uint256 delay = getFairDelayTime(beneficiary); + for (uint256 i = tranches.length; i != 0; ) { + unchecked { + --i; + } + + if (time - delay > tranches[i].time) { + return tranches[i].vestedFraction; + } + } + + return 0; + } + + // Get a single tranche + function getTranche(uint256 i) public view returns (Tranche memory) { + return tranches[i]; + } + + // Get all tranches + function getTranches() public view returns (Tranche[] memory) { + return tranches; + } + + /** + * @dev Tranches can be updated at any time. The quantity of tokens a user can claim is based on the + * claimable quantity at the time of the claim and the quantity of tokens already claimed by the user. + */ + function _setTranches(Tranche[] memory _tranches) private { + require(_tranches.length != 0, 'tranches required'); + + delete tranches; + + uint128 lastTime = 0; + uint128 lastVestedFraction = 0; + + for (uint256 i = 0; i < _tranches.length; ) { + require(_tranches[i].vestedFraction != 0, 'tranche vested fraction == 0'); + require(_tranches[i].time > lastTime, 'tranche time must increase'); + require( + _tranches[i].vestedFraction > lastVestedFraction, + 'tranche vested fraction must increase' + ); + lastTime = _tranches[i].time; + lastVestedFraction = _tranches[i].vestedFraction; + tranches.push(_tranches[i]); + + emit SetTranche(i, lastTime, lastVestedFraction); + + unchecked { + ++i; + } + } + + require(lastTime <= 4102444800, 'vesting ends after 4102444800 (Jan 1 2100)'); + require(lastVestedFraction == fractionDenominator, 'last tranche must vest all tokens'); + } + + /** + * @notice Set the vesting tranches. Tranches must be sorted by time and vested fraction must monotonically increase. + * The last tranche must vest all tokens (vestedFraction == fractionDenominator) + */ + function setTranches(Tranche[] memory _tranches) external onlyOwner { + _setTranches(_tranches); + } +} diff --git a/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol b/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol index accbec24..d5d92d67 100644 --- a/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol +++ b/packages/hardhat/contracts/claim/abstract/TrancheVesting.sol @@ -35,7 +35,7 @@ abstract contract TrancheVesting is AdvancedDistributor, ITrancheVesting { */ function getVestedFraction( address beneficiary, - uint256 time, + uint256 time // start, end, cliff ) public view override returns (uint256) { // tranches = calculate the the tranche from start, end, cliff from record, record.start, record.end, record.cliff, diff --git a/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol b/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol new file mode 100644 index 00000000..290c8c10 --- /dev/null +++ b/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +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 {PerAddressDistributorInitializable, PerAddressDistributionRecord, IERC20} from "./PerAddressDistributorInitializable.sol"; +import {IAdjustable} from "../../interfaces/IAdjustable.sol"; +import {IVoting} from "../../interfaces/IVoting.sol"; +import {Sweepable} from "../../utilities/Sweepable.sol"; +import "./FairQueueInitializable.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 PerAddressDistributionRecord 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 PerAddressAdvancedDistributorInitializable is + Initializable, + Ownable, + Sweepable, + ERC20Votes, + PerAddressDistributorInitializable, + IAdjustable, + IVoting, + FairQueueInitializable +{ + using SafeERC20 for IERC20; + + uint256 private voteFactor; + + constructor() + ERC20Permit("Internal vote tracker") + ERC20("Internal vote tracker", "IVT") + Sweepable(payable(msg.sender)) + {} + + function __AdvancedDistributor_init( + IERC20 _token, + uint256 _total, + string memory _uri, + uint256 _voteFactor, + uint256 _fractionDenominator, + uint160 _maxDelayTime, + uint160 _salt, + address _owner + ) internal onlyInitializing { + _setSweepRecipient(payable(_owner)); + voteFactor = _voteFactor; + emit SetVoteFactor(voteFactor); + + __Distributor_init(_token, _total, _uri, _fractionDenominator); + __FairQueue_init(_maxDelayTime, _salt); + } + + /** + * 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) + internal + virtual + override + returns (uint256 _claimed) + { + _claimed = super._executeClaim(beneficiary, totalAmount); + _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 perAddressDistributionRecord = records[beneficiary]; + require(perAddressDistributionRecord.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(perAddressDistributionRecord.total >= diff, "decrease greater than perAddressDistributionRecord 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 for tracking voting power cannot be transferred + */ + function _transfer(address, address, uint256) internal pure override { + revert("disabled for voting power"); + } +} diff --git a/packages/hardhat/contracts/claim/factory/PerAddressDistributorInitializable.sol b/packages/hardhat/contracts/claim/factory/PerAddressDistributorInitializable.sol new file mode 100644 index 00000000..ad3f37eb --- /dev/null +++ b/packages/hardhat/contracts/claim/factory/PerAddressDistributorInitializable.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +import {IPerAddressDistributor, PerAddressDistributionRecord} from "../../interfaces/IPerAddressDistributor.sol"; + +/** + * @title Distributor + * @notice Distributes funds to beneficiaries and tracks distribution status + */ +abstract contract PerAddressDistributorInitializable is Initializable, IPerAddressDistributor, ReentrancyGuard { + using SafeERC20 for IERC20; + + mapping(address => PerAddressDistributionRecord) internal records; // track distribution records per user + IERC20 public token; // the token being claimed + uint256 public total; // total tokens allocated for claims + uint256 public claimed; // tokens already claimed + string public uri; // ipfs link on distributor info + uint256 internal fractionDenominator; // denominator for vesting fraction (e.g. if vested fraction is 100 and fractionDenominator is 10000, 1% of tokens have vested) + + // provide context on the contract name and version + function NAME() external view virtual returns (string memory); + + function VERSION() external view virtual returns (uint256); + + function __Distributor_init(IERC20 _token, uint256 _total, string memory _uri, uint256 _fractionDenominator) + internal + onlyInitializing + { + require(address(_token) != address(0), "Distributor: token is address(0)"); + require(_total > 0, "Distributor: total is 0"); + + token = _token; + total = _total; + uri = _uri; + fractionDenominator = _fractionDenominator; + emit InitializeDistributor(token, total, uri, fractionDenominator); + } + + /** + * @dev Set up the distribution record for a user. Permissions are not checked in this function. + * Amount is limited to type(uint120).max to allow each DistributionRecord to be packed into a single storage slot. + * + * @param beneficiary The address of the beneficiary + * @param _totalAmount The total amount of tokens to be distributed to the beneficiary + * @param _start The start time of the vesting schedule + * @param _end The end time of the vesting schedule + * @param _cliff The cliff time of the vesting schedule + */ + function _initializeDistributionRecord(address beneficiary, uint256 _totalAmount, uint256 _start, uint256 _end, uint256 _cliff) internal virtual { + // Checks + require(_totalAmount <= type(uint120).max, "Distributor: totalAmount > type(uint120).max"); + // TODO: add checks for start, end, cliff + uint120 totalAmount = uint120(_totalAmount); + + // Effects - note that the existing claimed quantity is re-used during re-initialization + records[beneficiary] = PerAddressDistributionRecord(true, totalAmount, records[beneficiary].claimed, _start, _end, _cliff); + emit InitializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); + } + + /** + * @notice Record the claim internally: + * @dev This function does not check permissions: caller must verify the claim is valid! + * this function should not call any untrusted external contracts to avoid reentrancy + */ + function _executeClaim(address beneficiary, uint256 _totalAmount, uint256 _start, uint256 _end, uint256 _cliff) internal virtual returns (uint256) { + uint120 totalAmount = uint120(_totalAmount); + + // effects + if (records[beneficiary].total != totalAmount) { + // re-initialize if the total has been updated + _initializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); + } + if(records[beneficiary].start != _start || records[beneficiary].end != _end || records[beneficiary].cliff != _cliff) { + _initializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); + } + + uint120 claimableAmount = uint120(getClaimableAmount(beneficiary)); + require(claimableAmount > 0, "Distributor: no more tokens claimable right now"); + + records[beneficiary].claimed += claimableAmount; + claimed += claimableAmount; + + return claimableAmount; + } + + /** + * @dev Move tokens associated with the claim to the recipient. This function should be called + * after the claim has been executed internally to avoid reentrancy issues. + * @param _recipient The address of the recipient + * @param _amount The amount of tokens to be transferred during this claim + */ + function _settleClaim(address _recipient, uint256 _amount) internal virtual { + token.safeTransfer(_recipient, _amount); + emit Claim(_recipient, _amount); + } + + /// @notice return a distribution record + function getDistributionRecord(address beneficiary) external view virtual returns (PerAddressDistributionRecord memory) { + return records[beneficiary]; + } + + // Get tokens vested as fraction of fractionDenominator + function getVestedFraction(address beneficiary, uint256 time, uint256 start, uint256 end, uint256 cliff) public view virtual returns (uint256); + + function getFractionDenominator() public view returns (uint256) { + return fractionDenominator; + } + + // get the number of tokens currently claimable by a specific use + function getClaimableAmount(address beneficiary) public view virtual returns (uint256) { + require(records[beneficiary].initialized, "Distributor: claim not initialized"); + + PerAddressDistributionRecord memory record = records[beneficiary]; + + // Pass in start, end, cliff from record + uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp, record.start, record.end, record.cliff)) / fractionDenominator; + // uint256 claimable = (record.total * getVestedFraction(beneficiary, block.timestamp)) / fractionDenominator; + return record.claimed >= claimable + ? 0 // no more tokens to claim + : claimable - record.claimed; // claim all available tokens + } +} diff --git a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol new file mode 100644 index 00000000..6a129610 --- /dev/null +++ b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./PerAddressAdvancedDistributorInitializable.sol"; +import {IPerAddressTrancheVesting, Tranche} from "../../interfaces/IPerAddressTrancheVesting.sol"; + +abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAddressAdvancedDistributorInitializable, IPerAddressTrancheVesting { + + function __TrancheVesting_init( + IERC20 _token, + uint256 _total, + string memory _uri, + uint160 _maxDelayTime, + uint160 _salt, + address _owner + ) internal onlyInitializing { + __AdvancedDistributor_init( + _token, + _total, + _uri, + 10000, // 1x voting power + 10000, // vested fraction + _maxDelayTime, + _salt, + _owner + ); + } + + /** + * @notice Get the vested fraction for a beneficiary at a given time. + * @dev Before the first tranche time, the vested fraction will be 0. At times between + * tranche_i and tranche_i+1, the vested fraction will be tranche_i+1's vested fraction. + * After the last tranche time, the vested fraction will be the fraction denominator. + */ + function getVestedFraction(address beneficiary, uint256 time, uint256 start, uint256 end, uint256 cliff) public view override returns (uint256) { + + // tranches = calculate the the tranche from start, end, cliff from record, record.start, record.end, record.cliff, + Tranche[] tranches = []; + uint256 delay = getFairDelayTime(beneficiary); + for (uint256 i = tranches.length; i != 0;) { + unchecked { + --i; + } + + if (time - delay > tranches[i].time) { + return tranches[i].vestedFraction; + } + } + + return 0; + } + + // Get a single tranche + function getTranche(uint256 i) public view returns (Tranche memory) { + // return tranches[i]; + } + + // TODO: get tranche for address + function getTranches() public view returns (Tranche[] memory) { + // return tranches; + } + + /** + * @dev Tranches can be updated at any time. The quantity of tokens a user can claim is based on the + * claimable quantity at the time of the claim and the quantity of tokens already claimed by the user. + */ + function _setTranches(Tranche[] memory _tranches) private { + require(_tranches.length != 0, "tranches required"); + + // delete tranches; + + uint128 lastTime = 0; + uint128 lastVestedFraction = 0; + + for (uint256 i = 0; i < _tranches.length;) { + require(_tranches[i].vestedFraction != 0, "tranche vested fraction == 0"); + require(_tranches[i].time > lastTime, "tranche time must increase"); + require(_tranches[i].vestedFraction > lastVestedFraction, "tranche vested fraction must increase"); + lastTime = _tranches[i].time; + lastVestedFraction = _tranches[i].vestedFraction; + tranches.push(_tranches[i]); + + emit SetTranche(i, lastTime, lastVestedFraction); + + unchecked { + ++i; + } + } + + require(lastTime <= 4102444800, "vesting ends after 4102444800 (Jan 1 2100)"); + require(lastVestedFraction == fractionDenominator, "last tranche must vest all tokens"); + } + + /** + * @notice Set the vesting tranches. Tranches must be sorted by time and vested fraction must monotonically increase. + * The last tranche must vest all tokens (vestedFraction == fractionDenominator) + */ + function setTranches(Tranche[] memory _tranches) external onlyOwner { + _setTranches(_tranches); + } +} diff --git a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol new file mode 100644 index 00000000..3ef1a47c --- /dev/null +++ b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./PerAddressTrancheVestingInitializable.sol"; +import "./MerkleSetInitializable.sol"; + +contract PerAddressTrancheVestingMerkleDistributor is + Initializable, + PerAddressTrancheVestingInitializable, + MerkleSetInitializable +{ + constructor() { + _disableInitializers(); + } + + function initialize( + IERC20 _token, // the token being claimed + uint256 _total, // the total claimable by all users + string memory _uri, // information on the sale (e.g. merkle proofs) + bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time), + uint160 _maxDelayTime, // the maximum delay time for the fair queue + address _owner + ) public initializer { + __TrancheVesting_init(_token, _total, _uri, _maxDelayTime, uint160(uint256(_merkleRoot)), _owner); + + __MerkleSet_init(_merkleRoot); + + _transferOwnership(_owner); + } + + function NAME() external pure override returns (string memory) { + return "PerAddressTrancheVestingMerkleDistributor"; + } + + function VERSION() external pure override returns (uint256) { + return 3; + } + + function initializeDistributionRecord( + uint256 index, // the beneficiary's index in the merkle root + address beneficiary, // the address that will receive tokens + uint256 amount, // the total claimable by this beneficiary + uint256 start, // the start time of the vesting + uint256 end, // the end time of the vesting + uint256 cliff, // the cliff time of the vesting + bytes32[] calldata merkleProof + ) external validMerkleProof(keccak256(abi.encodePacked(index, beneficiary, amount, start, end, cliff)), merkleProof) { + _initializeDistributionRecord(beneficiary, amount, start, end, cliff); + } + + function claim( + uint256 index, // the beneficiary's index in the merkle root + address beneficiary, // the address that will receive tokens + uint256 totalAmount, // the total claimable by this beneficiary + bytes32[] calldata merkleProof + ) + external + validMerkleProof(keccak256(abi.encodePacked(index, beneficiary, totalAmount)), merkleProof) + nonReentrant + { + // effects + uint256 claimedAmount = _executeClaim(beneficiary, totalAmount); + // interactions + _settleClaim(beneficiary, claimedAmount); + } + + function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner { + _setMerkleRoot(_merkleRoot); + } +} diff --git a/packages/hardhat/contracts/interfaces/IPerAddressDistributor.sol b/packages/hardhat/contracts/interfaces/IPerAddressDistributor.sol new file mode 100644 index 00000000..845f1d1a --- /dev/null +++ b/packages/hardhat/contracts/interfaces/IPerAddressDistributor.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +/** + * @dev This struct tracks claim progress for a given beneficiary. + * Because many claims are stored in a merkle root, this struct is only valid once initialized. + * Users can no longer claim once their claimed quantity meets or exceeds their total quantity. + * Note that admins may update the merkle root or adjust the total quantity for a specific + * beneficiary after initialization! + */ +struct PerAddressDistributionRecord { + bool initialized; // has the claim record been initialized + uint120 total; // total token quantity claimable + uint120 claimed; // token quantity already claimed + uint256 start; + uint256 end; + uint256 cliff; +} + +interface IPerAddressDistributor { + event InitializeDistributor( + IERC20 indexed token, // the ERC20 token being distributed + uint256 total, // total distribution quantity across all beneficiaries + string uri, // a URI for additional information about the distribution + uint256 fractionDenominator // the denominator for vesting fractions represented as integers + ); + + // Fired once when a beneficiary's distribution record is set up + event InitializeDistributionRecord(address indexed beneficiary, uint256 total, uint256 start, uint256 end, uint256 cliff); + + // Fired every time a claim for a beneficiary occurs + event Claim(address indexed beneficiary, uint256 amount); + + /** + * @dev get the current distribution status for a particular user + * @param beneficiary the address of the beneficiary + */ + function getDistributionRecord( + address beneficiary + ) external view returns (PerAddressDistributionRecord memory); + + /** + * @dev get the amount of tokens currently claimable by a beneficiary + * @param beneficiary the address of the beneficiary + */ + function getClaimableAmount(address beneficiary) external view returns (uint256); + + /** + * @dev get the denominator for vesting fractions represented as integers + */ + function getFractionDenominator() external view returns (uint256); + + /** + * @dev get the ERC20 token being distributed + */ + function token() external view returns (IERC20); + + /** + * @dev get the total distribution quantity across all beneficiaries + */ + function total() external view returns (uint256); + + /** + * @dev get a URI for additional information about the distribution + */ + function uri() external view returns (string memory); + + /** + * @dev get a human-readable name for the distributor that describes basic functionality + * On-chain consumers should rely on registered ERC165 interface IDs or similar for more specificity + */ + function NAME() external view returns (string memory); + + /** + * @dev get a human-readable version for the distributor that describes basic functionality + * The version should update whenever functionality significantly changes + * On-chain consumers should rely on registered ERC165 interface IDs or similar for more specificity + */ + function VERSION() external view returns (uint256); +} diff --git a/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol b/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol new file mode 100644 index 00000000..40cbabc7 --- /dev/null +++ b/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +struct Tranche { + uint128 time; // block.timestamp upon which the tranche vests + uint128 vestedFraction; // fraction of tokens unlockable as basis points (e.g. 100% of vested tokens is the fraction denominator, defaulting to 10000) +} + +interface IPerAddressTrancheVesting { + event SetTranche(uint256 indexed index, uint128 time, uint128 VestedFraction); + + function getTranche(uint256 i) external view returns (Tranche memory); + + function getTranches() external view returns (Tranche[] memory); + + function setTranches(Tranche[] memory _tranches) external; +} From 31d395429e7f237a887633155d5978116bcbb55e Mon Sep 17 00:00:00 2001 From: Jack Dishman Date: Mon, 26 Feb 2024 11:48:22 -0500 Subject: [PATCH 3/7] feat: add generate tranches and revert tranchevesting sol --- .../claim/GeneratePeriodicTranches.sol | 50 +++++++++++++++++++ .../claim/abstract/ContinuousVesting.sol | 15 +++--- 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol diff --git a/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol b/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol new file mode 100644 index 00000000..419d5cdc --- /dev/null +++ b/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +contract TrancheVesting { + struct Tranche { + uint256 time; + uint256 vestedFraction; + uint256 date; + } + + function generatePeriodicTranches(uint256 startTime, uint256 cliffTime, uint256 endTime, string memory units) external pure returns (Tranche[] memory) { + uint256 totalTranches = getDifference(startTime, endTime, units); + + Tranche[] memory tranches = new Tranche[](totalTranches); + + uint256 currentIndex = 0; + + for (uint256 i = 0; i < totalTranches; i++) { + uint256 trancheStartTime = addToDateTime(startTime, i, units); + + if (trancheStartTime < cliffTime) { + continue; + } + + uint256 vestedFraction = ((i + 1) * (100 / totalTranches)) * 100; + + tranches[currentIndex] = Tranche({ + time: trancheStartTime, + vestedFraction: vestedFraction, + date: trancheStartTime + }); + + currentIndex++; + } + + return tranches; + } + + // Function to calculate the difference between two timestamps + function getDifference(uint256 startTime, uint256 endTime, string memory units) internal pure returns (uint256) { + // Implementation of getDifference based on units + // You need to implement this function according to your requirements + } + + // Function to add time to a given timestamp + function addToDateTime(uint256 timestamp, uint256 amount, string memory units) internal pure returns (uint256) { + // Implementation of addToDateTime based on units + // You need to implement this function according to your requirements + } +} diff --git a/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol b/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol index 2f9a9b63..69698c21 100644 --- a/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol +++ b/packages/hardhat/contracts/claim/abstract/ContinuousVesting.sol @@ -24,17 +24,17 @@ abstract contract ContinuousVesting is AdvancedDistributor, IContinuousVesting { // use a large fraction denominator to provide the highest resolution on continuous vesting. AdvancedDistributor(_token, _total, _uri, _voteFactor, 10**18, _maxDelayTime, _salt) { - // require(_start <= _cliff, "vesting cliff before start"); - // require(_cliff <= _end, "vesting end before cliff"); - // require(_end <= 4102444800, "vesting ends after 4102444800 (Jan 1 2100)"); + require(_start <= _cliff, "vesting cliff before start"); + require(_cliff <= _end, "vesting end before cliff"); + require(_end <= 4102444800, "vesting ends after 4102444800 (Jan 1 2100)"); - // start = _start; - // cliff = _cliff; - // end = _end; + start = _start; + cliff = _cliff; + end = _end; // No longer need to initialize start, end, and cliff // emit SetContinuousVesting(start, cliff, end); - emit SetContinuousVesting(); + emit SetContinuousVesting(start, cliff, end); } function getVestedFraction( @@ -66,7 +66,6 @@ abstract contract ContinuousVesting is AdvancedDistributor, IContinuousVesting { uint256 ) { - // We probably need to update this so that it takes in an address return (start, cliff, end); } From 200c1037d3f41cb3d064766393d9bbb934ac60fe Mon Sep 17 00:00:00 2001 From: Jack Dishman Date: Mon, 26 Feb 2024 15:16:55 -0500 Subject: [PATCH 4/7] generate periodic tranches --- .../claim/GeneratePeriodicTranches.sol | 64 +++++++++---------- .../abstract/PerAddressTrancheVesting.sol | 56 +--------------- .../PerAddressTrancheVestingInitializable.sol | 54 +--------------- 3 files changed, 38 insertions(+), 136 deletions(-) diff --git a/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol b/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol index 419d5cdc..296a29a3 100644 --- a/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol +++ b/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol @@ -1,50 +1,50 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.21; +pragma solidity ^0.8.0; contract TrancheVesting { + struct Tranche { - uint256 time; - uint256 vestedFraction; - uint256 date; + uint128 time; // Timestamp upon which the tranche vests + uint128 vestedFraction; // Fraction of tokens unlockable as basis points } - function generatePeriodicTranches(uint256 startTime, uint256 cliffTime, uint256 endTime, string memory units) external pure returns (Tranche[] memory) { - uint256 totalTranches = getDifference(startTime, endTime, units); + // 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"); - Tranche[] memory tranches = new Tranche[](totalTranches); + uint256 vestingStart = cliff > start ? cliff : start; + uint256 vestingDuration = end - vestingStart; - uint256 currentIndex = 0; + // 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; - for (uint256 i = 0; i < totalTranches; i++) { - uint256 trancheStartTime = addToDateTime(startTime, i, units); + // Ensure there is at least one period + if (periods == 0) { + periods = 1; + } - if (trancheStartTime < cliffTime) { - continue; - } + Tranche[] memory tranches = new Tranche[](periods); + uint256 periodDuration = vestingDuration / periods; + uint256 totalVestedFraction = 0; - uint256 vestedFraction = ((i + 1) * (100 / totalTranches)) * 100; + 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; - tranches[currentIndex] = Tranche({ - time: trancheStartTime, - vestedFraction: vestedFraction, - date: trancheStartTime - }); + // Adjust the last tranche to ensure total vested fraction equals 10000 + if (i == periods - 1 && totalVestedFraction != 10000) { + vestedFraction += (10000 - uint128(totalVestedFraction)); + } - currentIndex++; + tranches[i] = Tranche(uint128(trancheTime), vestedFraction); } return tranches; } - - // Function to calculate the difference between two timestamps - function getDifference(uint256 startTime, uint256 endTime, string memory units) internal pure returns (uint256) { - // Implementation of getDifference based on units - // You need to implement this function according to your requirements - } - - // Function to add time to a given timestamp - function addToDateTime(uint256 timestamp, uint256 amount, string memory units) internal pure returns (uint256) { - // Implementation of addToDateTime based on units - // You need to implement this function according to your requirements - } } diff --git a/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol b/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol index bda64d1b..7527dc19 100644 --- a/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol +++ b/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol @@ -4,13 +4,14 @@ pragma solidity 0.8.21; import { PerAddressAdvancedDistributor } from './PerAddressAdvancedDistributor.sol'; import { IPerAddressTrancheVesting, Tranche } from '../../interfaces/IPerAddressTrancheVesting.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { TrancheVesting } from '../GeneratePeriodicTranches.sol'; import "hardhat/console.sol"; /** * @title TrancheVesting * @notice Distributes funds to beneficiaries over time in tranches. */ -abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPerAddressTrancheVesting { +abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPerAddressTrancheVesting, TrancheVesting { // time and vested fraction must monotonically increase in the tranche array constructor( @@ -41,6 +42,7 @@ abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPe // calculateTranche(beneficiary, record); // TODO: calculate tranches with start, end, cliff + Tranche[] memory tranches = generateTranches(start, end, cliff); uint256 delay = getFairDelayTime(beneficiary); for (uint256 i = tranches.length; i != 0; ) { @@ -54,57 +56,5 @@ abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPe } return 0; - } - - // Get a single tranche - function getTranche(uint256 i) public view returns (Tranche memory) { - return tranches[i]; - } - - // Get all tranches - function getTranches() public view returns (Tranche[] memory) { - return tranches; - } - - /** - * @dev Tranches can be updated at any time. The quantity of tokens a user can claim is based on the - * claimable quantity at the time of the claim and the quantity of tokens already claimed by the user. - */ - function _setTranches(Tranche[] memory _tranches) private { - require(_tranches.length != 0, 'tranches required'); - - delete tranches; - - uint128 lastTime = 0; - uint128 lastVestedFraction = 0; - - for (uint256 i = 0; i < _tranches.length; ) { - require(_tranches[i].vestedFraction != 0, 'tranche vested fraction == 0'); - require(_tranches[i].time > lastTime, 'tranche time must increase'); - require( - _tranches[i].vestedFraction > lastVestedFraction, - 'tranche vested fraction must increase' - ); - lastTime = _tranches[i].time; - lastVestedFraction = _tranches[i].vestedFraction; - tranches.push(_tranches[i]); - - emit SetTranche(i, lastTime, lastVestedFraction); - - unchecked { - ++i; - } - } - - require(lastTime <= 4102444800, 'vesting ends after 4102444800 (Jan 1 2100)'); - require(lastVestedFraction == fractionDenominator, 'last tranche must vest all tokens'); - } - - /** - * @notice Set the vesting tranches. Tranches must be sorted by time and vested fraction must monotonically increase. - * The last tranche must vest all tokens (vestedFraction == fractionDenominator) - */ - function setTranches(Tranche[] memory _tranches) external onlyOwner { - _setTranches(_tranches); } } diff --git a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol index 6a129610..ad3435c6 100644 --- a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol +++ b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol @@ -6,8 +6,9 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./PerAddressAdvancedDistributorInitializable.sol"; import {IPerAddressTrancheVesting, Tranche} from "../../interfaces/IPerAddressTrancheVesting.sol"; +import { TrancheVesting } from '../GeneratePeriodicTranches.sol'; -abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAddressAdvancedDistributorInitializable, IPerAddressTrancheVesting { +abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAddressAdvancedDistributorInitializable, IPerAddressTrancheVesting, TrancheVesting { function __TrancheVesting_init( IERC20 _token, @@ -38,7 +39,7 @@ abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAdd function getVestedFraction(address beneficiary, uint256 time, uint256 start, uint256 end, uint256 cliff) public view override returns (uint256) { // tranches = calculate the the tranche from start, end, cliff from record, record.start, record.end, record.cliff, - Tranche[] tranches = []; + Tranche[] memory tranches = generateTranches(start, end, cliff); uint256 delay = getFairDelayTime(beneficiary); for (uint256 i = tranches.length; i != 0;) { unchecked { @@ -52,53 +53,4 @@ abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAdd return 0; } - - // Get a single tranche - function getTranche(uint256 i) public view returns (Tranche memory) { - // return tranches[i]; - } - - // TODO: get tranche for address - function getTranches() public view returns (Tranche[] memory) { - // return tranches; - } - - /** - * @dev Tranches can be updated at any time. The quantity of tokens a user can claim is based on the - * claimable quantity at the time of the claim and the quantity of tokens already claimed by the user. - */ - function _setTranches(Tranche[] memory _tranches) private { - require(_tranches.length != 0, "tranches required"); - - // delete tranches; - - uint128 lastTime = 0; - uint128 lastVestedFraction = 0; - - for (uint256 i = 0; i < _tranches.length;) { - require(_tranches[i].vestedFraction != 0, "tranche vested fraction == 0"); - require(_tranches[i].time > lastTime, "tranche time must increase"); - require(_tranches[i].vestedFraction > lastVestedFraction, "tranche vested fraction must increase"); - lastTime = _tranches[i].time; - lastVestedFraction = _tranches[i].vestedFraction; - tranches.push(_tranches[i]); - - emit SetTranche(i, lastTime, lastVestedFraction); - - unchecked { - ++i; - } - } - - require(lastTime <= 4102444800, "vesting ends after 4102444800 (Jan 1 2100)"); - require(lastVestedFraction == fractionDenominator, "last tranche must vest all tokens"); - } - - /** - * @notice Set the vesting tranches. Tranches must be sorted by time and vested fraction must monotonically increase. - * The last tranche must vest all tokens (vestedFraction == fractionDenominator) - */ - function setTranches(Tranche[] memory _tranches) external onlyOwner { - _setTranches(_tranches); - } } From 8c03c4e4380f905de1dc7e573ab0b98907a8f94d Mon Sep 17 00:00:00 2001 From: Jack Dishman Date: Tue, 27 Feb 2024 10:54:02 -0500 Subject: [PATCH 5/7] fix: rename PeriodicTranches and added params --- ...GeneratePeriodicTranches.sol => PeriodicTranches.sol} | 4 ++-- .../contracts/claim/abstract/PerAddressDistributor.sol | 9 ++------- .../claim/abstract/PerAddressTrancheVesting.sol | 4 ++-- .../PerAddressAdvancedDistributorInitializable.sol | 4 ++-- .../factory/PerAddressTrancheVestingInitializable.sol | 4 ++-- 5 files changed, 10 insertions(+), 15 deletions(-) rename packages/hardhat/contracts/claim/{GeneratePeriodicTranches.sol => PeriodicTranches.sol} (97%) diff --git a/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol b/packages/hardhat/contracts/claim/PeriodicTranches.sol similarity index 97% rename from packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol rename to packages/hardhat/contracts/claim/PeriodicTranches.sol index 296a29a3..7a9cd417 100644 --- a/packages/hardhat/contracts/claim/GeneratePeriodicTranches.sol +++ b/packages/hardhat/contracts/claim/PeriodicTranches.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.21; -contract TrancheVesting { +contract PeriodicTranches { struct Tranche { uint128 time; // Timestamp upon which the tranche vests diff --git a/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol b/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol index bfe5bf58..4e382d42 100644 --- a/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol +++ b/packages/hardhat/contracts/claim/abstract/PerAddressDistributor.sol @@ -76,18 +76,13 @@ abstract contract PerAddressDistributor is IPerAddressDistributor, ReentrancyGua uint120 totalAmount = uint120(_totalAmount); // effects - if (records[beneficiary].total != totalAmount) { - // re-initialize if the total has been updated + if (records[beneficiary].total != totalAmount || records[beneficiary].start != _start || records[beneficiary].end != _end || records[beneficiary].cliff != _cliff) { _initializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); } - if(records[beneficiary].start != _start || records[beneficiary].end != _end || records[beneficiary].cliff != _cliff) { - _initializeDistributionRecord(beneficiary, totalAmount, _start, _end, _cliff); - } - + uint120 claimableAmount = uint120(getClaimableAmount(beneficiary)); require(claimableAmount > 0, 'Distributor: no more tokens claimable right now'); - records[beneficiary].claimed += claimableAmount; claimed += claimableAmount; diff --git a/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol b/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol index 7527dc19..e9031872 100644 --- a/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol +++ b/packages/hardhat/contracts/claim/abstract/PerAddressTrancheVesting.sol @@ -4,14 +4,14 @@ pragma solidity 0.8.21; import { PerAddressAdvancedDistributor } from './PerAddressAdvancedDistributor.sol'; import { IPerAddressTrancheVesting, Tranche } from '../../interfaces/IPerAddressTrancheVesting.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import { TrancheVesting } from '../GeneratePeriodicTranches.sol'; +import { PeriodicTranches } from '../PeriodicTranches.sol'; import "hardhat/console.sol"; /** * @title TrancheVesting * @notice Distributes funds to beneficiaries over time in tranches. */ -abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPerAddressTrancheVesting, TrancheVesting { +abstract contract PerAddressTrancheVesting is PerAddressAdvancedDistributor, IPerAddressTrancheVesting, PeriodicTranches { // time and vested fraction must monotonically increase in the tranche array constructor( diff --git a/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol b/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol index 290c8c10..e173d4a1 100644 --- a/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol +++ b/packages/hardhat/contracts/claim/factory/PerAddressAdvancedDistributorInitializable.sol @@ -103,13 +103,13 @@ abstract contract PerAddressAdvancedDistributorInitializable is _reconcileVotingPower(beneficiary); } - function _executeClaim(address beneficiary, uint256 totalAmount) + function _executeClaim(address beneficiary, uint256 totalAmount, uint256 start, uint256 end, uint256 cliff) internal virtual override returns (uint256 _claimed) { - _claimed = super._executeClaim(beneficiary, totalAmount); + _claimed = super._executeClaim(beneficiary, totalAmount, start, end, cliff); _reconcileVotingPower(beneficiary); } diff --git a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol index ad3435c6..7523481f 100644 --- a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol +++ b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingInitializable.sol @@ -6,9 +6,9 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./PerAddressAdvancedDistributorInitializable.sol"; import {IPerAddressTrancheVesting, Tranche} from "../../interfaces/IPerAddressTrancheVesting.sol"; -import { TrancheVesting } from '../GeneratePeriodicTranches.sol'; +import { PeriodicTranches } from '../PeriodicTranches.sol'; -abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAddressAdvancedDistributorInitializable, IPerAddressTrancheVesting, TrancheVesting { +abstract contract PerAddressTrancheVestingInitializable is Initializable, PerAddressAdvancedDistributorInitializable, IPerAddressTrancheVesting, PeriodicTranches { function __TrancheVesting_init( IERC20 _token, From f7d09ef078b9fc76371d5fae084cd8db8663a0f8 Mon Sep 17 00:00:00 2001 From: Jack Dishman Date: Tue, 27 Feb 2024 11:28:51 -0500 Subject: [PATCH 6/7] feat: add factory --- ...AddressTrancheVestingMerkleDistributor.sol | 7 +- ...TrancheVestingMerkleDistributorFactory.sol | 95 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol diff --git a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol index 3ef1a47c..6b8ccf03 100644 --- a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol +++ b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributor.sol @@ -55,14 +55,17 @@ contract PerAddressTrancheVestingMerkleDistributor is uint256 index, // the beneficiary's index in the merkle root address beneficiary, // the address that will receive tokens uint256 totalAmount, // the total claimable by this beneficiary - bytes32[] calldata merkleProof + bytes32[] calldata merkleProof, + uint256 start, // the start time of the vesting + uint256 end, // the end time of the vesting + uint256 cliff // the cliff time of the vesting ) external validMerkleProof(keccak256(abi.encodePacked(index, beneficiary, totalAmount)), merkleProof) nonReentrant { // effects - uint256 claimedAmount = _executeClaim(beneficiary, totalAmount); + uint256 claimedAmount = _executeClaim(beneficiary, totalAmount, start, end, cliff); // interactions _settleClaim(beneficiary, claimedAmount); } diff --git a/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol new file mode 100644 index 00000000..ef89ab2b --- /dev/null +++ b/packages/hardhat/contracts/claim/factory/PerAddressTrancheVestingMerkleDistributorFactory.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; + +import {PerAddressTrancheVestingMerkleDistributor, Tranche} from "./PerAddressTrancheVestingMerkleDistributor.sol"; + +contract PerAddressTrancheVestingMerkleDistributorFactory { + address private immutable i_implementation; + address[] public distributors; + + event DistributorDeployed(address indexed distributor); + + constructor(address implementation) { + i_implementation = implementation; + } + + function _getSalt( + IERC20 _token, // the token being claimed + uint256 _total, // the total claimable by all users + string memory _uri, // information on the sale (e.g. merkle proofs) + bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time), + uint160 _maxDelayTime, // the maximum delay time for the fair queue + address _owner, + uint256 _nonce + ) private pure returns (bytes32) { + return keccak256(abi.encode( + _token, + _total, + _uri, + _merkleRoot, + _maxDelayTime, + _owner, + _nonce + )); + } + + function deployDistributor( + IERC20 _token, // the token being claimed + uint256 _total, // the total claimable by all users + string memory _uri, // information on the sale (e.g. merkle proofs) + bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time), + uint160 _maxDelayTime, // the maximum delay time for the fair queue + address _owner, + uint256 _nonce + ) public returns (PerAddressTrancheVestingMerkleDistributor distributor) { + bytes32 salt = _getSalt( + _token, + _total, + _uri, + _merkleRoot, + _maxDelayTime, + _owner, + _nonce + ); + + distributor = + PerAddressTrancheVestingMerkleDistributor(Clones.cloneDeterministic(i_implementation, salt)); + distributors.push(address(distributor)); + + emit DistributorDeployed(address(distributor)); + + distributor.initialize(_token, _total, _uri, _merkleRoot, _maxDelayTime, _owner); + + return distributor; + } + + function getImplementation() public view returns (address) { + return i_implementation; + } + + function predictDistributorAddress( + IERC20 _token, // the token being claimed + uint256 _total, // the total claimable by all users + string memory _uri, // information on the sale (e.g. merkle proofs) + bytes32 _merkleRoot, // the merkle root for claim membership (also used as salt for the fair queue delay time), + uint160 _maxDelayTime, // the maximum delay time for the fair queue + address _owner, + uint256 _nonce + ) public view returns (address) { + bytes32 salt = _getSalt( + _token, + _total, + _uri, + _merkleRoot, + _maxDelayTime, + _owner, + _nonce + ); + + return Clones.predictDeterministicAddress(i_implementation, salt, address(this)); + } +} From 4b0b40b513eb7f76d29a230a5078632a8fd0ecfe Mon Sep 17 00:00:00 2001 From: Jack Dishman Date: Tue, 27 Feb 2024 12:33:53 -0500 Subject: [PATCH 7/7] fix: remoe unused interface --- .../contracts/interfaces/IPerAddressTrancheVesting.sol | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol b/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol index 40cbabc7..37b2b10d 100644 --- a/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol +++ b/packages/hardhat/contracts/interfaces/IPerAddressTrancheVesting.sol @@ -8,12 +8,4 @@ struct Tranche { uint128 vestedFraction; // fraction of tokens unlockable as basis points (e.g. 100% of vested tokens is the fraction denominator, defaulting to 10000) } -interface IPerAddressTrancheVesting { - event SetTranche(uint256 indexed index, uint128 time, uint128 VestedFraction); - - function getTranche(uint256 i) external view returns (Tranche memory); - - function getTranches() external view returns (Tranche[] memory); - - function setTranches(Tranche[] memory _tranches) external; -} +interface IPerAddressTrancheVesting {}