From da66781bc8e15f5e7c3ee3745d5c90be61ca0733 Mon Sep 17 00:00:00 2001 From: detoo Date: Thu, 23 Oct 2025 11:08:18 -0700 Subject: [PATCH 01/25] wip: feat: re-enable other options and fix contract sizes --- src/MetaVesTController.sol | 219 ++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 125 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 6c83a09..8b7b37b 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -39,8 +39,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address public dao; address public registry; address public vestingFactory; -// address public tokenOptionFactory; -// address public restrictedTokenFactory; + address public tokenOptionFactory; + address public restrictedTokenFactory; address internal _pendingAuthority; address internal _pendingDao; @@ -68,6 +68,10 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes32 agreementId; metavestType _metavestType; address grantee; + address paymentToken; + uint256 exercisePrice; + uint256 shortStopDuration; + uint256 longStopDate; BaseAllocation.Allocation allocation; BaseAllocation.Milestone[] milestones; address metavest; @@ -321,6 +325,10 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { agreementId: agreementId, _metavestType: _metavestType, grantee: grantee, + paymentToken: address(0), // TODO WIP + exercisePrice: 0, // TODO WIP + shortStopDuration: 0, // TODO WIP + longStopDate: 0, // TODO WIP allocation: allocation, milestones: milestones, metavest: address(0) // Not deployed yet @@ -390,17 +398,15 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if(deal._metavestType == metavestType.Vesting) { - deal.metavest = createVestingAllocation(deal.grantee, recipient, deal.allocation, deal.milestones); + deal.metavest = createVestingAllocation(deal, recipient); } else if(deal._metavestType == metavestType.TokenOption) { - // TODO will be supported in the next stage - revert MetaVesTController_TypeNotSupported(deal._metavestType); + deal.metavest = createTokenOptionAllocation(deal, recipient); } else if(deal._metavestType == metavestType.RestrictedTokenAward) { - // TODO will be supported in the next stage - revert MetaVesTController_TypeNotSupported(deal._metavestType); + deal.metavest = createRestrictedTokenAward(deal, recipient); } else { @@ -413,13 +419,11 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function validateInputParameters( - address _grantee, - address _recipient, - address _paymentToken, - uint256 _exercisePrice, - VestingAllocation.Allocation memory _allocation - ) internal pure { - if (_grantee == address(0) || _recipient == address(0) || _allocation.tokenContract == address(0) || _paymentToken == address(0) || _exercisePrice == 0) + DealData storage deal, + address recipient + ) internal view { + // TODO must differentiate metavest types + if (deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) || deal.paymentToken == address(0) || deal.exercisePrice == 0) revert MetaVesTController_ZeroAddress(); } @@ -451,117 +455,81 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) revert MetaVesTController_AmountNotApprovedForTransferFrom(); } -// function createAndInitializeTokenOptionAllocation( -// address _grantee, -// address _paymentToken, -// uint256 _exercisePrice, -// uint256 _shortStopDuration, -// VestingAllocation.Allocation calldata _allocation, -// VestingAllocation.Milestone[] calldata _milestones -// ) internal returns (address) { -// return IAllocationFactory(tokenOptionFactory).createAllocation( -// IAllocationFactory.AllocationType.TokenOption, -// _grantee, -// address(this), -// _allocation, -// _milestones, -// _paymentToken, -// _exercisePrice, -// _shortStopDuration -// ); -// } -// -// function createAndInitializeRestrictedTokenAward( -// address _grantee, -// address _paymentToken, -// uint256 _repurchasePrice, -// uint256 _shortStopDuration, -// VestingAllocation.Allocation calldata _allocation, -// VestingAllocation.Milestone[] calldata _milestones -// ) internal returns (address) { -// return IAllocationFactory(restrictedTokenFactory).createAllocation( -// IAllocationFactory.AllocationType.RestrictedToken, -// _grantee, -// address(this), -// _allocation, -// _milestones, -// _paymentToken, -// _repurchasePrice, -// _shortStopDuration -// ); -// } - - - function createVestingAllocation(address _grantee, address _recipient, VestingAllocation.Allocation memory _allocation, VestingAllocation.Milestone[] memory _milestones) internal returns (address){ - //hard code values not to trigger the failure for the 2 parameters that don't matter for this type of allocation - validateInputParameters(_grantee, _recipient, address(this), 1, _allocation); - validateAllocation(_allocation); - uint256 _milestoneTotal = validateAndCalculateMilestones(_milestones); - - uint256 _total = _allocation.tokenStreamTotal + _milestoneTotal; + // TODO why doesn't it need conditionCheck? + function createVestingAllocation(DealData storage deal, address recipient) internal returns (address){ + validateInputParameters(deal, recipient); + validateAllocation(deal.allocation); + uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); + + uint256 _total = deal.allocation.tokenStreamTotal + _milestoneTotal; if (_total == 0) revert MetaVesTController_ZeroAmount(); - validateTokenApprovalAndBalance(_allocation.tokenContract, _total); + validateTokenApprovalAndBalance(deal.allocation.tokenContract, _total); address vestingAllocation = IAllocationFactory(vestingFactory).createAllocation( IAllocationFactory.AllocationType.Vesting, - _grantee, - _recipient, + deal.grantee, + recipient, address(this), - _allocation, - _milestones, + deal.allocation, + deal.milestones, address(0), 0, 0 ); - safeTransferFrom(_allocation.tokenContract, authority, vestingAllocation, _total); + safeTransferFrom(deal.allocation.tokenContract, authority, vestingAllocation, _total); return vestingAllocation; } -// function createTokenOptionAllocation(address _grantee, uint256 _exercisePrice, address _paymentToken, uint256 _shortStopDuration, VestingAllocation.Allocation calldata _allocation, VestingAllocation.Milestone[] calldata _milestones) internal conditionCheck returns (address) { -// -// validateInputParameters(_grantee, _paymentToken, _exercisePrice, _allocation); -// validateAllocation(_allocation); -// uint256 _milestoneTotal = validateAndCalculateMilestones(_milestones); -// -// uint256 _total = _allocation.tokenStreamTotal + _milestoneTotal; -// if (_total == 0) revert MetaVesTController_ZeroAmount(); -// validateTokenApprovalAndBalance(_allocation.tokenContract, _total); -// -// address tokenOptionAllocation = createAndInitializeTokenOptionAllocation( -// _grantee, -// _paymentToken, -// _exercisePrice, -// _shortStopDuration, -// _allocation, -// _milestones -// ); -// -// safeTransferFrom(_allocation.tokenContract, authority, tokenOptionAllocation, _total); -// return tokenOptionAllocation; -// } -// -// function createRestrictedTokenAward(address _grantee, uint256 _repurchasePrice, address _paymentToken, uint256 _shortStopDuration, VestingAllocation.Allocation calldata _allocation, VestingAllocation.Milestone[] calldata _milestones) internal conditionCheck returns (address){ -// validateInputParameters(_grantee, _paymentToken, _repurchasePrice, _allocation); -// validateAllocation(_allocation); -// uint256 _milestoneTotal = validateAndCalculateMilestones(_milestones); -// -// uint256 _total = _allocation.tokenStreamTotal + _milestoneTotal; -// if (_total == 0) revert MetaVesTController_ZeroAmount(); -// validateTokenApprovalAndBalance(_allocation.tokenContract, _total); -// -// address restrictedTokenAward = createAndInitializeRestrictedTokenAward( -// _grantee, -// _paymentToken, -// _repurchasePrice, -// _shortStopDuration, -// _allocation, -// _milestones -// ); -// -// safeTransferFrom(_allocation.tokenContract, authority, restrictedTokenAward, _total); -// return restrictedTokenAward; -// } + function createTokenOptionAllocation(DealData storage deal, address recipient) internal conditionCheck returns (address) { + validateInputParameters(deal, recipient); + validateAllocation(deal.allocation); + uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); + + uint256 _total = deal.allocation.tokenStreamTotal + _milestoneTotal; + if (_total == 0) revert MetaVesTController_ZeroAmount(); + validateTokenApprovalAndBalance(deal.allocation.tokenContract, _total); + + address tokenOptionAllocation = IAllocationFactory(tokenOptionFactory).createAllocation( + IAllocationFactory.AllocationType.TokenOption, + deal.grantee, + recipient, + address(this), + deal.allocation, + deal.milestones, + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration + ); + + safeTransferFrom(deal.allocation.tokenContract, authority, tokenOptionAllocation, _total); + return tokenOptionAllocation; + } + + function createRestrictedTokenAward(DealData storage deal, address recipient) internal conditionCheck returns (address){ + validateInputParameters(deal, recipient); + validateAllocation(deal.allocation); + uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); + + uint256 _total = deal.allocation.tokenStreamTotal + _milestoneTotal; + if (_total == 0) revert MetaVesTController_ZeroAmount(); + validateTokenApprovalAndBalance(deal.allocation.tokenContract, _total); + + address restrictedTokenAward = IAllocationFactory(restrictedTokenFactory).createAllocation( + IAllocationFactory.AllocationType.RestrictedToken, + deal.grantee, + recipient, + address(this), + deal.allocation, + deal.milestones, + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration + ); + + safeTransferFrom(deal.allocation.tokenContract, authority, restrictedTokenAward, _total); + return restrictedTokenAward; + } function getMetaVestType(address _grant) public view returns (uint256) { return BaseAllocation(_grant).getVestingType(); @@ -587,19 +555,20 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { BaseAllocation(_grant).updateTransferability(_isTransferable); } -// /// @notice for the controller to update either exercisePrice or repurchasePrice for a '_grantee' and their transferees, as applicable depending on the '_grantee''s MetaVesTType -// /// @param _grant address of grantee whose applicable price is being updated -// /// @param _newPrice new exercisePrice (if token option) or (repurchase price if restricted token award) as 'paymentToken' per 1 metavested token in vesting token decimals but only up to payment decimal precision -// function updateExerciseOrRepurchasePrice( -// address _grant, -// uint256 _newPrice -// ) external onlyAuthority conditionCheck consentCheck(_grant, msg.data) { -// if (_newPrice == 0) revert MetaVesTController_ZeroPrice(); -// IPriceAllocation grant = IPriceAllocation(_grant); -// if(grant.getVestingType()!=2 && grant.getVestingType()!=3) revert MetaVesTController_IncorrectMetaVesTType(); -// _resetAmendmentParams(_grant, msg.sig); -// grant.updatePrice(_newPrice); -// } + // TODO review needed + /// @notice for the controller to update either exercisePrice or repurchasePrice for a '_grantee' and their transferees, as applicable depending on the '_grantee''s MetaVesTType + /// @param _grant address of grantee whose applicable price is being updated + /// @param _newPrice new exercisePrice (if token option) or (repurchase price if restricted token award) as 'paymentToken' per 1 metavested token in vesting token decimals but only up to payment decimal precision + function updateExerciseOrRepurchasePrice( + address _grant, + uint256 _newPrice + ) external onlyAuthority conditionCheck consentCheck(_grant, msg.data) { + if (_newPrice == 0) revert MetaVesTController_ZeroPrice(); + IPriceAllocation grant = IPriceAllocation(_grant); + if(grant.getVestingType()!=2 && grant.getVestingType()!=3) revert MetaVesTController_IncorrectMetaVesTType(); + _resetAmendmentParams(_grant, msg.sig); + grant.updatePrice(_newPrice); + } /// @notice removes a milestone from '_grantee''s MetaVesT if such milestone has not yet been confirmed, also making the corresponding 'milestoneAward' tokens withdrawable by controller /// @param _grant address of grantee whose MetaVesT is being updated From 6d0cc97d99cf882d5decefe7cb02b64465340566 Mon Sep 17 00:00:00 2001 From: detoo Date: Thu, 23 Oct 2025 11:48:18 -0700 Subject: [PATCH 02/25] wip: feat: create MetaVestDeal library --- scripts/proposeMetavestDeal.s.sol | 11 ++- src/MetaVesTController.sol | 97 +++++++------------------ src/lib/MetaVestDealLib.sol | 81 +++++++++++++++++++++ test/amendement.t.sol | 4 +- test/controller.t.sol | 10 +-- test/lib/MetaVesTControllerTestBase.sol | 14 ++-- 6 files changed, 130 insertions(+), 87 deletions(-) create mode 100644 src/lib/MetaVestDealLib.sol diff --git a/scripts/proposeMetavestDeal.s.sol b/scripts/proposeMetavestDeal.s.sol index 7ab3e0d..92fa448 100644 --- a/scripts/proposeMetavestDeal.s.sol +++ b/scripts/proposeMetavestDeal.s.sol @@ -13,8 +13,10 @@ import {Script} from "forge-std/Script.sol"; import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; +import {MetaVestDealLib, MetaVestDeal} from "../src/lib/MetaVestDealLib.sol"; contract ProposeMetaVestDealScript is SafeTxHelper, Script { + using MetaVestDealLib for MetaVestDeal; using YearnBorgCompensation2025_2026 for YearnBorgCompensation2025_2026.Config; /// @dev For running from `forge script`. Provide the deployer private key through env var. @@ -104,10 +106,11 @@ contract ProposeMetaVestDealScript is SafeTxHelper, Script { bytes32 contractId = config.controller.proposeAndSignDeal( guardianInfo.compTemplate.id, agreementSalt, - metavestController.metavestType.Vesting, - guardianInfo.partyInfo.evmAddress, - allocation, - config.milestones, + MetaVestDealLib.draft().setVesting( + guardianInfo.partyInfo.evmAddress, + allocation, + config.milestones + ), globalValues, parties, partyValues, diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 8b7b37b..af440eb 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -10,6 +10,7 @@ pragma solidity ^0.8.24; import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "./lib/MetaVestDealLib.sol"; import "./BaseAllocation.sol"; //import "./RestrictedTokenAllocation.sol"; import "./interfaces/IAllocationFactory.sol"; @@ -63,22 +64,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { mapping(address => uint256) appliedProposalCreatedAt; mapping(address => uint256) voterPower; } - - struct DealData { - bytes32 agreementId; - metavestType _metavestType; - address grantee; - address paymentToken; - uint256 exercisePrice; - uint256 shortStopDuration; - uint256 longStopDate; - BaseAllocation.Allocation allocation; - BaseAllocation.Milestone[] milestones; - address metavest; - } - - enum metavestType { Vesting, TokenOption, RestrictedTokenAward } - + /// @notice maps a function's signature to a Condition contract address mapping(bytes4 => address[]) public functionToConditions; @@ -94,7 +80,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { mapping(bytes32 => bool) public setMajorityVoteActive; /// @notice granteeId => granteeData - mapping(bytes32 => DealData) public deals; + mapping(bytes32 => MetaVestDeal) public deals; /// @notice Maps agreement IDs to arrays of counter party values for closed deals. mapping(bytes32 => string[]) public counterPartyValues; @@ -120,7 +106,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { event MetaVesTController_DealProposed( bytes32 indexed agreementId, address indexed grantee, - metavestType metavestType, + MetaVestType MetaVestType, BaseAllocation.Allocation allocation, BaseAllocation.Milestone[] milestones, bool hasSecret, @@ -166,7 +152,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { error MetaVestController_MetaVestNotInSet(); error MetaVesTController_SetAlreadyExists(); error MetaVesTController_StringTooLong(); - error MetaVesTController_TypeNotSupported(metavestType _type); + error MetaVesTController_TypeNotSupported(MetaVestType _type); error MetaVesTController_DealAlreadyFinalized(); error MetaVesTController_DealVoided(); error MetaVesTController_CounterPartyNotFound(); @@ -285,10 +271,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function proposeAndSignDeal( bytes32 templateId, uint256 salt, - metavestType _metavestType, - address grantee, - BaseAllocation.Allocation memory allocation, - BaseAllocation.Milestone[] memory milestones, + MetaVestDeal memory dealDraft, string[] memory globalValues, address[] memory parties, string[][] memory partyValues, @@ -297,73 +280,45 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { uint256 expiry ) external returns (bytes32) { + // TODO validate parties against deal + // Call internal function to avoid stack-too-deep errors - bytes32 agreementId = _createAgreement( + dealDraft.agreementId = ICyberAgreementRegistry(registry).createContract( templateId, salt, globalValues, parties, partyValues, secretHash, + address(this), expiry ); if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); - counterPartyValues[agreementId] = partyValues[1]; + counterPartyValues[dealDraft.agreementId] = partyValues[1]; ICyberAgreementRegistry(registry).signContractFor( authority, // First party (grantor) should always be the authority - agreementId, + dealDraft.agreementId, partyValues[0], signature, false, // Not meant for anyone else other than the signer "" // Signer == proposer, no secret needed ); - deals[agreementId] = DealData({ - agreementId: agreementId, - _metavestType: _metavestType, - grantee: grantee, - paymentToken: address(0), // TODO WIP - exercisePrice: 0, // TODO WIP - shortStopDuration: 0, // TODO WIP - longStopDate: 0, // TODO WIP - allocation: allocation, - milestones: milestones, - metavest: address(0) // Not deployed yet - }); - dealIds.push(agreementId); + deals[dealDraft.agreementId] = dealDraft; + dealIds.push(dealDraft.agreementId); emit MetaVesTController_DealProposed( - agreementId, grantee, _metavestType, allocation, milestones, + dealDraft.agreementId, dealDraft.grantee, dealDraft.metavestType, dealDraft.allocation, dealDraft.milestones, secretHash > 0, registry ); - return agreementId; - } - - function _createAgreement( - bytes32 templateId, - uint256 salt, - string[] memory globalValues, - address[] memory parties, - string[][] memory partyValues, - bytes32 secretHash, - uint256 expiry - ) internal returns (bytes32) { - return ICyberAgreementRegistry(registry).createContract( - templateId, - salt, - globalValues, - parties, - partyValues, - secretHash, - address(this), - expiry - ); + return dealDraft.agreementId; } + // TODO handle cases when agreement is signed externally function signDealAndCreateMetavest( address grantee, address recipient, @@ -394,17 +349,17 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } function _createMetavest(bytes32 agreementId, address recipient) internal returns (address) { - DealData storage deal = deals[agreementId]; + MetaVestDeal storage deal = deals[agreementId]; - if(deal._metavestType == metavestType.Vesting) + if(deal.metavestType == MetaVestType.Vesting) { deal.metavest = createVestingAllocation(deal, recipient); } - else if(deal._metavestType == metavestType.TokenOption) + else if(deal.metavestType == MetaVestType.TokenOption) { deal.metavest = createTokenOptionAllocation(deal, recipient); } - else if(deal._metavestType == metavestType.RestrictedTokenAward) + else if(deal.metavestType == MetaVestType.RestrictedTokenAward) { deal.metavest = createRestrictedTokenAward(deal, recipient); } @@ -419,7 +374,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function validateInputParameters( - DealData storage deal, + MetaVestDeal storage deal, address recipient ) internal view { // TODO must differentiate metavest types @@ -456,7 +411,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } // TODO why doesn't it need conditionCheck? - function createVestingAllocation(DealData storage deal, address recipient) internal returns (address){ + function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address){ validateInputParameters(deal, recipient); validateAllocation(deal.allocation); uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); @@ -481,7 +436,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { return vestingAllocation; } - function createTokenOptionAllocation(DealData storage deal, address recipient) internal conditionCheck returns (address) { + function createTokenOptionAllocation(MetaVestDeal storage deal, address recipient) internal conditionCheck returns (address) { validateInputParameters(deal, recipient); validateAllocation(deal.allocation); uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); @@ -506,7 +461,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { return tokenOptionAllocation; } - function createRestrictedTokenAward(DealData storage deal, address recipient) internal conditionCheck returns (address){ + function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal conditionCheck returns (address){ validateInputParameters(deal, recipient); validateAllocation(deal.allocation); uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); @@ -885,7 +840,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { emit MetaVesTController_AddressRemovedFromSet(_name, _metaVest); } - function getDeal(bytes32 agreementId) public view returns (DealData memory) { + function getDeal(bytes32 agreementId) public view returns (MetaVestDeal memory) { return deals[agreementId]; } diff --git a/src/lib/MetaVestDealLib.sol b/src/lib/MetaVestDealLib.sol new file mode 100644 index 0000000..4ce998b --- /dev/null +++ b/src/lib/MetaVestDealLib.sol @@ -0,0 +1,81 @@ +/* .o. + .888. + .8"888. + .8' `888. + .88ooo8888. + .8' `888. + o88o o8888o + + + + ooo ooooo . ooooo ooooooo ooooo + `88. .888' .o8 `888' `8888 d8' + 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P + 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' + 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. + 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b + o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o + + + + .oooooo. .o8 .oooooo. + d8P' `Y8b "888 d8P' `Y8b + 888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo. + 888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b + 888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 + `88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. + `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P + .o..P' 888 + `Y8P' o888o + _______________________________________________________________________________________________________ + + All software, documentation and other files and information in this repository (collectively, the "Software") + are copyright MetaLeX Labs, Inc., a Delaware corporation. + + All rights reserved. + + The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, + distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or + mechanical, including photocopying, recording, or by any information storage and retrieval system, + except with the express prior written permission of the copyright holder.*/ + +pragma solidity ^0.8.28; + +import {BaseAllocation} from "../BaseAllocation.sol"; + +enum MetaVestType { Vesting, TokenOption, RestrictedTokenAward } + +struct MetaVestDeal { + bytes32 agreementId; + MetaVestType metavestType; + address grantee; + address paymentToken; + uint256 exercisePrice; + uint256 shortStopDuration; + uint256 longStopDate; + BaseAllocation.Allocation allocation; + BaseAllocation.Milestone[] milestones; + address metavest; +} + +library MetaVestDealLib { + function draft() internal pure returns (MetaVestDeal memory) { + MetaVestDeal memory deal; // all default values + return deal; + } + + /// @notice Partially fill the given deal struct + /// @dev Beware of which fields are not filled and using default values + function setVesting( + MetaVestDeal memory deal, + address grantee, + BaseAllocation.Allocation memory allocation, + BaseAllocation.Milestone[] memory milestones + ) internal pure returns (MetaVestDeal memory) { + deal.metavestType = MetaVestType.Vesting; + deal.grantee = grantee; + deal.allocation = allocation; + deal.milestones = milestones; + return deal; + } +} diff --git a/test/amendement.t.sol b/test/amendement.t.sol index 48e7d19..e2fc5ca 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -423,7 +423,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // token.approve(address(controller), 1100e18); // // return controller.createMetavest( -// metavestController.metavestType.TokenOption, +// MetaVestType.TokenOption, // grantee, // allocation, // milestones, @@ -457,7 +457,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // token.approve(address(controller), 1100e18); // // return controller.createMetavest( -// metavestController.metavestType.RestrictedTokenAward, +// MetaVestType.RestrictedTokenAward, // grantee, // allocation, // milestones, diff --git a/test/controller.t.sol b/test/controller.t.sol index a63f233..95e6084 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -122,7 +122,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // }); // // address tokenOptionAllocation = controller.createMetavest( -// metavestController.metavestType.TokenOption, +// MetaVestType.TokenOption, // grantee, // allocation, // milestones, @@ -159,7 +159,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // token.approve(address(controller), 1100e18); // // address restrictedTokenAward = controller.createMetavest( -// metavestController.metavestType.RestrictedTokenAward, +// MetaVestType.RestrictedTokenAward, // grantee, // allocation, // milestones, @@ -754,7 +754,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // token.approve(address(controller), 2000e18); // // return controller.createMetavest( -// metavestController.metavestType.TokenOption, +// MetaVestType.TokenOption, // grantee, // allocation, // milestones, @@ -789,7 +789,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // token.approve(address(controller), 2100e18); // // return controller.createMetavest( -// metavestController.metavestType.RestrictedTokenAward, +// MetaVestType.RestrictedTokenAward, // grantee, // allocation, // milestones, @@ -824,7 +824,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // token.approve(address(controller), 2100e18); // // return controller.createMetavest( -// metavestController.metavestType.RestrictedTokenAward, +// MetaVestType.RestrictedTokenAward, // grantee, // allocation, // milestones, diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 8b07847..56c3a66 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -9,8 +9,11 @@ import {BorgAuth} from "cybercorps-contracts/src/libs/auth.sol"; import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; import {CyberAgreementUtils} from "cybercorps-contracts/test/libs/CyberAgreementUtils.sol"; import {MockERC20} from "../mocks/MockERC20.sol"; +import {MetaVestDealLib, MetaVestDeal} from "../../src/lib/MetaVestDealLib.sol"; contract MetaVesTControllerTestBase is Test { + using MetaVestDealLib for MetaVestDeal; + MockERC20 paymentToken = new MockERC20("Payment Token", "PAY", 18); address deployer = address(0x2); @@ -180,10 +183,11 @@ contract MetaVesTControllerTestBase is Test { bytes32 contractId = controller.proposeAndSignDeal( templateId, agreementSalt, - metavestController.metavestType.Vesting, - grantee, - allocation, - milestones, + MetaVestDealLib.draft().setVesting( + grantee, + allocation, + milestones + ), globalValues, parties, partyValues, @@ -221,7 +225,7 @@ contract MetaVesTControllerTestBase is Test { string memory partyName, bytes memory expectRevertData ) internal returns(address) { - metavestController.DealData memory deal = controller.getDeal(contractId); + MetaVestDeal memory deal = controller.getDeal(contractId); string[] memory globalValues = new string[](11); globalValues[0] = "0"; // metavestType: Vesting From 960c01630e810108adfe79ef124bf0ec90c62261 Mon Sep 17 00:00:00 2001 From: detoo Date: Thu, 23 Oct 2025 16:01:17 -0700 Subject: [PATCH 03/25] wip: feat: create MetaVesTControllerStorage --- src/MetaVesTController.sol | 428 ++++++++++------------ src/storage/MetaVesTControllerStorage.sol | 193 ++++++++++ test/AuditBaseA2.t.sol | 6 +- test/amendement.t.sol | 8 +- 4 files changed, 385 insertions(+), 250 deletions(-) create mode 100644 src/storage/MetaVesTControllerStorage.sol diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index af440eb..0149d88 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -11,6 +11,7 @@ pragma solidity ^0.8.24; import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "./lib/MetaVestDealLib.sol"; +import {MetaVesTControllerStorage} from "./storage/MetaVesTControllerStorage.sol"; import "./BaseAllocation.sol"; //import "./RestrictedTokenAllocation.sol"; import "./interfaces/IAllocationFactory.sol"; @@ -27,67 +28,14 @@ import "./lib/EnumberableSet.sol"; * by an applicable affected grantee or a majority-in-governing power of similar token grantees **/ contract metavestController is UUPSUpgradeable, SafeTransferLib { + using MetaVesTControllerStorage for MetaVesTControllerStorage.MetaVesTControllerData; using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; + /// @dev opinionated time limit for a MetaVesT amendment, one calendar week in seconds uint256 internal constant AMENDMENT_TIME_LIMIT = 604800; uint256 internal constant ARRAY_LENGTH_LIMIT = 20; - mapping(bytes32 => EnumerableSet.AddressSet) private sets; - EnumerableSet.Bytes32Set private setNames; - - address public authority; - address public dao; - address public registry; - address public vestingFactory; - address public tokenOptionFactory; - address public restrictedTokenFactory; - address internal _pendingAuthority; - address internal _pendingDao; - - // Simple indexer for UX - bytes32[] public dealIds; - - struct AmendmentProposal { - bool isPending; - bytes32 dataHash; - bool inFavor; - } - - struct MajorityAmendmentProposal { - uint256 totalVotingPower; - uint256 currentVotingPower; - uint256 time; - bool isPending; - bytes32 dataHash; - address[] voters; - mapping(address => uint256) appliedProposalCreatedAt; - mapping(address => uint256) voterPower; - } - - /// @notice maps a function's signature to a Condition contract address - mapping(bytes4 => address[]) public functionToConditions; - - /// @notice maps a metavest-parameter-updating function's signature to token contract to whether a majority amendment is pending - mapping(bytes4 => mapping(bytes32 => MajorityAmendmentProposal)) public functionToSetMajorityProposal; - - /// @notice maps a metavest-parameter-updating function's signature to affected grantee address to whether an amendment is pending - mapping(bytes4 => mapping(address => AmendmentProposal)) public functionToGranteeToAmendmentPending; - - /// @notice tracks if an address has voted for an amendment by mapping a hash of the pertinent details to time they last voted for these details (voter, function and affected grantee) - mapping(bytes32 => uint256) internal _lastVoted; - - mapping(bytes32 => bool) public setMajorityVoteActive; - - /// @notice granteeId => granteeData - mapping(bytes32 => MetaVestDeal) public deals; - - /// @notice Maps agreement IDs to arrays of counter party values for closed deals. - mapping(bytes32 => string[]) public counterPartyValues; - - /// @notice Map MetaVesT contract address to its corresponding agreement ID - mapping(address => bytes32) public metavestAgreementIds; - /// /// EVENTS /// @@ -118,7 +66,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address metavest ); - /// /// ERRORS /// @@ -165,7 +112,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// modifier conditionCheck() { - address[] memory conditions = functionToConditions[msg.sig]; + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address[] memory conditions = st.functionToConditions[msg.sig]; for (uint256 i; i < conditions.length; ++i) { if (!IConditionM(conditions[i]).checkCondition(address(this), msg.sig, "")) { revert MetaVesTController_ConditionNotSatisfied(conditions[i]); @@ -175,9 +123,10 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } modifier consentCheck(address _grant, bytes calldata _data) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if (isMetavestInSet(_grant)) { bytes32 set = getSetOfMetavest(_grant); - MajorityAmendmentProposal storage proposal = functionToSetMajorityProposal[msg.sig][set]; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msg.sig][set]; if(proposal.appliedProposalCreatedAt[_grant] == proposal.time) revert MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); if (_data.length>32 && _data.length<69) { @@ -187,7 +136,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } else revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); } else { - AmendmentProposal storage proposal = functionToGranteeToAmendmentPending[msg.sig][_grant]; + MetaVesTControllerStorage.AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msg.sig][_grant]; if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); } @@ -196,12 +145,12 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } modifier onlyAuthority() { - if (msg.sender != authority) revert MetaVesTController_OnlyAuthority(); + if (msg.sender != MetaVesTControllerStorage.getStorage().authority) revert MetaVesTController_OnlyAuthority(); _; } modifier onlyDao() { - if (msg.sender != dao) revert MetaVesTController_OnlyDAO(); + if (msg.sender != MetaVesTControllerStorage.getStorage().dao) revert MetaVesTController_OnlyDAO(); _; } @@ -219,24 +168,27 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { __UUPSUpgradeable_init(); if (_authority == address(0)) revert MetaVesTController_ZeroAddress(); - authority = _authority; - registry = _registry; - vestingFactory = _vestingFactory; + + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + st.authority = _authority; + st.registry = _registry; + st.vestingFactory = _vestingFactory; // tokenOptionFactory = _tokenOptionFactory; // restrictedTokenFactory = _restrictedTokenFactory; - dao = _dao; + st.dao = _dao; } /// @notice for a grantee to consent to an update to one of their metavestDetails by 'authority' corresponding to the applicable function in this controller /// @param _msgSig function signature of the function in this controller which (if successfully executed) will execute the grantee's metavest detail update /// @param _inFavor whether msg.sender consents to the applicable amending function call (rather than assuming true, this param allows a grantee to later revoke decision should 'authority' delay or breach agreement elsewhere) function consentToMetavestAmendment(address _grant, bytes4 _msgSig, bool _inFavor) external { - if (!functionToGranteeToAmendmentPending[_msgSig][_grant].isPending) + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (!st.functionToGranteeToAmendmentPending[_msgSig][_grant].isPending) revert MetaVesTController_NoPendingAmendment(_msgSig, _grant); - address grantee =BaseAllocation(_grant).grantee(); - if(msg.sender!= grantee) revert MetaVesTController_OnlyGranteeMayCall(); + address grantee = BaseAllocation(_grant).grantee(); + if (msg.sender != grantee) revert MetaVesTController_OnlyGranteeMayCall(); - functionToGranteeToAmendmentPending[_msgSig][_grant].inFavor = _inFavor; + st.functionToGranteeToAmendmentPending[_msgSig][_grant].inFavor = _inFavor; emit MetaVesTController_AmendmentConsentUpdated(_msgSig, msg.sender, _inFavor); } @@ -245,18 +197,20 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @param _condition address of the applicable Condition contract-- pass address(0) to remove the requirement for '_functionSig' /// @param _functionSig signature of the function which is having its condition requirement updated function updateFunctionCondition(address _condition, bytes4 _functionSig) external onlyDao { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); //call check condition to ensure the condition is valid IConditionM(_condition).checkCondition(address(this), msg.sig, ""); //check to ensure the condition is unique - for (uint256 i; i < functionToConditions[_functionSig].length; ++i) { - if (functionToConditions[_functionSig][i] == _condition) revert MetaVestController_DuplicateCondition(); + for (uint256 i; i < st.functionToConditions[_functionSig].length; ++i) { + if (st.functionToConditions[_functionSig][i] == _condition) revert MetaVestController_DuplicateCondition(); } - functionToConditions[_functionSig].push(_condition); + st.functionToConditions[_functionSig].push(_condition); emit MetaVesTController_ConditionUpdated(_condition, _functionSig); } function removeFunctionCondition(address _condition, bytes4 _functionSig) external onlyDao { - address[] storage conditions = functionToConditions[_functionSig]; + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address[] storage conditions = st.functionToConditions[_functionSig]; for (uint256 i; i < conditions.length; ++i) { if (conditions[i] == _condition) { conditions[i] = conditions[conditions.length - 1]; @@ -279,11 +233,12 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes32 secretHash, uint256 expiry ) external returns (bytes32) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // TODO validate parties against deal // Call internal function to avoid stack-too-deep errors - dealDraft.agreementId = ICyberAgreementRegistry(registry).createContract( + dealDraft.agreementId = ICyberAgreementRegistry(st.registry).createContract( templateId, salt, globalValues, @@ -296,10 +251,10 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); - counterPartyValues[dealDraft.agreementId] = partyValues[1]; + st.counterPartyValues[dealDraft.agreementId] = partyValues[1]; - ICyberAgreementRegistry(registry).signContractFor( - authority, // First party (grantor) should always be the authority + ICyberAgreementRegistry(st.registry).signContractFor( + st.authority, // First party (grantor) should always be the authority dealDraft.agreementId, partyValues[0], signature, @@ -307,13 +262,13 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { "" // Signer == proposer, no secret needed ); - deals[dealDraft.agreementId] = dealDraft; - dealIds.push(dealDraft.agreementId); + st.deals[dealDraft.agreementId] = dealDraft; + st.dealIds.push(dealDraft.agreementId); emit MetaVesTController_DealProposed( dealDraft.agreementId, dealDraft.grantee, dealDraft.metavestType, dealDraft.allocation, dealDraft.milestones, secretHash > 0, - registry + st.registry ); return dealDraft.agreementId; } @@ -327,51 +282,50 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes memory signature, string memory secret ) external conditionCheck returns (address) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + // Finalize agreement - if(ICyberAgreementRegistry(registry).isVoided(agreementId)) revert MetaVesTController_DealVoided(); - if(ICyberAgreementRegistry(registry).isFinalized(agreementId)) revert MetaVesTController_DealAlreadyFinalized(); + if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) revert MetaVesTController_DealVoided(); + if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) revert MetaVesTController_DealAlreadyFinalized(); - string[] storage counterPartyCheck = counterPartyValues[agreementId]; + string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) revert MetaVesTController_CounterPartyValueMismatch(); - ICyberAgreementRegistry(registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); + ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); - ICyberAgreementRegistry(registry).finalizeContract(agreementId); + ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); // Create and provision MetaVesT + MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; + uint256 total = validate(deal, recipient); + address newMetavest = MetaVesTControllerStorage.createMetavest(agreementId, recipient); - address newMetavest = _createMetavest(agreementId, recipient); + if (newMetavest == address(0)) { + revert MetaVesTController_IncorrectMetaVesTType(); + } - emit MetaVesTController_DealFinalizedAndMetaVestCreated(agreementId, recipient, newMetavest); + // Interaction: transfer tokens to escrow + safeTransferFrom(deal.allocation.tokenContract, st.authority, newMetavest, total); + emit MetaVesTController_DealFinalizedAndMetaVestCreated(agreementId, recipient, newMetavest); return newMetavest; } - function _createMetavest(bytes32 agreementId, address recipient) internal returns (address) { - MetaVestDeal storage deal = deals[agreementId]; + // TODO merge the unnecessary functions + function validate(MetaVestDeal storage deal, address recipient) internal view returns (uint256 total) { + validateInputParameters(deal, recipient); + validateAllocation(deal.allocation); - if(deal.metavestType == MetaVestType.Vesting) - { - deal.metavest = createVestingAllocation(deal, recipient); - } - else if(deal.metavestType == MetaVestType.TokenOption) - { - deal.metavest = createTokenOptionAllocation(deal, recipient); - } - else if(deal.metavestType == MetaVestType.RestrictedTokenAward) - { - deal.metavest = createRestrictedTokenAward(deal, recipient); - } - else - { - revert MetaVesTController_IncorrectMetaVesTType(); - } - metavestAgreementIds[deal.metavest] = agreementId; + uint256 milestoneTotal = validateAndCalculateMilestones(deal.milestones); + total = deal.allocation.tokenStreamTotal + milestoneTotal; + + if (total == 0) revert MetaVesTController_ZeroAmount(); - return deal.metavest; + validateTokenApprovalAndBalance(deal.allocation.tokenContract, total); + + return total; } - function validateInputParameters( MetaVestDeal storage deal, @@ -404,88 +358,13 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } function validateTokenApprovalAndBalance(address tokenContract, uint256 total) internal view { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if ( - IERC20M(tokenContract).allowance(authority, address(this)) < total || - IERC20M(tokenContract).balanceOf(authority) < total + IERC20M(tokenContract).allowance(st.authority, address(this)) < total || + IERC20M(tokenContract).balanceOf(st.authority) < total ) revert MetaVesTController_AmountNotApprovedForTransferFrom(); } - // TODO why doesn't it need conditionCheck? - function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address){ - validateInputParameters(deal, recipient); - validateAllocation(deal.allocation); - uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); - - uint256 _total = deal.allocation.tokenStreamTotal + _milestoneTotal; - if (_total == 0) revert MetaVesTController_ZeroAmount(); - validateTokenApprovalAndBalance(deal.allocation.tokenContract, _total); - - address vestingAllocation = IAllocationFactory(vestingFactory).createAllocation( - IAllocationFactory.AllocationType.Vesting, - deal.grantee, - recipient, - address(this), - deal.allocation, - deal.milestones, - address(0), - 0, - 0 - ); - safeTransferFrom(deal.allocation.tokenContract, authority, vestingAllocation, _total); - - return vestingAllocation; - } - - function createTokenOptionAllocation(MetaVestDeal storage deal, address recipient) internal conditionCheck returns (address) { - validateInputParameters(deal, recipient); - validateAllocation(deal.allocation); - uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); - - uint256 _total = deal.allocation.tokenStreamTotal + _milestoneTotal; - if (_total == 0) revert MetaVesTController_ZeroAmount(); - validateTokenApprovalAndBalance(deal.allocation.tokenContract, _total); - - address tokenOptionAllocation = IAllocationFactory(tokenOptionFactory).createAllocation( - IAllocationFactory.AllocationType.TokenOption, - deal.grantee, - recipient, - address(this), - deal.allocation, - deal.milestones, - deal.paymentToken, - deal.exercisePrice, - deal.shortStopDuration - ); - - safeTransferFrom(deal.allocation.tokenContract, authority, tokenOptionAllocation, _total); - return tokenOptionAllocation; - } - - function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal conditionCheck returns (address){ - validateInputParameters(deal, recipient); - validateAllocation(deal.allocation); - uint256 _milestoneTotal = validateAndCalculateMilestones(deal.milestones); - - uint256 _total = deal.allocation.tokenStreamTotal + _milestoneTotal; - if (_total == 0) revert MetaVesTController_ZeroAmount(); - validateTokenApprovalAndBalance(deal.allocation.tokenContract, _total); - - address restrictedTokenAward = IAllocationFactory(restrictedTokenFactory).createAllocation( - IAllocationFactory.AllocationType.RestrictedToken, - deal.grantee, - recipient, - address(this), - deal.allocation, - deal.milestones, - deal.paymentToken, - deal.exercisePrice, - deal.shortStopDuration - ); - - safeTransferFrom(deal.allocation.tokenContract, authority, restrictedTokenAward, _total); - return restrictedTokenAward; - } - function getMetaVestType(address _grant) public view returns (uint256) { return BaseAllocation(_grant).getVestingType(); } @@ -493,10 +372,11 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @notice for 'authority' to withdraw tokens from this controller (i.e. which it has withdrawn from 'metavest', typically 'paymentToken') /// @param _tokenContract contract address of the token which is being withdrawn function withdrawFromController(address _tokenContract) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); uint256 _balance = IERC20M(_tokenContract).balanceOf(address(this)); if (_balance == 0) revert MetaVesTController_ZeroAmount(); - safeTransfer(_tokenContract, authority, _balance); + safeTransfer(_tokenContract, st.authority, _balance); } /// @notice for 'authority' to toggle whether '_grantee''s MetaVesT is transferable-- does not revoke previous transfers, but does cause such transferees' MetaVesTs transferability to be similarly updated @@ -624,15 +504,16 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @param _newAuthority new address for pending 'authority', who must accept the role by calling 'acceptAuthorityRole' function initiateAuthorityUpdate(address _newAuthority) external onlyAuthority { if (_newAuthority == address(0)) revert MetaVesTController_ZeroAddress(); - _pendingAuthority = _newAuthority; + MetaVesTControllerStorage.getStorage()._pendingAuthority = _newAuthority; } /// @notice allows the pending new authority to accept the role transfer /// @dev access restricted to the address stored as '_pendingauthority' to accept the two-step change. Transfers 'authority' role to the caller (reflected in 'metavest') and deletes '_pendingauthority' to reset. function acceptAuthorityRole() external { - if (msg.sender != _pendingAuthority) revert MetaVesTController_OnlyPendingAuthority(); - delete _pendingAuthority; - authority = msg.sender; + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (msg.sender != st._pendingAuthority) revert MetaVesTController_OnlyPendingAuthority(); + delete st._pendingAuthority; + st.authority = msg.sender; emit MetaVesTController_AuthorityUpdated(msg.sender); } @@ -641,16 +522,17 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @param _newDao new address for pending 'dao', who must accept the role by calling 'acceptDaoRole' function initiateDaoUpdate(address _newDao) external onlyDao { if (_newDao == address(0)) revert MetaVesTController_ZeroAddress(); - _pendingDao = _newDao; + MetaVesTControllerStorage.getStorage()._pendingDao = _newDao; } /// @notice allows the pending new dao to accept the role transfer /// @dev access restricted to the address stored as '_pendingDao' to accept the two-step change. Transfers 'dao' role to the caller (reflected in 'metavest') and deletes '_pendingDao' to reset. /// no 'conditionCheck' necessary as it more properly contained in 'initiateAuthorityUpdate' function acceptDaoRole() external { - if (msg.sender != _pendingDao) revert MetaVesTController_OnlyPendingDao(); - delete _pendingDao; - dao = msg.sender; + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (msg.sender != st._pendingDao) revert MetaVesTController_OnlyPendingDao(); + delete st._pendingDao; + st.dao = msg.sender; emit MetaVesTController_DaoUpdated(msg.sender); } @@ -662,12 +544,13 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes4 _msgSig, bytes memory _callData ) external onlyAuthority { - //override existing amendment if it exists - functionToGranteeToAmendmentPending[_msgSig][_grant] = AmendmentProposal( - true, - keccak256(_callData), - false - ); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + //override existing amendment if it exists + st.functionToGranteeToAmendmentPending[_msgSig][_grant] = MetaVesTControllerStorage.AmendmentProposal( + true, + keccak256(_callData), + false + ); emit MetaVesTController_AmendmentProposed(_grant, _msgSig); } @@ -680,14 +563,15 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes4 _msgSig, bytes calldata _callData ) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if(!doesSetExist(setName)) revert MetaVesTController_SetDoesNotExist(); if(_callData.length!=68) revert MetaVesTController_LengthMismatch(); bytes32 nameHash = keccak256(bytes(setName)); //if the majority proposal is already pending and not expired, revert - if ((functionToSetMajorityProposal[_msgSig][nameHash].isPending && block.timestamp < functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) || setMajorityVoteActive[nameHash]) + if ((st.functionToSetMajorityProposal[_msgSig][nameHash].isPending && block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) || st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); - MajorityAmendmentProposal storage proposal = functionToSetMajorityProposal[_msgSig][nameHash]; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][nameHash]; proposal.isPending = true; proposal.dataHash = keccak256(_callData[_callData.length - 32:]); proposal.time = block.timestamp; @@ -695,14 +579,14 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { proposal.currentVotingPower = 0; uint256 totalVotingPower; - for (uint256 i; i < sets[nameHash].length(); ++i) { - uint256 _votingPower = BaseAllocation(sets[nameHash].at(i)).getMajorityVotingPower(); + for (uint256 i; i < st.sets[nameHash].length(); ++i) { + uint256 _votingPower = BaseAllocation(st.sets[nameHash].at(i)).getMajorityVotingPower(); totalVotingPower += _votingPower; - proposal.voterPower[sets[nameHash].at(i)] = _votingPower; + proposal.voterPower[st.sets[nameHash].at(i)] = _votingPower; } proposal.totalVotingPower = totalVotingPower; - setMajorityVoteActive[nameHash] = true; + st.setMajorityVoteActive[nameHash] = true; emit MetaVesTController_MajorityAmendmentProposed(setName, _msgSig, _callData, totalVotingPower); } @@ -710,27 +594,28 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @param _setName name of the set for majority set amendment proposal /// @param _msgSig function signature of the function in this controller which (if successfully executed) will execute the metavest detail update function cancelExpiredMajorityMetavestAmendment(string memory _setName, bytes4 _msgSig) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if(!doesSetExist(_setName)) revert MetaVesTController_SetDoesNotExist(); bytes32 nameHash = keccak256(bytes(_setName)); - if (!setMajorityVoteActive[nameHash] || block.timestamp < functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) revert MetaVesTController_AmendmentCannotBeCanceled(); - setMajorityVoteActive[nameHash] = false; + if (!st.setMajorityVoteActive[nameHash] || block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) revert MetaVesTController_AmendmentCannotBeCanceled(); + st.setMajorityVoteActive[nameHash] = false; } /// @notice for a grantees to vote upon a metavest update for which they share a common amount of 'tokenGoverningPower' /// @param _msgSig function signature of the function in this controller which (if successfully executed) will execute the metavest detail update /// @param _inFavor whether msg.sender is in favor of the applicable amendment function voteOnMetavestAmendment(address _grant, string memory _setName, bytes4 _msgSig, bool _inFavor) external { - + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_setName)); if(BaseAllocation(_grant).grantee() != msg.sender) revert MetaVesTController_OnlyGranteeMayCall(); if (!isMetavestInSet(_grant, _setName)) revert MetaVesTController_SetDoesNotExist(); - if (!functionToSetMajorityProposal[_msgSig][nameHash].isPending) revert MetaVesTController_NoPendingAmendment(_msgSig, _grant); + if (!st.functionToSetMajorityProposal[_msgSig][nameHash].isPending) revert MetaVesTController_NoPendingAmendment(_msgSig, _grant); if (!_checkFunctionToTokenToAmendmentTime(_msgSig, _setName)) revert MetaVesTController_ProposedAmendmentExpired(); - metavestController.MajorityAmendmentProposal storage proposal = functionToSetMajorityProposal[_msgSig][nameHash]; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][nameHash]; uint256 _callerPower = proposal.voterPower[_grant]; //check if the grant has already voted. @@ -747,57 +632,62 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @notice resets applicable amendment variables because either the applicable amending function has been successfully called or a pending amendment is being overridden with a new one function _resetAmendmentParams(address _grant, bytes4 _msgSig) internal { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if(isMetavestInSet(_grant)) { bytes32 set = getSetOfMetavest(_grant); - MajorityAmendmentProposal storage proposal = functionToSetMajorityProposal[_msgSig][set]; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][set]; proposal.appliedProposalCreatedAt[_grant] = proposal.time; - setMajorityVoteActive[set] = false; + st.setMajorityVoteActive[set] = false; } - delete functionToGranteeToAmendmentPending[_msgSig][_grant]; + delete st.functionToGranteeToAmendmentPending[_msgSig][_grant]; } /// @notice check whether the applicable proposed amendment has expired function _checkFunctionToTokenToAmendmentTime(bytes4 _msgSig, string memory _setName) internal view returns (bool) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); //check the majority proposal time bytes32 nameHash = keccak256(bytes(_setName)); - return (block.timestamp < functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT); + return (block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT); } function createSet(string memory _name) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); if(bytes(_name).length == 0) revert MetaVesTController_ZeroAddress(); - if (setNames.contains(nameHash)) revert MetaVesTController_SetAlreadyExists(); + if (st.setNames.contains(nameHash)) revert MetaVesTController_SetAlreadyExists(); if (bytes(_name).length > 512) revert MetaVesTController_StringTooLong(); - - setNames.add(nameHash); + + st.setNames.add(nameHash); emit MetaVesTController_SetCreated(_name); } function removeSet(string memory _name) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if (setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); - if (!setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); + if (!st.setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); // Remove all addresses from the set starting from the last element - for (uint256 i = sets[nameHash].length(); i > 0; i--) { - address _grant = sets[nameHash].at(i - 1); - sets[nameHash].remove(_grant); + for (uint256 i = st.sets[nameHash].length(); i > 0; i--) { + address _grant = st.sets[nameHash].at(i - 1); + st.sets[nameHash].remove(_grant); } - setNames.remove(nameHash); + st.setNames.remove(nameHash); emit MetaVesTController_SetRemoved(_name); } function doesSetExist(string memory _name) internal view returns (bool) { - return setNames.contains(keccak256(bytes(_name))); + return MetaVesTControllerStorage.getStorage().setNames.contains(keccak256(bytes(_name))); } function isMetavestInSet(address _metavest) internal view returns (bool) { - uint256 length = setNames.length(); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + uint256 length = st.setNames.length(); for (uint256 i = 0; i < length; i++) { - bytes32 nameHash = setNames.at(i); - if (sets[nameHash].contains(_metavest)) { + bytes32 nameHash = st.setNames.at(i); + if (st.sets[nameHash].contains(_metavest)) { return true; } } @@ -806,14 +696,15 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function isMetavestInSet(address _metavest, string memory _setName) internal view returns (bool) { bytes32 nameHash = keccak256(bytes(_setName)); - return sets[nameHash].contains(_metavest); + return MetaVesTControllerStorage.getStorage().sets[nameHash].contains(_metavest); } function getSetOfMetavest(address _metavest) internal view returns (bytes32) { - uint256 length = setNames.length(); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + uint256 length = st.setNames.length(); for (uint256 i = 0; i < length; i++) { - bytes32 nameHash = setNames.at(i); - if (sets[nameHash].contains(_metavest)) { + bytes32 nameHash = st.setNames.at(i); + if (st.sets[nameHash].contains(_metavest)) { return nameHash; } } @@ -821,37 +712,88 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } function addMetaVestToSet(string memory _name, address _metaVest) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if (!setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); + if (!st.setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); if (isMetavestInSet(_metaVest)) revert MetaVesTController_MetaVesTAlreadyExists(); - if (setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); - sets[nameHash].add(_metaVest); + st.sets[nameHash].add(_metaVest); emit MetaVesTController_AddressAddedToSet(_name, _metaVest); } function removeMetaVestFromSet(string memory _name, address _metaVest) external onlyAuthority { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if (!setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); - if (setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); - if (!sets[nameHash].contains(_metaVest)) revert MetaVestController_MetaVestNotInSet(); + if (!st.setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); + if (!st.sets[nameHash].contains(_metaVest)) revert MetaVestController_MetaVestNotInSet(); - sets[nameHash].remove(_metaVest); + st.sets[nameHash].remove(_metaVest); emit MetaVesTController_AddressRemovedFromSet(_name, _metaVest); } function getDeal(bytes32 agreementId) public view returns (MetaVestDeal memory) { - return deals[agreementId]; + return MetaVesTControllerStorage.getStorage().deals[agreementId]; } // Simple indexer for UX function getNumberOfDeals() public view returns(uint256) { - return dealIds.length; + return MetaVesTControllerStorage.getStorage().dealIds.length; } function getDealId(uint256 index) public view returns(bytes32) { - return dealIds[index]; + return MetaVesTControllerStorage.getStorage().dealIds[index]; + } + + function authority() external view returns (address) { + return MetaVesTControllerStorage.getStorage().authority; + } + + function dao() external view returns (address) { + return MetaVesTControllerStorage.getStorage().dao; + } + + function registry() external view returns (address) { + return MetaVesTControllerStorage.getStorage().registry; + } + + function vestingFactory() external view returns (address) { + return MetaVesTControllerStorage.getStorage().vestingFactory; + } + + function tokenOptionFactory() external view returns (address) { + return MetaVesTControllerStorage.getStorage().tokenOptionFactory; + } + + function restrictedTokenFactory() external view returns (address) { + return MetaVesTControllerStorage.getStorage().restrictedTokenFactory; + } + + function functionToConditions(bytes4 sig, uint256 idx) external view returns (address) { + return MetaVesTControllerStorage.getStorage().functionToConditions[sig][idx]; + } + + function functionToGranteeToAmendmentPending(bytes4 sig, address grant) external view returns (MetaVesTControllerStorage.AmendmentProposal memory) { + return MetaVesTControllerStorage.getStorage().functionToGranteeToAmendmentPending[sig][grant]; + } + + function functionToSetMajorityProposal(bytes4 sig, bytes32 set) external view returns ( + uint256 totalVotingPower, + uint256 currentVotingPower, + uint256 time, + bool isPending, + bytes32 dataHash + ) { + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = MetaVesTControllerStorage.getStorage().functionToSetMajorityProposal[sig][set]; + return ( + proposal.totalVotingPower, + proposal.currentVotingPower, + proposal.time, + proposal.isPending, + proposal.dataHash + ); } function _authorizeUpgrade( diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol new file mode 100644 index 0000000..47173c5 --- /dev/null +++ b/src/storage/MetaVesTControllerStorage.sol @@ -0,0 +1,193 @@ + +/* .o. + .888. + .8"888. + .8' `888. + .88ooo8888. + .8' `888. + o88o o8888o + + + + ooo ooooo . ooooo ooooooo ooooo + `88. .888' .o8 `888' `8888 d8' + 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P + 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' + 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. + 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b + o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o + + + + .oooooo. .o8 .oooooo. + d8P' `Y8b "888 d8P' `Y8b + 888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo. + 888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b + 888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 + `88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. + `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P + .o..P' 888 + `Y8P' o888o + _______________________________________________________________________________________________________ + + All software, documentation and other files and information in this repository (collectively, the "Software") + are copyright MetaLeX Labs, Inc., a Delaware corporation. + + All rights reserved. + + The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, + distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or + mechanical, including photocopying, recording, or by any information storage and retrieval system, + except with the express prior written permission of the copyright holder.*/ + +pragma solidity 0.8.28; + +import {EnumerableSet} from "../lib/EnumberableSet.sol"; +import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "../lib/MetaVestDealLib.sol"; +import {IAllocationFactory} from "../interfaces/IAllocationFactory.sol"; + +library MetaVesTControllerStorage { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; + + // Storage slot for our struct + bytes32 constant internal STORAGE_POSITION = keccak256("cybercorp.metavest.controller.storage.v1"); + + struct AmendmentProposal { + bool isPending; + bytes32 dataHash; + bool inFavor; + } + + struct MajorityAmendmentProposal { + uint256 totalVotingPower; + uint256 currentVotingPower; + uint256 time; + bool isPending; + bytes32 dataHash; + address[] voters; + mapping(address => uint256) appliedProposalCreatedAt; + mapping(address => uint256) voterPower; + } + + struct MetaVesTControllerData { + address authority; + address dao; + address registry; + address vestingFactory; + address tokenOptionFactory; + address restrictedTokenFactory; + address _pendingAuthority; + address _pendingDao; + + // Simple indexer for UX + bytes32[] dealIds; + + EnumerableSet.Bytes32Set setNames; + + mapping(bytes32 => EnumerableSet.AddressSet) sets; + + /// @notice maps a function's signature to a Condition contract address + mapping(bytes4 => address[]) functionToConditions; + + /// @notice maps a metavest-parameter-updating function's signature to token contract to whether a majority amendment is pending + mapping(bytes4 => mapping(bytes32 => MajorityAmendmentProposal)) functionToSetMajorityProposal; + + /// @notice maps a metavest-parameter-updating function's signature to affected grantee address to whether an amendment is pending + mapping(bytes4 => mapping(address => AmendmentProposal)) functionToGranteeToAmendmentPending; + + /// @notice tracks if an address has voted for an amendment by mapping a hash of the pertinent details to time they last voted for these details (voter, function and affected grantee) + mapping(bytes32 => uint256) _lastVoted; + + mapping(bytes32 => bool) setMajorityVoteActive; + + /// @notice granteeId => granteeData + mapping(bytes32 => MetaVestDeal) deals; + + /// @notice Maps agreement IDs to arrays of counter party values for closed deals. + mapping(bytes32 => string[]) counterPartyValues; + + /// @notice Map MetaVesT contract address to its corresponding agreement ID + mapping(address => bytes32) metavestAgreementIds; + } + + function getStorage() internal pure returns (MetaVesTControllerData storage st) { + bytes32 position = STORAGE_POSITION; + assembly { + st.slot := position + } + } + + function createMetavest(bytes32 agreementId, address recipient) external returns (address) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + + MetaVestDeal storage deal = st.deals[agreementId]; + + if(deal.metavestType == MetaVestType.Vesting) { + deal.metavest = createVestingAllocation(deal, recipient); + } else if(deal.metavestType == MetaVestType.TokenOption) { + deal.metavest = createTokenOptionAllocation(deal, recipient); + } else if(deal.metavestType == MetaVestType.RestrictedTokenAward) { + deal.metavest = createRestrictedTokenAward(deal, recipient); + } else { + return address(0); + } + st.metavestAgreementIds[deal.metavest] = agreementId; + + return deal.metavest; + } + + // TODO why doesn't it need conditionCheck? + function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address){ + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address vestingAllocation = IAllocationFactory(st.vestingFactory).createAllocation( + IAllocationFactory.AllocationType.Vesting, + deal.grantee, + recipient, + address(this), + deal.allocation, + deal.milestones, + address(0), + 0, + 0 + ); + + return vestingAllocation; + } + + // TODO where should we put conditionCheck instead since it will emit events? + function createTokenOptionAllocation(MetaVestDeal storage deal, address recipient) internal returns (address) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address tokenOptionAllocation = IAllocationFactory(st.tokenOptionFactory).createAllocation( + IAllocationFactory.AllocationType.TokenOption, + deal.grantee, + recipient, + address(this), + deal.allocation, + deal.milestones, + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration + ); + + return tokenOptionAllocation; + } + + // TODO where should we put conditionCheck instead since it will emit events? + function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal returns (address){ + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address restrictedTokenAward = IAllocationFactory(st.restrictedTokenFactory).createAllocation( + IAllocationFactory.AllocationType.RestrictedToken, + deal.grantee, + recipient, + address(this), + deal.allocation, + deal.milestones, + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration + ); + + return restrictedTokenAward; + } +} diff --git a/test/AuditBaseA2.t.sol b/test/AuditBaseA2.t.sol index 427dd78..11f7acb 100644 --- a/test/AuditBaseA2.t.sol +++ b/test/AuditBaseA2.t.sol @@ -144,9 +144,9 @@ contract Audit is MetaVestControllerTest { controller.consentToMetavestAmendment(vestingAllocation, controller.removeMetavestMilestone.selector, false); // emit MetaVesTController_AmendmentConsentUpdated(msgSig: 0x75b89e4f00000000000000000000000000000000000000000000000000000000, grantee: ECAdd: [0x0000000000000000000000000000000000000006], inFavor: false) console.log("expected inFavor: false"); - (,,bool inFavor) = controller.functionToGranteeToAmendmentPending(selector, vestingAllocation); - console.log("output: ", inFavor); - assertEq(inFavor, false); + MetaVesTControllerStorage.AmendmentProposal memory proposal = controller.functionToGranteeToAmendmentPending(selector, vestingAllocation); + console.log("output: ", proposal.inFavor); + assertEq(proposal.inFavor, false); } diff --git a/test/amendement.t.sol b/test/amendement.t.sol index e2fc5ca..933a9eb 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -75,11 +75,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.prank(authority); controller.proposeMetavestAmendment(address(vestingAllocation), msgSig, callData); - (bool isPending, bytes32 dataHash, bool inFavor) = controller.functionToGranteeToAmendmentPending(msgSig, address(vestingAllocation)); + MetaVesTControllerStorage.AmendmentProposal memory proposal = controller.functionToGranteeToAmendmentPending(msgSig, address(vestingAllocation)); - assertTrue(isPending); - assertEq(dataHash, keccak256(callData)); - assertFalse(inFavor); + assertTrue(proposal.isPending); + assertEq(proposal.dataHash, keccak256(callData)); + assertFalse(proposal.inFavor); } function test_RevertIf_ProposeMajorityMetavestAmendment() public { From 10d7a99f7d1b1cb8207a30d65df1393368c1a29a Mon Sep 17 00:00:00 2001 From: detoo Date: Thu, 23 Oct 2025 17:47:28 -0700 Subject: [PATCH 04/25] wip: feat: first successfully contract size reduction --- src/MetaVesTController.sol | 57 ++++++----------- src/storage/MetaVesTControllerStorage.sol | 75 ++++++++++++++++++++++- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 0149d88..3aaea2f 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -209,15 +209,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } function removeFunctionCondition(address _condition, bytes4 _functionSig) external onlyDao { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address[] storage conditions = st.functionToConditions[_functionSig]; - for (uint256 i; i < conditions.length; ++i) { - if (conditions[i] == _condition) { - conditions[i] = conditions[conditions.length - 1]; - conditions.pop(); - break; - } - } + MetaVesTControllerStorage.removeFunctionCondition(_condition, _functionSig); emit MetaVesTController_ConditionUpdated(_condition, _functionSig); } @@ -236,41 +228,28 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // TODO validate parties against deal + // Check: verify inputs + if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); + if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); - // Call internal function to avoid stack-too-deep errors - dealDraft.agreementId = ICyberAgreementRegistry(st.registry).createContract( + MetaVestDeal memory dealProposed = MetaVesTControllerStorage.proposeAndSignDeal( templateId, salt, + dealDraft, globalValues, parties, partyValues, + signature, secretHash, - address(this), expiry ); - if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); - if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); - st.counterPartyValues[dealDraft.agreementId] = partyValues[1]; - - ICyberAgreementRegistry(st.registry).signContractFor( - st.authority, // First party (grantor) should always be the authority - dealDraft.agreementId, - partyValues[0], - signature, - false, // Not meant for anyone else other than the signer - "" // Signer == proposer, no secret needed - ); - - st.deals[dealDraft.agreementId] = dealDraft; - st.dealIds.push(dealDraft.agreementId); - emit MetaVesTController_DealProposed( - dealDraft.agreementId, dealDraft.grantee, dealDraft.metavestType, dealDraft.allocation, dealDraft.milestones, + dealProposed.agreementId, dealProposed.grantee, dealProposed.metavestType, dealProposed.allocation, dealProposed.milestones, secretHash > 0, st.registry ); - return dealDraft.agreementId; + return dealProposed.agreementId; } // TODO handle cases when agreement is signed externally @@ -284,7 +263,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) external conditionCheck returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - // Finalize agreement + // Check: verify inputs if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) revert MetaVesTController_DealVoided(); if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) revert MetaVesTController_DealAlreadyFinalized(); @@ -292,14 +271,18 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) revert MetaVesTController_CounterPartyValueMismatch(); - ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); - - ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); - - // Create and provision MetaVesT MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; uint256 total = validate(deal, recipient); - address newMetavest = MetaVesTControllerStorage.createMetavest(agreementId, recipient); + + // Interaction: finalize the deal and create metavest contract + address newMetavest = MetaVesTControllerStorage.signDealAndCreateMetavest( + grantee, + recipient, + agreementId, + partyValues, + signature, + secret + ); if (newMetavest == address(0)) { revert MetaVesTController_IncorrectMetaVesTType(); diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index 47173c5..754ea6e 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -42,6 +42,7 @@ pragma solidity 0.8.28; +import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; import {EnumerableSet} from "../lib/EnumberableSet.sol"; import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "../lib/MetaVestDealLib.sol"; import {IAllocationFactory} from "../interfaces/IAllocationFactory.sol"; @@ -118,7 +119,7 @@ library MetaVesTControllerStorage { } } - function createMetavest(bytes32 agreementId, address recipient) external returns (address) { + function createMetavest(bytes32 agreementId, address recipient) internal returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); MetaVestDeal storage deal = st.deals[agreementId]; @@ -190,4 +191,76 @@ library MetaVesTControllerStorage { return restrictedTokenAward; } + + function proposeAndSignDeal( + bytes32 templateId, + uint256 salt, + MetaVestDeal memory dealDraft, + string[] memory globalValues, + address[] memory parties, + string[][] memory partyValues, + bytes calldata signature, + bytes32 secretHash, + uint256 expiry + ) external returns (MetaVestDeal memory) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + + // Call internal function to avoid stack-too-deep errors + dealDraft.agreementId = ICyberAgreementRegistry(st.registry).createContract( + templateId, + salt, + globalValues, + parties, + partyValues, + secretHash, + address(this), + expiry + ); + + st.counterPartyValues[dealDraft.agreementId] = partyValues[1]; + + ICyberAgreementRegistry(st.registry).signContractFor( + st.authority, // First party (grantor) should always be the authority + dealDraft.agreementId, + partyValues[0], + signature, + false, // Not meant for anyone else other than the signer + "" // Signer == proposer, no secret needed + ); + + st.deals[dealDraft.agreementId] = dealDraft; + st.dealIds.push(dealDraft.agreementId); + + return dealDraft; + } + + function signDealAndCreateMetavest( + address grantee, + address recipient, + bytes32 agreementId, + string[] memory partyValues, + bytes memory signature, + string memory secret + ) external returns (address) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + + // Finalize agreement + ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); + ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); + + // Create and provision MetaVesT + return createMetavest(agreementId, recipient); + } + + function removeFunctionCondition(address _condition, bytes4 _functionSig) external { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address[] storage conditions = st.functionToConditions[_functionSig]; + for (uint256 i; i < conditions.length; ++i) { + if (conditions[i] == _condition) { + conditions[i] = conditions[conditions.length - 1]; + conditions.pop(); + break; + } + } + } } From 372cc3cc81dd78144158e293381d4c31746f60e9 Mon Sep 17 00:00:00 2001 From: detoo Date: Fri, 24 Oct 2025 10:50:39 -0700 Subject: [PATCH 05/25] wip: feat: more aggressive external library logic --- src/MetaVesTController.sol | 264 +++++++--------------- src/storage/MetaVesTControllerStorage.sol | 204 ++++++++++++++--- test/AuditBaseA.t.sol | 4 +- test/AuditBaseA2.t.sol | 24 +- test/amendement.t.sol | 28 +-- test/controller.t.sol | 32 +-- 6 files changed, 299 insertions(+), 257 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 3aaea2f..8a9b23f 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -13,7 +13,6 @@ import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UU import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "./lib/MetaVestDealLib.sol"; import {MetaVesTControllerStorage} from "./storage/MetaVesTControllerStorage.sol"; import "./BaseAllocation.sol"; -//import "./RestrictedTokenAllocation.sol"; import "./interfaces/IAllocationFactory.sol"; import "./interfaces/IPriceAllocation.sol"; import "./lib/EnumberableSet.sol"; @@ -66,91 +65,35 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address metavest ); - /// - /// ERRORS - /// - - error MetaVesTController_AlreadyVoted(); - error MetaVesTController_OnlyGranteeMayCall(); - error MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - error MetaVesTController_AmendmentAlreadyPending(); - error MetaVesTController_AmendmentCannotBeCanceled(); - error MetaVesTController_AmountNotApprovedForTransferFrom(); - error MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); - error MetaVesTController_CliffGreaterThanTotal(); - error MetaVesTController_ConditionNotSatisfied(address condition); - error MetaVesTController_EmergencyUnlockNotSatisfied(); - error MetaVestController_DuplicateCondition(); - error MetaVesTController_IncorrectMetaVesTType(); - error MetaVesTController_LengthMismatch(); - error MetaVesTController_MetaVesTAlreadyExists(); - error MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); - error MetaVesTController_NoPendingAmendment(bytes4 msgSig, address affectedGrantee); - error MetaVesTController_OnlyAuthority(); - error MetaVesTController_OnlyDAO(); - error MetaVesTController_OnlyPendingAuthority(); - error MetaVesTController_OnlyPendingDao(); - error MetaVesTController_ProposedAmendmentExpired(); - error MetaVesTController_ZeroAddress(); - error MetaVesTController_ZeroAmount(); - error MetaVesTController_ZeroPrice(); - error MetaVesT_AmountNotApprovedForTransferFrom(); - error MetaVesTController_SetDoesNotExist(); - error MetaVestController_MetaVestNotInSet(); - error MetaVesTController_SetAlreadyExists(); - error MetaVesTController_StringTooLong(); - error MetaVesTController_TypeNotSupported(MetaVestType _type); - error MetaVesTController_DealAlreadyFinalized(); - error MetaVesTController_DealVoided(); - error MetaVesTController_CounterPartyNotFound(); - error MetaVesTController_PartyValuesLengthMismatch(); - error MetaVesTController_CounterPartyValueMismatch(); - error MetaVesTController_UnauthorizedToMint(); - /// /// FUNCTIONS /// modifier conditionCheck() { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address[] memory conditions = st.functionToConditions[msg.sig]; - for (uint256 i; i < conditions.length; ++i) { - if (!IConditionM(conditions[i]).checkCondition(address(this), msg.sig, "")) { - revert MetaVesTController_ConditionNotSatisfied(conditions[i]); - } + address failedCondition = MetaVesTControllerStorage.conditionCheck(); + if (failedCondition != address(0)) { + revert MetaVesTControllerStorage.MetaVesTController_ConditionNotSatisfied(failedCondition); } _; } modifier consentCheck(address _grant, bytes calldata _data) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if (isMetavestInSet(_grant)) { - bytes32 set = getSetOfMetavest(_grant); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msg.sig][set]; - if(proposal.appliedProposalCreatedAt[_grant] == proposal.time) revert MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); - if (_data.length>32 && _data.length<69) - { - if (!proposal.isPending || proposal.totalVotingPower>proposal.currentVotingPower*2 || keccak256(_data[_data.length - 32:]) != proposal.dataHash ) { - revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - } - } - else revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - } else { - MetaVesTControllerStorage.AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msg.sig][_grant]; - if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { - revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - } + bytes4 error = MetaVesTControllerStorage.consentCheck(_grant, _data); + if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector) { + revert MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); + } else if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector) { + revert MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); } _; } modifier onlyAuthority() { - if (msg.sender != MetaVesTControllerStorage.getStorage().authority) revert MetaVesTController_OnlyAuthority(); + if (msg.sender != MetaVesTControllerStorage.getStorage().authority) revert MetaVesTControllerStorage.MetaVesTController_OnlyAuthority(); _; } modifier onlyDao() { - if (msg.sender != MetaVesTControllerStorage.getStorage().dao) revert MetaVesTController_OnlyDAO(); + if (msg.sender != MetaVesTControllerStorage.getStorage().dao) revert MetaVesTControllerStorage.MetaVesTController_OnlyDAO(); _; } @@ -167,7 +110,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) public initializer { __UUPSUpgradeable_init(); - if (_authority == address(0)) revert MetaVesTController_ZeroAddress(); + if (_authority == address(0)) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); st.authority = _authority; @@ -184,9 +127,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function consentToMetavestAmendment(address _grant, bytes4 _msgSig, bool _inFavor) external { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if (!st.functionToGranteeToAmendmentPending[_msgSig][_grant].isPending) - revert MetaVesTController_NoPendingAmendment(_msgSig, _grant); + revert MetaVesTControllerStorage.MetaVesTController_NoPendingAmendment(_msgSig, _grant); address grantee = BaseAllocation(_grant).grantee(); - if (msg.sender != grantee) revert MetaVesTController_OnlyGranteeMayCall(); + if (msg.sender != grantee) revert MetaVesTControllerStorage.MetaVesTController_OnlyGranteeMayCall(); st.functionToGranteeToAmendmentPending[_msgSig][_grant].inFavor = _inFavor; emit MetaVesTController_AmendmentConsentUpdated(_msgSig, msg.sender, _inFavor); @@ -202,14 +145,22 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { IConditionM(_condition).checkCondition(address(this), msg.sig, ""); //check to ensure the condition is unique for (uint256 i; i < st.functionToConditions[_functionSig].length; ++i) { - if (st.functionToConditions[_functionSig][i] == _condition) revert MetaVestController_DuplicateCondition(); + if (st.functionToConditions[_functionSig][i] == _condition) revert MetaVesTControllerStorage.MetaVestController_DuplicateCondition(); } st.functionToConditions[_functionSig].push(_condition); emit MetaVesTController_ConditionUpdated(_condition, _functionSig); } function removeFunctionCondition(address _condition, bytes4 _functionSig) external onlyDao { - MetaVesTControllerStorage.removeFunctionCondition(_condition, _functionSig); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address[] storage conditions = st.functionToConditions[_functionSig]; + for (uint256 i; i < conditions.length; ++i) { + if (conditions[i] == _condition) { + conditions[i] = conditions[conditions.length - 1]; + conditions.pop(); + break; + } + } emit MetaVesTController_ConditionUpdated(_condition, _functionSig); } @@ -229,8 +180,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { // TODO validate parties against deal // Check: verify inputs - if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); - if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); + if (partyValues.length < 2) revert MetaVesTControllerStorage.MetaVesTController_CounterPartyNotFound(); + if (partyValues[1].length != partyValues[0].length) revert MetaVesTControllerStorage.MetaVesTController_PartyValuesLengthMismatch(); MetaVestDeal memory dealProposed = MetaVesTControllerStorage.proposeAndSignDeal( templateId, @@ -260,22 +211,21 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { string[] memory partyValues, bytes memory signature, string memory secret - ) external conditionCheck returns (address) { + ) external conditionCheck() returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // Check: verify inputs - if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) revert MetaVesTController_DealVoided(); - if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) revert MetaVesTController_DealAlreadyFinalized(); + if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) revert MetaVesTControllerStorage.MetaVesTController_DealVoided(); + if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) revert MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized(); string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; - if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) revert MetaVesTController_CounterPartyValueMismatch(); + if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) revert MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch(); MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; - uint256 total = validate(deal, recipient); // Interaction: finalize the deal and create metavest contract - address newMetavest = MetaVesTControllerStorage.signDealAndCreateMetavest( + (address newMetavest, uint256 total, bytes4 error) = MetaVesTControllerStorage.signDealAndCreateMetavest( grantee, recipient, agreementId, @@ -284,8 +234,18 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { secret ); - if (newMetavest == address(0)) { - revert MetaVesTController_IncorrectMetaVesTType(); + if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAddress.selector) { + revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); + } else if (error == MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal.selector) { + revert MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal(); + } else if (error == MetaVesTControllerStorage.MetaVesTController_LengthMismatch.selector) { + revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); + } else if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAmount.selector) { + revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); + } else if (error == MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom.selector) { + revert MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom(); + } else if (error == MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType.selector) { + revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); } // Interaction: transfer tokens to escrow @@ -295,59 +255,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { return newMetavest; } - // TODO merge the unnecessary functions - function validate(MetaVestDeal storage deal, address recipient) internal view returns (uint256 total) { - validateInputParameters(deal, recipient); - validateAllocation(deal.allocation); - - uint256 milestoneTotal = validateAndCalculateMilestones(deal.milestones); - total = deal.allocation.tokenStreamTotal + milestoneTotal; - - if (total == 0) revert MetaVesTController_ZeroAmount(); - - validateTokenApprovalAndBalance(deal.allocation.tokenContract, total); - - return total; - } - - function validateInputParameters( - MetaVestDeal storage deal, - address recipient - ) internal view { - // TODO must differentiate metavest types - if (deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) || deal.paymentToken == address(0) || deal.exercisePrice == 0) - revert MetaVesTController_ZeroAddress(); - } - - function validateAllocation(VestingAllocation.Allocation memory _allocation) internal pure { - if ( - _allocation.vestingCliffCredit > _allocation.tokenStreamTotal || - _allocation.unlockingCliffCredit > _allocation.tokenStreamTotal - ) revert MetaVesTController_CliffGreaterThanTotal(); - } - - function validateAndCalculateMilestones( - VestingAllocation.Milestone[] memory _milestones - ) internal pure returns (uint256 _milestoneTotal) { - if (_milestones.length != 0) { - if (_milestones.length > ARRAY_LENGTH_LIMIT) revert MetaVesTController_LengthMismatch(); - for (uint256 i; i < _milestones.length; ++i) { - if (_milestones[i].conditionContracts.length > ARRAY_LENGTH_LIMIT) - revert MetaVesTController_LengthMismatch(); - if (_milestones[i].milestoneAward == 0) revert MetaVesTController_ZeroAmount(); - _milestoneTotal += _milestones[i].milestoneAward; - } - } - } - - function validateTokenApprovalAndBalance(address tokenContract, uint256 total) internal view { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if ( - IERC20M(tokenContract).allowance(st.authority, address(this)) < total || - IERC20M(tokenContract).balanceOf(st.authority) < total - ) revert MetaVesTController_AmountNotApprovedForTransferFrom(); - } - function getMetaVestType(address _grant) public view returns (uint256) { return BaseAllocation(_grant).getVestingType(); } @@ -357,7 +264,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function withdrawFromController(address _tokenContract) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); uint256 _balance = IERC20M(_tokenContract).balanceOf(address(this)); - if (_balance == 0) revert MetaVesTController_ZeroAmount(); + if (_balance == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); safeTransfer(_tokenContract, st.authority, _balance); } @@ -381,9 +288,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address _grant, uint256 _newPrice ) external onlyAuthority conditionCheck consentCheck(_grant, msg.data) { - if (_newPrice == 0) revert MetaVesTController_ZeroPrice(); + if (_newPrice == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroPrice(); IPriceAllocation grant = IPriceAllocation(_grant); - if(grant.getVestingType()!=2 && grant.getVestingType()!=3) revert MetaVesTController_IncorrectMetaVesTType(); + if(grant.getVestingType()!=2 && grant.getVestingType()!=3) revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); _resetAmendmentParams(_grant, msg.sig); grant.updatePrice(_newPrice); } @@ -397,7 +304,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) external onlyAuthority conditionCheck consentCheck(_grant, msg.data) { _resetAmendmentParams(_grant, msg.sig); (uint256 milestoneAward, , bool completed) = BaseAllocation(_grant).milestones(_milestoneIndex); - if(completed || milestoneAward == 0) revert MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); + if(completed || milestoneAward == 0) revert MetaVesTControllerStorage.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); BaseAllocation(_grant).removeMilestone(_milestoneIndex); } @@ -407,13 +314,13 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function addMetavestMilestone(address _grant, VestingAllocation.Milestone calldata _milestone) external onlyAuthority { address _tokenContract = BaseAllocation(_grant).getMetavestDetails().tokenContract; - if (_milestone.milestoneAward == 0) revert MetaVesTController_ZeroAmount(); - if (_milestone.conditionContracts.length > ARRAY_LENGTH_LIMIT) revert MetaVesTController_LengthMismatch(); - if (_milestone.complete == true) revert MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); + if (_milestone.milestoneAward == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); + if (_milestone.conditionContracts.length > ARRAY_LENGTH_LIMIT) revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); + if (_milestone.complete == true) revert MetaVesTControllerStorage.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); if ( IERC20M(_tokenContract).allowance(msg.sender, address(this)) < _milestone.milestoneAward || IERC20M(_tokenContract).balanceOf(msg.sender) < _milestone.milestoneAward - ) revert MetaVesT_AmountNotApprovedForTransferFrom(); + ) revert MetaVesTControllerStorage.MetaVesT_AmountNotApprovedForTransferFrom(); // send the new milestoneAward to 'metavest' safeTransferFrom(_tokenContract, msg.sender, _grant, _milestone.milestoneAward); @@ -441,7 +348,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) external onlyAuthority conditionCheck { //get unlock rate from the _grant (,,,,,uint160 unlockRate,,) = BaseAllocation(_grant).allocation(); - if(BaseAllocation(_grant).terminated() == false || unlockRate != 0) revert MetaVesTController_EmergencyUnlockNotSatisfied(); + if(BaseAllocation(_grant).terminated() == false || unlockRate != 0) revert MetaVesTControllerStorage.MetaVesTController_EmergencyUnlockNotSatisfied(); BaseAllocation(_grant).updateUnlockRate(_unlockRate); } @@ -486,7 +393,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @dev use care in updating 'authority' as it must have the ability to call 'acceptAuthorityRole()', or once it needs to be replaced, 'updateAuthority()' /// @param _newAuthority new address for pending 'authority', who must accept the role by calling 'acceptAuthorityRole' function initiateAuthorityUpdate(address _newAuthority) external onlyAuthority { - if (_newAuthority == address(0)) revert MetaVesTController_ZeroAddress(); + if (_newAuthority == address(0)) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); MetaVesTControllerStorage.getStorage()._pendingAuthority = _newAuthority; } @@ -494,7 +401,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @dev access restricted to the address stored as '_pendingauthority' to accept the two-step change. Transfers 'authority' role to the caller (reflected in 'metavest') and deletes '_pendingauthority' to reset. function acceptAuthorityRole() external { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if (msg.sender != st._pendingAuthority) revert MetaVesTController_OnlyPendingAuthority(); + if (msg.sender != st._pendingAuthority) revert MetaVesTControllerStorage.MetaVesTController_OnlyPendingAuthority(); delete st._pendingAuthority; st.authority = msg.sender; emit MetaVesTController_AuthorityUpdated(msg.sender); @@ -504,7 +411,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @dev use care in updating 'dao' as it must have the ability to call 'acceptDaoRole()' /// @param _newDao new address for pending 'dao', who must accept the role by calling 'acceptDaoRole' function initiateDaoUpdate(address _newDao) external onlyDao { - if (_newDao == address(0)) revert MetaVesTController_ZeroAddress(); + if (_newDao == address(0)) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); MetaVesTControllerStorage.getStorage()._pendingDao = _newDao; } @@ -513,7 +420,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// no 'conditionCheck' necessary as it more properly contained in 'initiateAuthorityUpdate' function acceptDaoRole() external { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if (msg.sender != st._pendingDao) revert MetaVesTController_OnlyPendingDao(); + if (msg.sender != st._pendingDao) revert MetaVesTControllerStorage.MetaVesTController_OnlyPendingDao(); delete st._pendingDao; st.dao = msg.sender; emit MetaVesTController_DaoUpdated(msg.sender); @@ -547,29 +454,18 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes calldata _callData ) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if(!doesSetExist(setName)) revert MetaVesTController_SetDoesNotExist(); - if(_callData.length!=68) revert MetaVesTController_LengthMismatch(); + + // Check: verify inputs + + if(!doesSetExist(setName)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); + if(_callData.length!=68) revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); bytes32 nameHash = keccak256(bytes(setName)); //if the majority proposal is already pending and not expired, revert if ((st.functionToSetMajorityProposal[_msgSig][nameHash].isPending && block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) || st.setMajorityVoteActive[nameHash]) - revert MetaVesTController_AmendmentAlreadyPending(); + revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][nameHash]; - proposal.isPending = true; - proposal.dataHash = keccak256(_callData[_callData.length - 32:]); - proposal.time = block.timestamp; - proposal.voters = new address[](0); - proposal.currentVotingPower = 0; + uint256 totalVotingPower = MetaVesTControllerStorage.proposeMajorityMetavestAmendment(setName, _msgSig, _callData); - uint256 totalVotingPower; - for (uint256 i; i < st.sets[nameHash].length(); ++i) { - uint256 _votingPower = BaseAllocation(st.sets[nameHash].at(i)).getMajorityVotingPower(); - totalVotingPower += _votingPower; - proposal.voterPower[st.sets[nameHash].at(i)] = _votingPower; - } - proposal.totalVotingPower = totalVotingPower; - - st.setMajorityVoteActive[nameHash] = true; emit MetaVesTController_MajorityAmendmentProposed(setName, _msgSig, _callData, totalVotingPower); } @@ -578,9 +474,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @param _msgSig function signature of the function in this controller which (if successfully executed) will execute the metavest detail update function cancelExpiredMajorityMetavestAmendment(string memory _setName, bytes4 _msgSig) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if(!doesSetExist(_setName)) revert MetaVesTController_SetDoesNotExist(); + if(!doesSetExist(_setName)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); bytes32 nameHash = keccak256(bytes(_setName)); - if (!st.setMajorityVoteActive[nameHash] || block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) revert MetaVesTController_AmendmentCannotBeCanceled(); + if (!st.setMajorityVoteActive[nameHash] || block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) revert MetaVesTControllerStorage.MetaVesTController_AmendmentCannotBeCanceled(); st.setMajorityVoteActive[nameHash] = false; } @@ -590,11 +486,11 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function voteOnMetavestAmendment(address _grant, string memory _setName, bytes4 _msgSig, bool _inFavor) external { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_setName)); - if(BaseAllocation(_grant).grantee() != msg.sender) revert MetaVesTController_OnlyGranteeMayCall(); - if (!isMetavestInSet(_grant, _setName)) revert MetaVesTController_SetDoesNotExist(); - if (!st.functionToSetMajorityProposal[_msgSig][nameHash].isPending) revert MetaVesTController_NoPendingAmendment(_msgSig, _grant); + if(BaseAllocation(_grant).grantee() != msg.sender) revert MetaVesTControllerStorage.MetaVesTController_OnlyGranteeMayCall(); + if (!isMetavestInSet(_grant, _setName)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); + if (!st.functionToSetMajorityProposal[_msgSig][nameHash].isPending) revert MetaVesTControllerStorage.MetaVesTController_NoPendingAmendment(_msgSig, _grant); if (!_checkFunctionToTokenToAmendmentTime(_msgSig, _setName)) - revert MetaVesTController_ProposedAmendmentExpired(); + revert MetaVesTControllerStorage.MetaVesTController_ProposedAmendmentExpired(); @@ -603,7 +499,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { //check if the grant has already voted. for (uint256 i; i < proposal.voters.length; ++i) { - if (proposal.voters[i] == _grant) revert MetaVesTController_AlreadyVoted(); + if (proposal.voters[i] == _grant) revert MetaVesTControllerStorage.MetaVesTController_AlreadyVoted(); } //add the msg.sender's vote if (_inFavor) { @@ -637,9 +533,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function createSet(string memory _name) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if(bytes(_name).length == 0) revert MetaVesTController_ZeroAddress(); - if (st.setNames.contains(nameHash)) revert MetaVesTController_SetAlreadyExists(); - if (bytes(_name).length > 512) revert MetaVesTController_StringTooLong(); + if(bytes(_name).length == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); + if (st.setNames.contains(nameHash)) revert MetaVesTControllerStorage.MetaVesTController_SetAlreadyExists(); + if (bytes(_name).length > 512) revert MetaVesTControllerStorage.MetaVesTController_StringTooLong(); st.setNames.add(nameHash); emit MetaVesTController_SetCreated(_name); @@ -648,8 +544,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function removeSet(string memory _name) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if (st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); - if (!st.setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); + if (!st.setNames.contains(nameHash)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); // Remove all addresses from the set starting from the last element for (uint256 i = st.sets[nameHash].length(); i > 0; i--) { @@ -697,9 +593,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function addMetaVestToSet(string memory _name, address _metaVest) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if (!st.setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); - if (isMetavestInSet(_metaVest)) revert MetaVesTController_MetaVesTAlreadyExists(); - if (st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); + if (!st.setNames.contains(nameHash)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); + if (isMetavestInSet(_metaVest)) revert MetaVesTControllerStorage.MetaVesTController_MetaVesTAlreadyExists(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); st.sets[nameHash].add(_metaVest); emit MetaVesTController_AddressAddedToSet(_name, _metaVest); @@ -708,9 +604,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function removeMetaVestFromSet(string memory _name, address _metaVest) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(_name)); - if (!st.setNames.contains(nameHash)) revert MetaVesTController_SetDoesNotExist(); - if (st.setMajorityVoteActive[nameHash]) revert MetaVesTController_AmendmentAlreadyPending(); - if (!st.sets[nameHash].contains(_metaVest)) revert MetaVestController_MetaVestNotInSet(); + if (!st.setNames.contains(nameHash)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); + if (!st.sets[nameHash].contains(_metaVest)) revert MetaVesTControllerStorage.MetaVestController_MetaVestNotInSet(); st.sets[nameHash].remove(_metaVest); emit MetaVesTController_AddressRemovedFromSet(_name, _metaVest); diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index 754ea6e..9aaa95a 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -43,6 +43,7 @@ pragma solidity 0.8.28; import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; +import {BaseAllocation, IERC20M, IConditionM} from "../BaseAllocation.sol"; import {EnumerableSet} from "../lib/EnumberableSet.sol"; import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "../lib/MetaVestDealLib.sol"; import {IAllocationFactory} from "../interfaces/IAllocationFactory.sol"; @@ -54,6 +55,8 @@ library MetaVesTControllerStorage { // Storage slot for our struct bytes32 constant internal STORAGE_POSITION = keccak256("cybercorp.metavest.controller.storage.v1"); + uint256 internal constant ARRAY_LENGTH_LIMIT = 20; + struct AmendmentProposal { bool isPending; bytes32 dataHash; @@ -112,6 +115,47 @@ library MetaVesTControllerStorage { mapping(address => bytes32) metavestAgreementIds; } + /// + /// ERRORS + /// + + error MetaVesTController_AlreadyVoted(); + error MetaVesTController_OnlyGranteeMayCall(); + error MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); + error MetaVesTController_AmendmentAlreadyPending(); + error MetaVesTController_AmendmentCannotBeCanceled(); + error MetaVesTController_AmountNotApprovedForTransferFrom(); + error MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); + error MetaVesTController_CliffGreaterThanTotal(); + error MetaVesTController_ConditionNotSatisfied(address condition); + error MetaVesTController_EmergencyUnlockNotSatisfied(); + error MetaVestController_DuplicateCondition(); + error MetaVesTController_IncorrectMetaVesTType(); + error MetaVesTController_LengthMismatch(); + error MetaVesTController_MetaVesTAlreadyExists(); + error MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); + error MetaVesTController_NoPendingAmendment(bytes4 msgSig, address affectedGrantee); + error MetaVesTController_OnlyAuthority(); + error MetaVesTController_OnlyDAO(); + error MetaVesTController_OnlyPendingAuthority(); + error MetaVesTController_OnlyPendingDao(); + error MetaVesTController_ProposedAmendmentExpired(); + error MetaVesTController_ZeroAddress(); + error MetaVesTController_ZeroAmount(); + error MetaVesTController_ZeroPrice(); + error MetaVesT_AmountNotApprovedForTransferFrom(); + error MetaVesTController_SetDoesNotExist(); + error MetaVestController_MetaVestNotInSet(); + error MetaVesTController_SetAlreadyExists(); + error MetaVesTController_StringTooLong(); + error MetaVesTController_TypeNotSupported(MetaVestType _type); + error MetaVesTController_DealAlreadyFinalized(); + error MetaVesTController_DealVoided(); + error MetaVesTController_CounterPartyNotFound(); + error MetaVesTController_PartyValuesLengthMismatch(); + error MetaVesTController_CounterPartyValueMismatch(); + error MetaVesTController_UnauthorizedToMint(); + function getStorage() internal pure returns (MetaVesTControllerData storage st) { bytes32 position = STORAGE_POSITION; assembly { @@ -119,25 +163,6 @@ library MetaVesTControllerStorage { } } - function createMetavest(bytes32 agreementId, address recipient) internal returns (address) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - - MetaVestDeal storage deal = st.deals[agreementId]; - - if(deal.metavestType == MetaVestType.Vesting) { - deal.metavest = createVestingAllocation(deal, recipient); - } else if(deal.metavestType == MetaVestType.TokenOption) { - deal.metavest = createTokenOptionAllocation(deal, recipient); - } else if(deal.metavestType == MetaVestType.RestrictedTokenAward) { - deal.metavest = createRestrictedTokenAward(deal, recipient); - } else { - return address(0); - } - st.metavestAgreementIds[deal.metavest] = agreementId; - - return deal.metavest; - } - // TODO why doesn't it need conditionCheck? function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address){ MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); @@ -241,26 +266,147 @@ library MetaVesTControllerStorage { string[] memory partyValues, bytes memory signature, string memory secret - ) external returns (address) { + ) external returns (address newMetavest, uint256 total, bytes4 error) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - // Finalize agreement + // Check: verify inputs + + MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; + if (deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) || deal.paymentToken == address(0) || deal.exercisePrice == 0) { + return (address(0), 0, MetaVesTController_ZeroAddress.selector); + } + if ( + deal.allocation.vestingCliffCredit > deal.allocation.tokenStreamTotal || + deal.allocation.unlockingCliffCredit > deal.allocation.tokenStreamTotal + ) { + return (address(0), 0, MetaVesTController_CliffGreaterThanTotal.selector); + } + uint256 milestoneTotal = 0; + if (deal.milestones.length != 0) { + if (deal.milestones.length > ARRAY_LENGTH_LIMIT) { + return (address(0), 0, MetaVesTController_LengthMismatch.selector); + } + for (uint256 i; i < deal.milestones.length; ++i) { + if (deal.milestones[i].conditionContracts.length > ARRAY_LENGTH_LIMIT) { + return (address(0), 0, MetaVesTController_LengthMismatch.selector); + } + if (deal.milestones[i].milestoneAward == 0) { + return (address(0), 0, MetaVesTController_ZeroAmount.selector); + } + milestoneTotal += deal.milestones[i].milestoneAward; + } + } + total = deal.allocation.tokenStreamTotal + milestoneTotal; + if (total == 0) { + return (address(0), 0, MetaVesTController_ZeroAmount.selector); + } + if ( + IERC20M(deal.allocation.tokenContract).allowance(st.authority, address(this)) < total || + IERC20M(deal.allocation.tokenContract).balanceOf(st.authority) < total + ) { + return (address(0), 0, MetaVesTController_AmountNotApprovedForTransferFrom.selector); + } + + // Interaction: Finalize agreement ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); - // Create and provision MetaVesT - return createMetavest(agreementId, recipient); + // Interaction: Create and provision MetaVesT + if(deal.metavestType == MetaVestType.Vesting) { + deal.metavest = createVestingAllocation(deal, recipient); + } else if(deal.metavestType == MetaVestType.TokenOption) { + deal.metavest = createTokenOptionAllocation(deal, recipient); + } else if(deal.metavestType == MetaVestType.RestrictedTokenAward) { + deal.metavest = createRestrictedTokenAward(deal, recipient); + } else { + return (address(0), 0, MetaVesTController_IncorrectMetaVesTType.selector); + } + st.metavestAgreementIds[deal.metavest] = agreementId; + + return (deal.metavest, total, 0); } - function removeFunctionCondition(address _condition, bytes4 _functionSig) external { + function proposeMajorityMetavestAmendment( + string memory setName, + bytes4 _msgSig, + bytes calldata _callData + ) external returns (uint256 totalVotingPower) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address[] storage conditions = st.functionToConditions[_functionSig]; + + bytes32 nameHash = keccak256(bytes(setName)); + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][nameHash]; + proposal.isPending = true; + proposal.dataHash = keccak256(_callData[_callData.length - 32:]); + proposal.time = block.timestamp; + proposal.voters = new address[](0); + proposal.currentVotingPower = 0; + + for (uint256 i; i < st.sets[nameHash].length(); ++i) { + uint256 _votingPower = BaseAllocation(st.sets[nameHash].at(i)).getMajorityVotingPower(); + totalVotingPower += _votingPower; + proposal.voterPower[st.sets[nameHash].at(i)] = _votingPower; + } + proposal.totalVotingPower = totalVotingPower; + + st.setMajorityVoteActive[nameHash] = true; + + return totalVotingPower; + } + + function conditionCheck() external returns (address) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + address[] memory conditions = st.functionToConditions[msg.sig]; for (uint256 i; i < conditions.length; ++i) { - if (conditions[i] == _condition) { - conditions[i] = conditions[conditions.length - 1]; - conditions.pop(); - break; + if (!IConditionM(conditions[i]).checkCondition(address(this), msg.sig, "")) { + return conditions[i]; + } + } + return address(0); + } + + function consentCheck(address _grant, bytes calldata _data) external returns (bytes4) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (isMetavestInSet(_grant)) { + bytes32 set = getSetOfMetavest(_grant); + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msg.sig][set]; + if(proposal.appliedProposalCreatedAt[_grant] == proposal.time) return MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector; + if (_data.length>32 && _data.length<69) + { + if (!proposal.isPending || proposal.totalVotingPower>proposal.currentVotingPower*2 || keccak256(_data[_data.length - 32:]) != proposal.dataHash ) { + return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; + } + } + else return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; + } else { + MetaVesTControllerStorage.AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msg.sig][_grant]; + if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { + return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; + } + } + return 0; + } + + function isMetavestInSet(address _metavest) internal view returns (bool) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + uint256 length = st.setNames.length(); + for (uint256 i = 0; i < length; i++) { + bytes32 nameHash = st.setNames.at(i); + if (st.sets[nameHash].contains(_metavest)) { + return true; + } + } + return false; + } + + function getSetOfMetavest(address _metavest) internal view returns (bytes32) { + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + uint256 length = st.setNames.length(); + for (uint256 i = 0; i < length; i++) { + bytes32 nameHash = st.setNames.at(i); + if (st.sets[nameHash].contains(_metavest)) { + return nameHash; } } + return ""; } } diff --git a/test/AuditBaseA.t.sol b/test/AuditBaseA.t.sol index 1846b52..6d7cb74 100644 --- a/test/AuditBaseA.t.sol +++ b/test/AuditBaseA.t.sol @@ -30,7 +30,7 @@ contract Audit is MetaVestControllerTest { address evil_grant = address(new EvilGrant()); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(address(evil_grant), "testSet", msgSig, true); (uint256 totalVotingPower, uint256 currentVotingPower, , , ) = controller.functionToSetMajorityProposal(msgSig, "testSet"); @@ -51,7 +51,7 @@ contract Audit is MetaVestControllerTest { vm.prank(grantee); controller.consentToMetavestAmendment(vestingAllocation, controller.removeMetavestMilestone.selector, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist.selector)); vm.prank(authority); controller.removeMetavestMilestone(vestingAllocation, 0); } diff --git a/test/AuditBaseA2.t.sol b/test/AuditBaseA2.t.sol index 11f7acb..61d6fb4 100644 --- a/test/AuditBaseA2.t.sol +++ b/test/AuditBaseA2.t.sol @@ -31,7 +31,7 @@ contract Audit is MetaVestControllerTest { address evil_grant = address(new EvilGrant()); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(address(evil_grant), "testSet", msgSig, true); (uint256 totalVotingPower, uint256 currentVotingPower, , , ) = controller.functionToSetMajorityProposal(msgSig, "testSet"); @@ -52,7 +52,7 @@ contract Audit is MetaVestControllerTest { vm.prank(grantee); controller.consentToMetavestAmendment(vestingAllocation, controller.removeMetavestMilestone.selector, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist.selector)); vm.prank(authority); controller.removeMetavestMilestone(vestingAllocation, 0); } @@ -168,29 +168,29 @@ contract Audit is MetaVestControllerTest { controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); vm.startPrank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentAlreadyPending.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending.selector)); controller.addMetaVestToSet("testSet", mockAllocation3); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentAlreadyPending.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending.selector)); controller.addMetaVestToSet("testSet", mockAllocation4); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentAlreadyPending.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending.selector)); controller.addMetaVestToSet("testSet", mockAllocation5); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentAlreadyPending.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending.selector)); controller.addMetaVestToSet("testSet", mockAllocation6); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentAlreadyPending.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending.selector)); controller.addMetaVestToSet("testSet", mockAllocation7); vm.stopPrank(); vm.startPrank(grantee); controller.voteOnMetavestAmendment(mockAllocation2, "testSet", msgSig, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(mockAllocation3, "testSet", msgSig, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(mockAllocation4, "testSet", msgSig, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(mockAllocation5, "testSet", msgSig, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(mockAllocation6, "testSet", msgSig, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(mockAllocation7, "testSet", msgSig, true); vm.stopPrank(); diff --git a/test/amendement.t.sol b/test/amendement.t.sol index 933a9eb..dac185e 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -105,7 +105,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.voteOnMetavestAmendment(mockAllocation2, "testSet", msgSig, true); vm.prank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); controller.updateMetavestTransferability(mockAllocation2, true); } @@ -277,11 +277,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); vm.prank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); controller.updateMetavestTransferability(mockAllocation2, true); vm.prank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); controller.updateMetavestTransferability(mockAllocation3, true); } @@ -314,7 +314,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.startPrank(grantee); controller.voteOnMetavestAmendment(address(vestingAllocation), "testSet", msgSig, true); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AlreadyVoted.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AlreadyVoted.selector)); controller.voteOnMetavestAmendment(address(vestingAllocation), "testSet", msgSig, true); vm.stopPrank(); } @@ -343,14 +343,14 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { function test_RevertIf_CreateDuplicateSet() public { vm.startPrank(authority); controller.createSet("duplicateSet"); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetAlreadyExists.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetAlreadyExists.selector)); controller.createSet("duplicateSet"); vm.stopPrank(); } function test_RevertIf_NonAuthorityCreateSet() public { vm.prank(grantee); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); controller.createSet("unauthorizedSet"); } @@ -494,11 +494,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.proposeMetavestAmendment(allocation, msgSig, callData); vm.prank(grantee); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(allocation, "testSet", msgSig, false); vm.prank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); controller.updateMetavestTransferability(allocation, true); } @@ -507,7 +507,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); vm.prank(grantee); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(allocation, "testSet", msgSig, true); } @@ -520,7 +520,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.proposeMetavestAmendment(allocation, msgSig, callData); vm.prank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); controller.updateMetavestTransferability(allocation, true); } @@ -533,7 +533,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.proposeMetavestAmendment(allocation, msgSig, callData); vm.prank(grantee); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(allocation, "testSet", msgSig, true); } @@ -546,7 +546,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.proposeMetavestAmendment(allocation, msgSig, callData); vm.prank(grantee); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_SetDoesNotExist.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist.selector)); controller.voteOnMetavestAmendment(allocation, "testSet", msgSig, true); } @@ -648,7 +648,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { bytes memory callData = abi.encodeWithSelector(msgSig, allocation, true); vm.prank(grantee); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_NoPendingAmendment.selector, msgSig, allocation)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_NoPendingAmendment.selector, msgSig, allocation)); controller.consentToMetavestAmendment(allocation, msgSig, true); } @@ -922,7 +922,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.proposeMetavestAmendment(allocation, msgSig, callData); vm.prank(authority); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector)); controller.setMetaVestGovVariables(allocation, BaseAllocation.GovType.vested); } } diff --git a/test/controller.t.sol b/test/controller.t.sol index 95e6084..a5a9889 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -248,7 +248,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.prank(authority); controller.updateMetavestTransferability(mockAllocation2, true);*/ - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_AmendmentAlreadyPending.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending.selector)); vm.prank(authority); controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); } @@ -277,7 +277,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { address mockAllocation2 = createDummyVestingAllocation(); vm.startPrank(authority); // controller.createSet("testSet"); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVestController_MetaVestNotInSet.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVestController_MetaVestNotInSet.selector)); controller.removeMetaVestFromSet("testSet", mockAllocation2); } @@ -394,7 +394,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.prank(authority); controller.terminateMetavestVesting(vestingAllocation); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_EmergencyUnlockNotSatisfied.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_EmergencyUnlockNotSatisfied.selector)); vm.prank(authority); controller.emergencyUpdateMetavestUnlockRate(vestingAllocation, 1e20); BaseAllocation.Allocation memory updatedAllocation = BaseAllocation(vestingAllocation).getMetavestDetails(); @@ -419,7 +419,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { BaseAllocation.Allocation memory updatedAllocation = BaseAllocation(vestingAllocation).getMetavestDetails(); assertEq(updatedAllocation.unlockRate, 0); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_EmergencyUnlockNotSatisfied.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_EmergencyUnlockNotSatisfied.selector)); vm.prank(authority); controller.emergencyUpdateMetavestUnlockRate(vestingAllocation, 1e20); updatedAllocation = BaseAllocation(vestingAllocation).getMetavestDetails(); @@ -889,7 +889,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { alice, // recipient alicePrivateKey, "Alice", - abi.encodeWithSelector(metavestController.MetaVesTController_ZeroAddress.selector) + abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_ZeroAddress.selector) ); } @@ -1291,7 +1291,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { function test_RevertIf_InitiateAuthorityUpdateNonAuthority() public { vm.prank(address(0x1234)); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); controller.initiateAuthorityUpdate(address(0x5678)); } @@ -1300,13 +1300,13 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.initiateAuthorityUpdate(address(0x5678)); vm.prank(address(0x1234)); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyPendingAuthority.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyPendingAuthority.selector)); controller.acceptAuthorityRole(); } function test_RevertIf_InitiateDaoUpdateNonDao() public { vm.prank(address(0x1234)); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyDAO.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyDAO.selector)); controller.initiateDaoUpdate(address(0x5678)); } @@ -1315,7 +1315,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.initiateDaoUpdate(address(0x5678)); vm.prank(address(0x1234)); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyPendingDao.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyPendingDao.selector)); controller.acceptDaoRole(); } @@ -1340,7 +1340,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { function test_RevertIf_UpdateFunctionConditionNonDao() public { bytes4 functionSig = bytes4(keccak256("updateMetavestStopTimes(address,uint48)")); address condition = address(0x1234); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyDAO.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyDAO.selector)); controller.updateFunctionCondition(condition, functionSig); } @@ -1381,7 +1381,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assert(controller.functionToConditions(functionSig, 0) == address(condition)); // create a dummy metavest createDummyVestingAllocation( - abi.encodeWithSelector(metavestController.MetaVesTController_ConditionNotSatisfied.selector, condition) // Expected revert + abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_ConditionNotSatisfied.selector, condition) // Expected revert ); } @@ -1401,7 +1401,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.updateFunctionCondition(address(condition), functionSig); assert(controller.functionToConditions(functionSig, 0) == address(condition)); vm.prank(dao); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVestController_DuplicateCondition.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVestController_DuplicateCondition.selector)); controller.updateFunctionCondition(address(condition), functionSig); } @@ -1565,7 +1565,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // // function test_RevertIf_PauseMintingNonAuthority() public { // // Non-authority should not be able to pause minting through controller -// vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); +// vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); // controller.pauseZkCappedMinter(); // } // @@ -1591,7 +1591,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // // function test_RevertIf_CloseMintingNonAuthority() public { // // Non-authority should not be able to close minting through controller -// vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); +// vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); // controller.closeZkCappedMinter(); // } @@ -1599,7 +1599,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // function test_RevertIf_MintUnauthorized() public { // // Should not be able to mint arbitrarily // vm.prank(alice); -// vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_UnauthorizedToMint.selector)); +// vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_UnauthorizedToMint.selector)); // controller.mint(alice, 1 ether); // } @@ -1610,7 +1610,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Upgrade to new implementation without initialization data // Non-owner should not be able to upgrade it - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); controller.upgradeToAndCall(newImplementation, ""); // Owner should be able to upgrade it From 3e305bdb911ee4b7c1a30e0e450e4c3a59a8072f Mon Sep 17 00:00:00 2001 From: detoo Date: Fri, 24 Oct 2025 14:42:27 -0700 Subject: [PATCH 06/25] wip: feat: more aggressive external library logic with error code lookup --- src/MetaVesTController.sol | 76 +++++++++++++++-------- src/storage/MetaVesTControllerStorage.sol | 14 ++++- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 8a9b23f..6b4c3ba 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -79,11 +79,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { modifier consentCheck(address _grant, bytes calldata _data) { bytes4 error = MetaVesTControllerStorage.consentCheck(_grant, _data); - if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector) { - revert MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); - } else if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector) { - revert MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - } + // This is a hack because libraries cannot emit events nor errors + _checkError(error); _; } @@ -213,15 +210,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { string memory secret ) external conditionCheck() returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - - // Check: verify inputs - - if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) revert MetaVesTControllerStorage.MetaVesTController_DealVoided(); - if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) revert MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized(); - - string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; - if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) revert MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch(); - MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; // Interaction: finalize the deal and create metavest contract @@ -234,19 +222,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { secret ); - if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAddress.selector) { - revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); - } else if (error == MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal.selector) { - revert MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal(); - } else if (error == MetaVesTControllerStorage.MetaVesTController_LengthMismatch.selector) { - revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); - } else if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAmount.selector) { - revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); - } else if (error == MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom.selector) { - revert MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom(); - } else if (error == MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType.selector) { - revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); - } + _checkError(error); // Interaction: transfer tokens to escrow safeTransferFrom(deal.allocation.tokenContract, st.authority, newMetavest, total); @@ -675,6 +651,52 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ); } + /// @notice This is a hack because libraries cannot throw errors, so it returns error codes for the core contract + /// to throw accordingly + /// @dev As inefficient as it looks, it actually saves 3000+ bytes because otherwise logic that throws errors + /// would not be able to move to external libraries + function _checkError(bytes4 error) internal { + if (error == 0) { + return; + + } else if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAddress.selector) { + revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal.selector) { + revert MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_LengthMismatch.selector) { + revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAmount.selector) { + revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom.selector) { + revert MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType.selector) { + revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector) { + revert MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector) { + revert MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_DealVoided.selector) { + revert MetaVesTControllerStorage.MetaVesTController_DealVoided(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized.selector) { + revert MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized(); + + } else if (error == MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector) { + revert MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch(); + + } else { + revert MetaVesTControllerStorage.MetaVesTController_UnknownError(error, ""); + } + } + function _authorizeUpgrade( address newImplementation ) internal virtual override onlyAuthority {} diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index 9aaa95a..4a5b15a 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -155,6 +155,7 @@ library MetaVesTControllerStorage { error MetaVesTController_PartyValuesLengthMismatch(); error MetaVesTController_CounterPartyValueMismatch(); error MetaVesTController_UnauthorizedToMint(); + error MetaVesTController_UnknownError(bytes4 error, bytes data); function getStorage() internal pure returns (MetaVesTControllerData storage st) { bytes32 position = STORAGE_POSITION; @@ -270,9 +271,20 @@ library MetaVesTControllerStorage { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // Check: verify inputs + + if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealVoided.selector); + if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized.selector); + + string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; + if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; - if (deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) || deal.paymentToken == address(0) || deal.exercisePrice == 0) { + + if ( + deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) + || (deal.metavestType == MetaVestType.TokenOption && deal.paymentToken == address(0) && deal.exercisePrice == 0) + || (deal.metavestType == MetaVestType.RestrictedTokenAward && deal.paymentToken == address(0) && deal.exercisePrice == 0) + ) { return (address(0), 0, MetaVesTController_ZeroAddress.selector); } if ( From 26d23e963e695361c4726ccd1feae4a4d86e3294 Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 26 Oct 2025 09:50:36 -0700 Subject: [PATCH 07/25] fix: msg.sig on external modifiers --- src/MetaVesTController.sol | 4 ++-- src/storage/MetaVesTControllerStorage.sol | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 6b4c3ba..7ab2bea 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -70,7 +70,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// modifier conditionCheck() { - address failedCondition = MetaVesTControllerStorage.conditionCheck(); + address failedCondition = MetaVesTControllerStorage.conditionCheck(msg.sig); if (failedCondition != address(0)) { revert MetaVesTControllerStorage.MetaVesTController_ConditionNotSatisfied(failedCondition); } @@ -78,7 +78,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } modifier consentCheck(address _grant, bytes calldata _data) { - bytes4 error = MetaVesTControllerStorage.consentCheck(_grant, _data); + bytes4 error = MetaVesTControllerStorage.consentCheck(msg.sig, _grant, _data); // This is a hack because libraries cannot emit events nor errors _checkError(error); _; diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index 4a5b15a..bc3f9dd 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -365,22 +365,22 @@ library MetaVesTControllerStorage { return totalVotingPower; } - function conditionCheck() external returns (address) { + function conditionCheck(bytes4 msgSig) external returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address[] memory conditions = st.functionToConditions[msg.sig]; + address[] memory conditions = st.functionToConditions[msgSig]; for (uint256 i; i < conditions.length; ++i) { - if (!IConditionM(conditions[i]).checkCondition(address(this), msg.sig, "")) { + if (!IConditionM(conditions[i]).checkCondition(address(this), msgSig, "")) { return conditions[i]; } } return address(0); } - function consentCheck(address _grant, bytes calldata _data) external returns (bytes4) { + function consentCheck(bytes4 msgSig, address _grant, bytes calldata _data) external returns (bytes4) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if (isMetavestInSet(_grant)) { bytes32 set = getSetOfMetavest(_grant); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msg.sig][set]; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msgSig][set]; if(proposal.appliedProposalCreatedAt[_grant] == proposal.time) return MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector; if (_data.length>32 && _data.length<69) { @@ -390,7 +390,7 @@ library MetaVesTControllerStorage { } else return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; } else { - MetaVesTControllerStorage.AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msg.sig][_grant]; + MetaVesTControllerStorage.AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msgSig][_grant]; if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; } From e10a8f831ecbf2e375d02d0be99a6baed844fc33 Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 26 Oct 2025 10:21:34 -0700 Subject: [PATCH 08/25] wip: feat: re-enable factories for token options and restricted tokens --- scripts/deployYearnBorgCompensation.s.sol | 4 +- .../lib/YearnBorgCompensation2025_2026.sol | 6 + .../YearnBorgCompensationSepolia2025_2026.sol | 4 + src/MetaVesTController.sol | 10 +- src/RestrictedTokenAllocation.sol | 233 ++++++++++++++++++ src/RestrictedTokenFactory.sol | 26 ++ src/TokenOptionAllocation.sol | 224 +++++++++++++++++ src/TokenOptionFactory.sol | 26 ++ test/amendement.t.sol | 4 +- test/controller.t.sol | 14 +- test/lib/MetaVesTControllerTestBase.sol | 6 +- 11 files changed, 544 insertions(+), 13 deletions(-) create mode 100644 src/RestrictedTokenAllocation.sol create mode 100644 src/RestrictedTokenFactory.sol create mode 100644 src/TokenOptionAllocation.sol create mode 100644 src/TokenOptionFactory.sol diff --git a/scripts/deployYearnBorgCompensation.s.sol b/scripts/deployYearnBorgCompensation.s.sol index 7935855..e585345 100644 --- a/scripts/deployYearnBorgCompensation.s.sol +++ b/scripts/deployYearnBorgCompensation.s.sol @@ -63,7 +63,9 @@ contract DeployYearnBorgCompensationScript is SafeTxHelper, Script { address(config.borgSafe), address(config.borgSafe), address(config.registry), - address(config.vestingAllocationFactory) + address(config.vestingAllocationFactory), + address(config.tokenOptionFactory), + address(config.restrictedTokenFactory) ) ))); diff --git a/scripts/lib/YearnBorgCompensation2025_2026.sol b/scripts/lib/YearnBorgCompensation2025_2026.sol index eaf4327..aa4b60b 100644 --- a/scripts/lib/YearnBorgCompensation2025_2026.sol +++ b/scripts/lib/YearnBorgCompensation2025_2026.sol @@ -7,6 +7,8 @@ import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementReg import {IGnosisSafe} from "../../test/lib/safe.sol"; import {BaseAllocation} from "../../src/BaseAllocation.sol"; import {VestingAllocationFactory} from "../../src/VestingAllocationFactory.sol"; +import {TokenOptionFactory} from "../../src/TokenOptionFactory.sol"; +import {RestrictedTokenFactory} from "../../src/RestrictedTokenFactory.sol"; import {metavestController} from "../../src/MetaVesTController.sol"; library YearnBorgCompensation2025_2026 { @@ -28,6 +30,8 @@ library YearnBorgCompensation2025_2026 { IGnosisSafe metalexSafe; CyberAgreementRegistry registry; VestingAllocationFactory vestingAllocationFactory; + TokenOptionFactory tokenOptionFactory; + RestrictedTokenFactory restrictedTokenFactory; metavestController controller; // Yearn BORG Director Compensation Agreement (one template per director for now) @@ -82,6 +86,8 @@ library YearnBorgCompensation2025_2026 { metalexSafe: metalexSafe, registry: CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134), vestingAllocationFactory: VestingAllocationFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD + tokenOptionFactory: TokenOptionFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD + restrictedTokenFactory: RestrictedTokenFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD controller: metavestController(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD // Yearn BORG Compensation Agreement diff --git a/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol b/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol index b8de1d6..9046a4a 100644 --- a/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol +++ b/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol @@ -7,6 +7,8 @@ import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementReg import {IGnosisSafe} from "../../test/lib/safe.sol"; import {BaseAllocation} from "../../src/BaseAllocation.sol"; import {VestingAllocationFactory} from "../../src/VestingAllocationFactory.sol"; +import {TokenOptionFactory} from "../../src/TokenOptionFactory.sol"; +import {RestrictedTokenFactory} from "../../src/RestrictedTokenFactory.sol"; import {metavestController} from "../../src/MetaVesTController.sol"; import {YearnBorgCompensation2025_2026} from "./YearnBorgCompensation2025_2026.sol"; @@ -36,6 +38,8 @@ library YearnBorgCompensationSepolia2025_2026 { metalexSafe: metalexSafe, registry: CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134), vestingAllocationFactory: VestingAllocationFactory(0x87dC5e3FBFE8B5F2B74C64eE34da8bdc9fedCb0f), + tokenOptionFactory: TokenOptionFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD + restrictedTokenFactory: RestrictedTokenFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD controller: metavestController(0xFa5Ab18bD5E02B1d6430e91C32C5CB5e7F43bB65), // Yearn BORG Compensation Agreement diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 7ab2bea..c3eaf6b 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -101,9 +101,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address _authority, address _dao, address _registry, - address _vestingFactory -// address _tokenOptionFactory, -// address _restrictedTokenFactory + address _vestingFactory, + address _tokenOptionFactory, + address _restrictedTokenFactory ) public initializer { __UUPSUpgradeable_init(); @@ -113,8 +113,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { st.authority = _authority; st.registry = _registry; st.vestingFactory = _vestingFactory; -// tokenOptionFactory = _tokenOptionFactory; -// restrictedTokenFactory = _restrictedTokenFactory; + st.tokenOptionFactory = _tokenOptionFactory; + st.restrictedTokenFactory = _restrictedTokenFactory; st.dao = _dao; } diff --git a/src/RestrictedTokenAllocation.sol b/src/RestrictedTokenAllocation.sol new file mode 100644 index 0000000..e23f251 --- /dev/null +++ b/src/RestrictedTokenAllocation.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: AGPL-3.0-only +import "./BaseAllocation.sol"; + +pragma solidity ^0.8.24; + +contract RestrictedTokenAward is BaseAllocation { + + /// @notice address of payment token used for token option exercises or restricted token repurchases + address public immutable paymentToken; + uint256 public shortStopDuration; + uint256 public shortStopDate; + uint256 public repurchasePrice; + uint256 public tokensRepurchased; + uint256 public tokensRepurchasedWithdrawn; + + /// @notice Constructor to deploy a new RestrictedTokenAward + /// @param _grantee - address of the grantee + /// @param _controller - address of the controller + /// @param _paymentToken - address of the payment token + /// @param _repurchasePrice - price at which the restricted tokens can be repurchased in vesting token decimals but only up to payment decimal precision + /// @param _shortStopDuration - duration after termination during which restricted tokens can be repurchased + /// @param _allocation - allocation details as an Allocation struct + /// @param _milestones - milestones with their conditions and awards + constructor ( + address _grantee, + address _recipient, // TODO review needed + address _controller, + address _paymentToken, + uint256 _repurchasePrice, + uint256 _shortStopDuration, + Allocation memory _allocation, + Milestone[] memory _milestones + ) BaseAllocation( + _grantee, + _recipient, + _controller + ) { + //perform input validation + if (_allocation.tokenContract == address(0)) revert MetaVesT_ZeroAddress(); + if (_allocation.tokenStreamTotal == 0) revert MetaVesT_ZeroAmount(); + if (_allocation.vestingRate > 1000*1e18 || _allocation.unlockRate > 1000*1e18) revert MetaVesT_RateTooHigh(); + + //set vesting allocation variables + allocation.tokenContract = _allocation.tokenContract; + allocation.tokenStreamTotal = _allocation.tokenStreamTotal; + allocation.vestingCliffCredit = _allocation.vestingCliffCredit; + allocation.unlockingCliffCredit = _allocation.unlockingCliffCredit; + allocation.vestingRate = _allocation.vestingRate; + allocation.vestingStartTime = _allocation.vestingStartTime; + allocation.unlockRate = _allocation.unlockRate; + allocation.unlockStartTime = _allocation.unlockStartTime; + + // set token option variables + repurchasePrice = _repurchasePrice; + shortStopDuration = _shortStopDuration; + + paymentToken = _paymentToken; + + // manually copy milestones + for (uint256 i; i < _milestones.length; ++i) { + milestones.push(_milestones[i]); + } + } + + /// @notice returns the vesting type for RestrictedTokenAward + /// @return uint256 type 3 + function getVestingType() external pure override returns (uint256) { + return 3; + } + + /// @notice returns the governing power for RestrictedTokenAward based on the govType + /// @return uint256 governingPower for this RestrictedTokenAward contract + function getGoverningPower() external view override returns (uint256) { + uint256 governingPower; + if(govType==GovType.all) + { + uint256 totalMilestoneAward = 0; + for(uint256 i; i < milestones.length; ++i) + { + totalMilestoneAward += milestones[i].milestoneAward; + } + governingPower = (allocation.tokenStreamTotal + totalMilestoneAward) - tokensWithdrawn; + } + else if(govType==GovType.vested) + governingPower = getVestedTokenAmount() - tokensWithdrawn; + else + governingPower = _min(getVestedTokenAmount(), getUnlockedTokenAmount()) - tokensWithdrawn; + + return governingPower; + } + + /// @notice updates the short stop time of the vesting contract + /// @dev onlyController -- must be called from the metavest controller + /// @param _shortStopTime - new short stop time to be set in seconds + function updateStopTimes(uint48 _shortStopTime) external override onlyController { + if(terminated) revert MetaVesT_AlreadyTerminated(); + shortStopDuration = _shortStopTime; + emit MetaVesT_StopTimesUpdated(grantee, _shortStopTime); + } + + /// @notice updates the exercise price + /// @dev onlyController -- must be called from the metavest controller + /// @param _newPrice - the new exercise price in vesting token decimals but only up to payment decimal precision + function updatePrice(uint256 _newPrice) external onlyController { + if(terminated) revert MetaVesT_AlreadyTerminated(); + repurchasePrice = _newPrice; + emit MetaVesT_PriceUpdated(grantee, _newPrice); + } + + /// @notice gets the payment amount for a given amount of tokens + /// @param _amount - the amount of tokens to calculate the payment amount in the vesting token decimals + /// @return paymentAmount - the payment amount for the given token amount in the payment token decimals + function getPaymentAmount(uint256 _amount) public view returns (uint256) { + uint8 paymentDecimals = IERC20M(paymentToken).decimals(); + uint8 repurchaseTokenDecimals = IERC20M(allocation.tokenContract).decimals(); + + // Calculate paymentAmount + uint256 paymentAmount; + paymentAmount = _amount * repurchasePrice / (10**repurchaseTokenDecimals); + + //scale paymentAmount to payment token decimals + if(paymentDecimals getAmountRepurchasable()) revert MetaVesT_MoreThanAvailable(); + if(block.timestampIERC20M(allocation.tokenContract).balanceOf(address(this))) + repurchaseAmount = IERC20M(allocation.tokenContract).balanceOf(address(this)); + return repurchaseAmount; + } + + /// @notice returns the amount of tokens that are vested + /// @return uint256 amount of tokens that are vested + function getVestedTokenAmount() public view returns (uint256) { + if(block.timestampallocation.tokenStreamTotal) + _tokensVested = allocation.tokenStreamTotal; + return _tokensVested += milestoneAwardTotal; + } + + /// @notice returns the amount of tokens that are unlocked + /// @return uint256 amount of tokens that are unlocked + function getUnlockedTokenAmount() public view returns (uint256) { + if(block.timestampallocation.tokenStreamTotal + milestoneAwardTotal) + _tokensUnlocked = allocation.tokenStreamTotal + milestoneAwardTotal; + + return _tokensUnlocked += milestoneUnlockedTotal; + } + + /// @notice returns the amount of tokens that can be withdrawn + /// @return uint256 amount of tokens that can be withdrawn + function getAmountWithdrawable() public view override returns (uint256) { + uint256 _tokensVested = getVestedTokenAmount(); + uint256 _tokensUnlocked = getUnlockedTokenAmount(); + uint256 withdrawableAmount = _min(_tokensVested, _tokensUnlocked); + if(withdrawableAmount>tokensWithdrawn) + return withdrawableAmount - tokensWithdrawn; + else + return 0; + } + +} diff --git a/src/RestrictedTokenFactory.sol b/src/RestrictedTokenFactory.sol new file mode 100644 index 0000000..614bf0a --- /dev/null +++ b/src/RestrictedTokenFactory.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.24; + +import "./RestrictedTokenAllocation.sol"; +import "./interfaces/IAllocationFactory.sol"; + +contract RestrictedTokenFactory is IAllocationFactory { + + function createAllocation( + AllocationType _allocationType, + address _grantee, + address _recipient, // TODO review needed + address _controller, + RestrictedTokenAward.Allocation memory _allocation, + RestrictedTokenAward.Milestone[] memory _milestones, + address _paymentToken, + uint256 _exercisePrice, + uint256 _shortStopDuration + ) external returns (address) { + if (_allocationType == AllocationType.RestrictedToken) { + return address(new RestrictedTokenAward(_grantee, _recipient, _controller, _paymentToken, _exercisePrice, _shortStopDuration, _allocation, _milestones)); + } else { + revert("AllocationFactory: invalid allocation type"); + } + } +} diff --git a/src/TokenOptionAllocation.sol b/src/TokenOptionAllocation.sol new file mode 100644 index 0000000..99d52a5 --- /dev/null +++ b/src/TokenOptionAllocation.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: AGPL-3.0-only +import "./BaseAllocation.sol"; + +pragma solidity ^0.8.24; + +contract TokenOptionAllocation is BaseAllocation { + + /// @notice address of payment token used for token option exercises or restricted token repurchases + address public immutable paymentToken; + uint256 public tokensExercised; + uint256 public exercisePrice; + uint256 public shortStopDuration; + uint256 public shortStopTime; + + event MetaVesT_TokenOptionExercised(address indexed _grantee, uint256 _tokensToExercise, uint256 _paymentAmount); + + /// @notice Constructor to create a TokenOptionAllocation + /// @param _grantee - address of the grantee + /// @param _controller - address of the controller + /// @param _paymentToken - address of the payment token + /// @param _exercisePrice - price of the token option exercise in vesting token decimals but only up to payment decimal precision + /// @param _shortStopDuration - duration of the short stop + /// @param _allocation - allocation details as an Allocation struct + /// @param _milestones - milestones with conditions and awards + constructor ( + address _grantee, + address _recipient, // TODO review needed + address _controller, + address _paymentToken, + uint256 _exercisePrice, + uint256 _shortStopDuration, + Allocation memory _allocation, + Milestone[] memory _milestones + ) BaseAllocation( + _grantee, + _recipient, + _controller + ) { + //perform input validation + if (_allocation.tokenContract == address(0)) revert MetaVesT_ZeroAddress(); + if (_allocation.tokenStreamTotal == 0) revert MetaVesT_ZeroAmount(); + if (_allocation.vestingRate > 1000*1e18 || _allocation.unlockRate > 1000*1e18) revert MetaVesT_RateTooHigh(); + + //set vesting allocation variables + allocation.tokenContract = _allocation.tokenContract; + allocation.tokenStreamTotal = _allocation.tokenStreamTotal; + allocation.vestingCliffCredit = _allocation.vestingCliffCredit; + allocation.unlockingCliffCredit = _allocation.unlockingCliffCredit; + allocation.vestingRate = _allocation.vestingRate; + allocation.vestingStartTime = _allocation.vestingStartTime; + allocation.unlockRate = _allocation.unlockRate; + allocation.unlockStartTime = _allocation.unlockStartTime; + + // set token option variables + exercisePrice = _exercisePrice; + shortStopDuration = _shortStopDuration; + + paymentToken = _paymentToken; + + // manually copy milestones + for (uint256 i; i < _milestones.length; ++i) { + milestones.push(_milestones[i]); + } + } + + /// @notice returns the contract vesting type 2 for TokenOptionAllocation + /// @return 2 + function getVestingType() external pure override returns (uint256) { + return 2; + } + + /// @notice returns the governing power of the TokenOptionAllocation + /// @return governingPower - the governing power of the TokenOptionAllocation based on the governance setting + function getGoverningPower() external view override returns (uint256) { + uint256 governingPower; + if(govType==GovType.all) + { + uint256 totalMilestoneAward = 0; + for(uint256 i; i < milestones.length; ++i) + { + totalMilestoneAward += milestones[i].milestoneAward; + } + governingPower = (allocation.tokenStreamTotal + totalMilestoneAward) - tokensWithdrawn; + } + else if(govType==GovType.vested) + governingPower = tokensExercised - tokensWithdrawn; + else + governingPower = _min(tokensExercised, getUnlockedTokenAmount()) - tokensWithdrawn; + + return governingPower; + } + + /// @notice updates the short stop time of the TokenOptionAllocation + /// @dev onlyController -- must be called from the metavest controller + /// @param _shortStopTime - the new short stop time + function updateStopTimes(uint48 _shortStopTime) external override onlyController { + if(terminated) revert MetaVesT_AlreadyTerminated(); + shortStopDuration = _shortStopTime; + emit MetaVesT_StopTimesUpdated(grantee, _shortStopTime); + } + + /// @notice updates the exercise price + /// @dev onlyController -- must be called from the metavest controller + /// @param _newPrice - the new exercise price in vesting token decimals but only up to payment decimal precision + function updatePrice(uint256 _newPrice) external onlyController { + if(terminated) revert MetaVesT_AlreadyTerminated(); + exercisePrice = _newPrice; + emit MetaVesT_PriceUpdated(grantee, _newPrice); + } + + /// @notice gets the payment amount for a given amount of tokens + /// @param _amount - the amount of tokens to calculate the payment amount in the vesting token decimals + /// @return paymentAmount - the payment amount for the given token amount in the payment token decimals + function getPaymentAmount(uint256 _amount) public view returns (uint256) { + uint8 paymentDecimals = IERC20M(paymentToken).decimals(); + uint8 exerciseTokenDecimals = IERC20M(allocation.tokenContract).decimals(); + + // Calculate paymentAmount + uint256 paymentAmount; + paymentAmount = _amount * exercisePrice / (10**exerciseTokenDecimals); + + //scale paymentAmount to payment token decimals + if(paymentDecimalsshortStopTime && terminated) revert MetaVest_ShortStopDatePassed(); + if (_tokensToExercise == 0) revert MetaVesT_ZeroAmount(); + if (_tokensToExercise > getAmountExercisable()) revert MetaVesT_MoreThanAvailable(); + + // Calculate paymentAmount + uint256 paymentAmount = getPaymentAmount(_tokensToExercise); + if(paymentAmount == 0) revert MetaVesT_TooSmallAmount(); + + safeTransferFrom(paymentToken, msg.sender, getAuthority(), paymentAmount); + tokensExercised += _tokensToExercise; + emit MetaVesT_TokenOptionExercised(msg.sender, _tokensToExercise, paymentAmount); + } + + /// @notice Allows the controller to terminate the TokenOptionAllocation + /// @dev onlyController -- must be called from the metavest controller + function terminate() external override onlyController nonReentrant { + if(terminated) revert MetaVesT_AlreadyTerminated(); + + uint256 milestonesAllocation = 0; + for (uint256 i; i < milestones.length; ++i) { + milestonesAllocation += milestones[i].milestoneAward; + } + uint256 tokensToRecover = allocation.tokenStreamTotal + milestonesAllocation - getAmountExercisable() - tokensExercised; + terminationTime = block.timestamp; + shortStopTime = block.timestamp + shortStopDuration; + safeTransfer(allocation.tokenContract, getAuthority(), tokensToRecover); + terminated = true; + emit MetaVesT_Terminated(grantee, tokensToRecover); + } + + /// @notice recovers any forfeited tokens after the short stop time + /// @dev onlyAuthority -- must be called from the authority + function recoverForfeitTokens() external onlyAuthority nonReentrant { + if(block.timestamp tokensExercised - tokensWithdrawn) + tokensToRecover = IERC20M(allocation.tokenContract).balanceOf(address(this)) + tokensWithdrawn - tokensExercised; + safeTransfer(allocation.tokenContract, getAuthority(), tokensToRecover); + } + + /// @notice gets the amount of tokens available for a grantee to exercise + /// @return uint256 amount of tokens available for the grantee to exercise + function getAmountExercisable() public view returns (uint256) { + if(block.timestampshortStopTime && shortStopTime>0)) + return 0; + + uint256 _timeElapsedSinceVest = block.timestamp - allocation.vestingStartTime; + if(terminated) + _timeElapsedSinceVest = terminationTime - allocation.vestingStartTime; + + uint256 _tokensVested = (_timeElapsedSinceVest * allocation.vestingRate) + allocation.vestingCliffCredit; + + if(_tokensVested>allocation.tokenStreamTotal) + _tokensVested = allocation.tokenStreamTotal; + uint256 _tokensExercisable = _tokensVested + milestoneAwardTotal; + if(_tokensExercisable>tokensExercised) + return _tokensExercisable - tokensExercised; + else + return 0; + } + + /// @notice gets the amount of tokens unlocked for a grantee + /// @return uint256 amount of tokens unlocked for the grantee + function getUnlockedTokenAmount() public view returns (uint256) { + if(block.timestampallocation.tokenStreamTotal + milestoneAwardTotal) + _tokensUnlocked = allocation.tokenStreamTotal + milestoneAwardTotal; + + return _tokensUnlocked + milestoneUnlockedTotal; + } + + /// @notice gets the amount of tokens available for a grantee to withdraw + /// @return uint256 amount of tokens available for the grantee to withdraw + function getAmountWithdrawable() public view override returns (uint256) { + uint256 _tokensUnlocked = getUnlockedTokenAmount(); + + uint256 withdrawableAmount = _min(tokensExercised, _tokensUnlocked); + if(withdrawableAmount>tokensWithdrawn) + return withdrawableAmount - tokensWithdrawn; + else + return 0; + } + +} diff --git a/src/TokenOptionFactory.sol b/src/TokenOptionFactory.sol new file mode 100644 index 0000000..9e9d67d --- /dev/null +++ b/src/TokenOptionFactory.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.24; + +import "./TokenOptionAllocation.sol"; +import "./interfaces/IAllocationFactory.sol"; + +contract TokenOptionFactory is IAllocationFactory { + + function createAllocation( + AllocationType _allocationType, + address _grantee, + address _recipient, // TODO review needed + address _controller, + TokenOptionAllocation.Allocation memory _allocation, + TokenOptionAllocation.Milestone[] memory _milestones, + address _paymentToken, + uint256 _exercisePrice, + uint256 _shortStopDuration + ) external returns (address) { + if (_allocationType == AllocationType.TokenOption) { + return address(new TokenOptionAllocation(_grantee, _recipient, _controller, _paymentToken, _exercisePrice, _shortStopDuration, _allocation, _milestones)); + } else { + revert("AllocationFactory: invalid allocation type"); + } + } +} diff --git a/test/amendement.t.sol b/test/amendement.t.sol index dac185e..f9fa740 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -40,7 +40,9 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { guardianSafe, guardianSafe, address(registry), - address(vestingAllocationFactory) + address(vestingAllocationFactory), + address(tokenOptionFactory), + address(restrictedTokenFactory) ) ))); diff --git a/test/controller.t.sol b/test/controller.t.sol index a5a9889..f813054 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.20; -//import "../src/RestrictedTokenAllocation.sol"; -//import "../src/RestrictedTokenFactory.sol"; -//import "../src/TokenOptionAllocation.sol"; -//import "../src/TokenOptionFactory.sol"; +import "../src/RestrictedTokenAllocation.sol"; +import "../src/RestrictedTokenFactory.sol"; +import "../src/TokenOptionAllocation.sol"; +import "../src/TokenOptionFactory.sol"; import "../src/VestingAllocation.sol"; import "../src/VestingAllocationFactory.sol"; import "../src/interfaces/IAllocationFactory.sol"; @@ -33,6 +33,8 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Deploy MetaVesT controller vestingAllocationFactory = new VestingAllocationFactory(); + tokenOptionFactory = new TokenOptionFactory(); + restrictedTokenFactory = new RestrictedTokenFactory(); controller = metavestController(address(new ERC1967Proxy{salt: salt}( address(new metavestController{salt: salt}()), @@ -41,7 +43,9 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { guardianSafe, guardianSafe, address(registry), - address(vestingAllocationFactory) + address(vestingAllocationFactory), + address(tokenOptionFactory), + address(restrictedTokenFactory) ) ))); diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 56c3a66..62828c9 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -3,12 +3,14 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; import "../../src/MetaVesTController.sol"; -import "../../src/VestingAllocationFactory.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {BorgAuth} from "cybercorps-contracts/src/libs/auth.sol"; import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; import {CyberAgreementUtils} from "cybercorps-contracts/test/libs/CyberAgreementUtils.sol"; import {MockERC20} from "../mocks/MockERC20.sol"; +import {VestingAllocationFactory} from "../../src/VestingAllocationFactory.sol"; +import {TokenOptionFactory} from "../../src/TokenOptionFactory.sol"; +import {RestrictedTokenFactory} from "../../src/RestrictedTokenFactory.sol"; import {MetaVestDealLib, MetaVestDeal} from "../../src/lib/MetaVestDealLib.sol"; contract MetaVesTControllerTestBase is Test { @@ -39,6 +41,8 @@ contract MetaVesTControllerTestBase is Test { CyberAgreementRegistry registry; VestingAllocationFactory vestingAllocationFactory; + TokenOptionFactory tokenOptionFactory; + RestrictedTokenFactory restrictedTokenFactory; metavestController controller; From 971f857b76d4ebfcc7ddd98239b08ff6de9197da Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 26 Oct 2025 14:27:34 -0700 Subject: [PATCH 09/25] wip: test: re-enable tests for token options and restricted tokens --- src/MetaVesTController.sol | 3 +- src/lib/MetaVestDealLib.sol | 43 ++- test/controller.t.sol | 390 ++++++++++++++---------- test/lib/MetaVesTControllerTestBase.sol | 53 +++- 4 files changed, 302 insertions(+), 187 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index c3eaf6b..a7308d4 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -266,7 +266,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) external onlyAuthority conditionCheck consentCheck(_grant, msg.data) { if (_newPrice == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroPrice(); IPriceAllocation grant = IPriceAllocation(_grant); - if(grant.getVestingType()!=2 && grant.getVestingType()!=3) revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); + // TODO review needed: it is supposed to be TokenOption, RestrictedTokenAward right? + if (grant.getVestingType() != uint256(MetaVestType.TokenOption) && grant.getVestingType() != uint256(MetaVestType.RestrictedTokenAward)) revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); _resetAmendmentParams(_grant, msg.sig); grant.updatePrice(_newPrice); } diff --git a/src/lib/MetaVestDealLib.sol b/src/lib/MetaVestDealLib.sol index 4ce998b..ba68706 100644 --- a/src/lib/MetaVestDealLib.sol +++ b/src/lib/MetaVestDealLib.sol @@ -52,7 +52,6 @@ struct MetaVestDeal { address paymentToken; uint256 exercisePrice; uint256 shortStopDuration; - uint256 longStopDate; BaseAllocation.Allocation allocation; BaseAllocation.Milestone[] milestones; address metavest; @@ -78,4 +77,46 @@ library MetaVestDealLib { deal.milestones = milestones; return deal; } + + /// @notice Partially fill the given deal struct + /// @dev Beware of which fields are not filled and using default values + function setTokenOption( + MetaVestDeal memory deal, + address grantee, + address paymentToken, + uint256 exercisePrice, + uint256 shortStopDuration, + BaseAllocation.Allocation memory allocation, + BaseAllocation.Milestone[] memory milestones + ) internal pure returns (MetaVestDeal memory) { + deal.metavestType = MetaVestType.TokenOption; + deal.grantee = grantee; + deal.paymentToken = paymentToken; + deal.exercisePrice = exercisePrice; + deal.shortStopDuration = shortStopDuration; + deal.allocation = allocation; + deal.milestones = milestones; + return deal; + } + + /// @notice Partially fill the given deal struct + /// @dev Beware of which fields are not filled and using default values + function setRestrictedToken( + MetaVestDeal memory deal, + address grantee, + address paymentToken, + uint256 exercisePrice, + uint256 shortStopDuration, + BaseAllocation.Allocation memory allocation, + BaseAllocation.Milestone[] memory milestones + ) internal pure returns (MetaVestDeal memory) { + deal.metavestType = MetaVestType.TokenOption; + deal.grantee = grantee; + deal.paymentToken = paymentToken; + deal.exercisePrice = exercisePrice; + deal.shortStopDuration = shortStopDuration; + deal.allocation = allocation; + deal.milestones = milestones; + return deal; + } } diff --git a/test/controller.t.sol b/test/controller.t.sol index f813054..1892b4c 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -14,6 +14,7 @@ import {ERC1967ProxyLib} from "./lib/ERC1967ProxyLib.sol"; contract MetaVestControllerTest is MetaVesTControllerTestBase { using ERC1967ProxyLib for address; + using MetaVestDealLib for MetaVestDeal; address authority = guardianSafe; address dao = guardianSafe; @@ -105,78 +106,97 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { _granteeWithdrawAndAsserts(vestingAllocationAlice, 60 ether, "Alice full"); } -// function testCreateTokenOptionAllocation() public { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(paymentToken), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 100e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// address tokenOptionAllocation = controller.createMetavest( -// MetaVestType.TokenOption, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 365 days, -// 0 -// ); -// -// assertEq(paymentToken.balanceOf(address(tokenOptionAllocation)), 0, "Vesting contract should not have any token (it mints on-demand)"); -// //assertEq(controller.tokenOptionAllocations(grantee, 0), tokenOptionAllocation); -// } + function testCreateTokenOptionAllocation() public { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 100e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); -// function testCreateRestrictedTokenAward() public { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(token), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 100e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 1100e18); -// -// address restrictedTokenAward = controller.createMetavest( -// MetaVestType.RestrictedTokenAward, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 365 days, -// 0 -// -// ); -// -// assertEq(token.balanceOf(restrictedTokenAward), 1100e18); -// //assertEq(controller.restrictedTokenAllocations(grantee, 0), restrictedTokenAward); -// } + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setTokenOption( + alice, + address(paymentToken), // TODO try a different token + 1e18, // exercisePrice + 365 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(paymentToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + "" + ); + + TokenOptionAllocation metavestAlice = TokenOptionAllocation(_granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + )); + + assertEq(paymentToken.balanceOf(address(metavestAlice)), 1100e18, "Vesting contract should have token in escrow"); + } + + function testCreateRestrictedTokenAward() public { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 100e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setTokenOption( + alice, + address(paymentToken), // TODO try a different token + 1e18, // exercisePrice + 365 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(paymentToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + "" + ); + + RestrictedTokenAward metavestAlice = RestrictedTokenAward(_granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + )); + + assertEq(paymentToken.balanceOf(address(metavestAlice)), 1100e18); + } function testUpdateTransferability() public { uint256 startTimestamp = block.timestamp; @@ -286,22 +306,24 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { } -// function testUpdateExercisePrice() public { -// address tokenOptionAllocation = createDummyTokenOptionAllocation(); -// -// //compute msg.data for updateExerciseOrRepurchasePrice(tokenOptionAllocation, 2e18) -// bytes4 selector = controller.updateExerciseOrRepurchasePrice.selector; -// bytes memory msgData = abi.encodeWithSelector(selector, tokenOptionAllocation, 2e18); -// -// controller.proposeMetavestAmendment(tokenOptionAllocation, controller.updateExerciseOrRepurchasePrice.selector, msgData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(tokenOptionAllocation, controller.updateExerciseOrRepurchasePrice.selector, true); -// -// controller.updateExerciseOrRepurchasePrice(tokenOptionAllocation, 2e18); -// -// assertEq(TokenOptionAllocation(tokenOptionAllocation).exercisePrice(), 2e18); -// } + function testUpdateExercisePrice() public { + address tokenOptionAllocation = createDummyTokenOptionAllocation(); + + //compute msg.data for updateExerciseOrRepurchasePrice(tokenOptionAllocation, 2e18) + bytes4 selector = controller.updateExerciseOrRepurchasePrice.selector; + bytes memory msgData = abi.encodeWithSelector(selector, tokenOptionAllocation, 2e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(tokenOptionAllocation, controller.updateExerciseOrRepurchasePrice.selector, msgData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(tokenOptionAllocation, controller.updateExerciseOrRepurchasePrice.selector, true); + + vm.prank(authority); + controller.updateExerciseOrRepurchasePrice(tokenOptionAllocation, 2e18); + + assertEq(TokenOptionAllocation(tokenOptionAllocation).exercisePrice(), 2e18); + } function testRemoveMilestone() public { address vestingAllocation = createDummyVestingAllocation(); @@ -447,20 +469,24 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertEq(updatedAllocation.vestingRate, 20e18); } -// function testUpdateStopTimes() public { -// -// address vestingAllocation = createDummyRestrictedTokenAward(); -// address[] memory addresses = new address[](1); -// addresses[0] = vestingAllocation; -// bytes4 selector = bytes4(keccak256("updateMetavestStopTimes(address,uint48)")); -// bytes memory msgData = abi.encodeWithSelector(selector, vestingAllocation, uint48(block.timestamp + 500 days)); -// controller.proposeMetavestAmendment(vestingAllocation, controller.updateMetavestStopTimes.selector, msgData); -// vm.prank(grantee); -// controller.consentToMetavestAmendment(vestingAllocation, controller.updateMetavestStopTimes.selector, true); -// uint48 newShortStopTime = uint48(block.timestamp + 500 days); -// -// controller.updateMetavestStopTimes(vestingAllocation, newShortStopTime); -// } + function testUpdateStopTimes() public { + + address metavest = createDummyRestrictedTokenAward(); + address[] memory addresses = new address[](1); + addresses[0] = metavest; + bytes4 selector = bytes4(keccak256("updateMetavestStopTimes(address,uint48)")); + bytes memory msgData = abi.encodeWithSelector(selector, metavest, uint48(block.timestamp + 500 days)); + + vm.prank(authority); + controller.proposeMetavestAmendment(metavest, controller.updateMetavestStopTimes.selector, msgData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(metavest, controller.updateMetavestStopTimes.selector, true); + uint48 newShortStopTime = uint48(block.timestamp + 500 days); + + vm.prank(authority); + controller.updateMetavestStopTimes(metavest, newShortStopTime); + } function testTerminateVesting() public { address vestingAllocation = createDummyVestingAllocation(); @@ -735,76 +761,102 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { ); } -// function createDummyTokenOptionAllocation() internal returns (address) { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(token), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 1000e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 2000e18); -// -// return controller.createMetavest( -// MetaVestType.TokenOption, -// grantee, -// allocation, -// milestones, -// 5e17, -// address(paymentToken), -// 1 days, -// 0 -// ); -// } + function createDummyTokenOptionAllocation() internal returns (address) { + return createDummyTokenOptionAllocation(""); // Expect no reverts + } + function createDummyTokenOptionAllocation(bytes memory expectRevertData) internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setTokenOption( + alice, + address(paymentToken), // TODO try a different token + 5e17, // exercisePrice + 1 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(paymentToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + function createDummyRestrictedTokenAward() internal returns (address) { + return createDummyRestrictedTokenAward(""); + } + + function createDummyRestrictedTokenAward(bytes memory expectRevertData) internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setTokenOption( + alice, + address(paymentToken), // TODO try a different token + 1e18, // exercisePrice + 1 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(paymentToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } -// function createDummyRestrictedTokenAward() internal returns (address) { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(token), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 1000e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 2100e18); -// -// return controller.createMetavest( -// MetaVestType.RestrictedTokenAward, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 1 days, -// 0 -// -// ); -// } -// // function createDummyRestrictedTokenAwardFuture() internal returns (address) { // BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ // tokenContract: address(token), diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 62828c9..7915bbb 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -118,6 +118,7 @@ contract MetaVesTControllerTestBase is Test { ); } + // TODO WIP: temporarily for backwards compatibility function _proposeAndSignDeal( bytes32 templateId, uint256 agreementSalt, @@ -128,19 +129,43 @@ contract MetaVesTControllerTestBase is Test { string memory partyName, uint256 expiry, bytes memory expectRevertData + ) internal returns(bytes32) { + return _proposeAndSignDeal( + templateId, + agreementSalt, + grantorOrDelegatePrivateKey, + MetaVestDealLib.draft().setVesting( + grantee, + allocation, + milestones + ), + partyName, + expiry, + expectRevertData + ); + } + + function _proposeAndSignDeal( + bytes32 templateId, + uint256 agreementSalt, + uint256 grantorOrDelegatePrivateKey, + MetaVestDeal memory dealDraft, + string memory partyName, + uint256 expiry, + bytes memory expectRevertData ) internal returns(bytes32) { string[] memory globalValues = new string[](11); globalValues[0] = "0"; // metavestType: Vesting globalValues[1] = vm.toString(address(guardianSafe)); // grantor - globalValues[2] = vm.toString(grantee); // grantee - globalValues[3] = vm.toString(allocation.tokenContract); // tokenContract - globalValues[4] = vm.toString(allocation.tokenStreamTotal / 1 ether); //tokenStreamTotal (human-readable) - globalValues[5] = vm.toString(allocation.vestingCliffCredit / 1 ether); // vestingCliffCredit (human-readable) - globalValues[6] = vm.toString(allocation.unlockingCliffCredit / 1 ether); // unlockingCliffCredit (human-readable) - globalValues[7] = vm.toString(allocation.vestingRate * 365 days / 1 ether); // vestingRate (annually) (human-readable) - globalValues[8] = vm.toString(allocation.vestingStartTime); // vestingStartTime - globalValues[9] = vm.toString(allocation.unlockRate * 365 days / 1 ether); // unlockRate (annually) (human-readable) - globalValues[10] = vm.toString(allocation.unlockStartTime); // unlockStartTime + globalValues[2] = vm.toString(dealDraft.grantee); // grantee + globalValues[3] = vm.toString(dealDraft.allocation.tokenContract); // tokenContract + globalValues[4] = vm.toString(dealDraft.allocation.tokenStreamTotal / 1 ether); //tokenStreamTotal (human-readable) + globalValues[5] = vm.toString(dealDraft.allocation.vestingCliffCredit / 1 ether); // vestingCliffCredit (human-readable) + globalValues[6] = vm.toString(dealDraft.allocation.unlockingCliffCredit / 1 ether); // unlockingCliffCredit (human-readable) + globalValues[7] = vm.toString(dealDraft.allocation.vestingRate * 365 days / 1 ether); // vestingRate (annually) (human-readable) + globalValues[8] = vm.toString(dealDraft.allocation.vestingStartTime); // vestingStartTime + globalValues[9] = vm.toString(dealDraft.allocation.unlockRate * 365 days / 1 ether); // unlockRate (annually) (human-readable) + globalValues[10] = vm.toString(dealDraft.allocation.unlockStartTime); // unlockStartTime // TODO what to do with milestones, which could be of dynamic lengths @@ -152,13 +177,13 @@ contract MetaVesTControllerTestBase is Test { partyValues[0][3] = "Foundation"; partyValues[1] = new string[](4); partyValues[1][0] = partyName; - partyValues[1][1] = vm.toString(grantee); // evmAddress + partyValues[1][1] = vm.toString(dealDraft.grantee); // evmAddress partyValues[1][2] = "email@company.com"; partyValues[1][3] = "individual"; address[] memory parties = new address[](2); parties[0] = address(guardianSafe); - parties[1] = grantee; + parties[1] = dealDraft.grantee; bytes32 expectedContractId = keccak256( abi.encode( templateId, @@ -187,11 +212,7 @@ contract MetaVesTControllerTestBase is Test { bytes32 contractId = controller.proposeAndSignDeal( templateId, agreementSalt, - MetaVestDealLib.draft().setVesting( - grantee, - allocation, - milestones - ), + dealDraft, globalValues, parties, partyValues, From 7b22f37e6253b3649a68d416ef3f31b70139a66f Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 26 Oct 2025 14:35:11 -0700 Subject: [PATCH 10/25] test: distinguish vesting token from payment token --- test/AuditBaseC.t.sol | 2 +- test/AuditBaseC3.t.sol | 2 +- test/amendement.t.sol | 6 +- test/controller.t.sol | 84 ++++++++++++++----------- test/lib/MetaVesTControllerTestBase.sol | 7 ++- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/test/AuditBaseC.t.sol b/test/AuditBaseC.t.sol index 8bb8b9b..7f2d29d 100644 --- a/test/AuditBaseC.t.sol +++ b/test/AuditBaseC.t.sol @@ -10,7 +10,7 @@ contract Audit is MetaVestControllerTest { function test_RevertIf_AuditTerminateFailAfterWithdraw() public { // template from testTerminateVestAndRecoverSlowUnlock address vestingAllocation = createDummyVestingAllocationSlowUnlock(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); VestingAllocation(vestingAllocation).confirmMilestone(0); vm.warp(block.timestamp + 25 seconds); vm.startPrank(grantee); diff --git a/test/AuditBaseC3.t.sol b/test/AuditBaseC3.t.sol index 418fe93..cfe44fa 100644 --- a/test/AuditBaseC3.t.sol +++ b/test/AuditBaseC3.t.sol @@ -10,7 +10,7 @@ contract Audit is MetaVestControllerTest { function test_RevertIf_AuditTerminateFailAfterWithdraw() public { // template from testTerminateVestAndRecoverSlowUnlock address vestingAllocation = createDummyVestingAllocationSlowUnlock(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); VestingAllocation(vestingAllocation).confirmMilestone(0); vm.warp(block.timestamp + 25 seconds); vm.startPrank(grantee); diff --git a/test/amendement.t.sol b/test/amendement.t.sol index f9fa740..78e4b00 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -49,12 +49,12 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.stopPrank(); // Prepare funds - paymentToken.mint( + vestingToken.mint( address(guardianSafe), 9999 ether ); vm.prank(address(guardianSafe)); - paymentToken.approve(address(controller), 9999 ether); + vestingToken.approve(address(controller), 9999 ether); vm.startPrank(guardianSafe); @@ -378,7 +378,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, // = grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000 ether, vestingCliffCredit: 100 ether, unlockingCliffCredit: 100 ether, diff --git a/test/controller.t.sol b/test/controller.t.sol index 1892b4c..fb6611c 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -52,7 +52,15 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.stopPrank(); - // Prepare funds + // Prepare funds (vesting token) + vestingToken.mint( + address(guardianSafe), + 9999 ether + ); + vm.prank(address(guardianSafe)); + vestingToken.approve(address(controller), 9999 ether); + + // Prepare funds (payment token) paymentToken.mint( address(guardianSafe), 9999 ether @@ -77,7 +85,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 60 ether, vestingCliffCredit: 30 ether, unlockingCliffCredit: 30 ether, @@ -121,11 +129,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, MetaVestDealLib.draft().setTokenOption( alice, - address(paymentToken), // TODO try a different token + address(paymentToken), 1e18, // exercisePrice 365 days, // shortStopDuration BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000e18, vestingCliffCredit: 100e18, unlockingCliffCredit: 100e18, @@ -149,7 +157,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { "Alice" )); - assertEq(paymentToken.balanceOf(address(metavestAlice)), 1100e18, "Vesting contract should have token in escrow"); + assertEq(vestingToken.balanceOf(address(metavestAlice)), 1100e18, "Vesting contract should have token in escrow"); } function testCreateRestrictedTokenAward() public { @@ -167,11 +175,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, MetaVestDealLib.draft().setTokenOption( alice, - address(paymentToken), // TODO try a different token + address(paymentToken), 1e18, // exercisePrice 365 days, // shortStopDuration BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000e18, vestingCliffCredit: 100e18, unlockingCliffCredit: 100e18, @@ -195,7 +203,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { "Alice" )); - assertEq(paymentToken.balanceOf(address(metavestAlice)), 1100e18); + assertEq(vestingToken.balanceOf(address(metavestAlice)), 1100e18); } function testUpdateTransferability() public { @@ -354,11 +362,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { conditionContracts: new address[](0) }); - uint256 balanceBefore = paymentToken.balanceOf(address(vestingAllocation)); + uint256 balanceBefore = vestingToken.balanceOf(address(vestingAllocation)); vm.prank(authority); controller.addMetavestMilestone(vestingAllocation, newMilestone); assertEq( - paymentToken.balanceOf(address(vestingAllocation)) - balanceBefore, + vestingToken.balanceOf(address(vestingAllocation)) - balanceBefore, 50 ether, "vesting contract should receive token amount add by the milestone" ); @@ -589,7 +597,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, // = grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000 ether, vestingCliffCredit: 100 ether, unlockingCliffCredit: 100 ether, @@ -630,7 +638,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, // = grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000 ether, vestingCliffCredit: 100 ether, unlockingCliffCredit: 100 ether, @@ -670,7 +678,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, // = grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000 ether, vestingCliffCredit: 100 ether, unlockingCliffCredit: 100 ether, @@ -704,7 +712,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, // = grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000 ether, vestingCliffCredit: 0 ether, unlockingCliffCredit: 0 ether, @@ -738,7 +746,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, // = grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000 ether, vestingCliffCredit: 0 ether, unlockingCliffCredit: 0 ether, @@ -780,11 +788,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, MetaVestDealLib.draft().setTokenOption( alice, - address(paymentToken), // TODO try a different token + address(paymentToken), 5e17, // exercisePrice 1 days, // shortStopDuration BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000e18, vestingCliffCredit: 100e18, unlockingCliffCredit: 100e18, @@ -828,11 +836,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, MetaVestDealLib.draft().setTokenOption( alice, - address(paymentToken), // TODO try a different token + address(paymentToken), 1e18, // exercisePrice 1 days, // shortStopDuration BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 1000e18, vestingCliffCredit: 100e18, unlockingCliffCredit: 100e18, @@ -951,14 +959,14 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { function testTerminateVestAndRecovers() public { address vestingAllocation = createDummyVestingAllocation(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); VestingAllocation(vestingAllocation).confirmMilestone(0); vm.warp(block.timestamp + 50 seconds); - uint256 authorityBalanceBefore = paymentToken.balanceOf(address(authority)); + uint256 authorityBalanceBefore = vestingToken.balanceOf(address(authority)); vm.prank(authority); controller.terminateMetavestVesting(vestingAllocation); - assertEq(paymentToken.balanceOf( + assertEq(vestingToken.balanceOf( address(authority)) - authorityBalanceBefore, 400 ether, // 1000 + 1000 - 1000 - 100 - 10 * 50 "authority should receive unvested funds" @@ -967,12 +975,12 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.startPrank(grantee); VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); vm.stopPrank(); - assertEq(paymentToken.balanceOf(vestingAllocation), 0); + assertEq(vestingToken.balanceOf(vestingAllocation), 0); } function testTerminateVestAndRecoverSlowUnlock() public { address vestingAllocation = createDummyVestingAllocationSlowUnlock(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); VestingAllocation(vestingAllocation).confirmMilestone(0); vm.warp(block.timestamp + 25 seconds); vm.prank(authority); @@ -982,18 +990,18 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.warp(block.timestamp + 25 seconds); VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); vm.stopPrank(); - assertEq(paymentToken.balanceOf(vestingAllocation), 0); + assertEq(vestingToken.balanceOf(vestingAllocation), 0); } function testTerminateRecoverAll() public { address vestingAllocation = createDummyVestingAllocationLarge(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); vm.warp(block.timestamp + 25 seconds); - uint256 authorityBalanceBefore = paymentToken.balanceOf(address(authority)); + uint256 authorityBalanceBefore = vestingToken.balanceOf(address(authority)); vm.prank(authority); controller.terminateMetavestVesting(vestingAllocation); - assertEq(paymentToken.balanceOf( + assertEq(vestingToken.balanceOf( address(authority)) - authorityBalanceBefore, 750 ether, // 1000 - 10 * 25 "authority should receive unvested funds" @@ -1002,22 +1010,22 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.startPrank(grantee); VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); vm.stopPrank(); - assertEq(paymentToken.balanceOf(vestingAllocation), 0); + assertEq(vestingToken.balanceOf(vestingAllocation), 0); } function testTerminateRecoverChunksBefore() public { address vestingAllocation = createDummyVestingAllocationLarge(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); vm.warp(block.timestamp + 25 seconds); vm.startPrank(grantee); VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); vm.stopPrank(); vm.warp(block.timestamp + 25 seconds); - uint256 authorityBalanceBefore = paymentToken.balanceOf(address(authority)); + uint256 authorityBalanceBefore = vestingToken.balanceOf(address(authority)); vm.prank(authority); controller.terminateMetavestVesting(vestingAllocation); - assertEq(paymentToken.balanceOf( + assertEq(vestingToken.balanceOf( address(authority)) - authorityBalanceBefore, 500 ether, // 1000 - 10 * 50 "authority should receive unvested funds" @@ -1026,7 +1034,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.startPrank(grantee); VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); vm.stopPrank(); - assertEq(paymentToken.balanceOf(vestingAllocation), 0); + assertEq(vestingToken.balanceOf(vestingAllocation), 0); } // function testConfirmingMilestoneRestrictedTokenAllocation() public { @@ -1054,7 +1062,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { function testUnlockMilestoneNotUnlocked() public { address vestingAllocation = createDummyVestingAllocationNoUnlock(); - uint256 snapshot = paymentToken.balanceOf(authority); + uint256 snapshot = vestingToken.balanceOf(authority); VestingAllocation(vestingAllocation).confirmMilestone(0); vm.warp(block.timestamp + 50 seconds); vm.startPrank(grantee); @@ -1504,7 +1512,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { alicePrivateKey, // Should fail because Alice is not delegated by the grantor alice, // grantee BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 100 ether, vestingCliffCredit: 10 ether, unlockingCliffCredit: 10 ether, @@ -1527,7 +1535,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 100 ether, vestingCliffCredit: 10 ether, unlockingCliffCredit: 10 ether, @@ -1565,7 +1573,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), tokenStreamTotal: 100 ether, vestingCliffCredit: 10 ether, unlockingCliffCredit: 10 ether, @@ -1682,7 +1690,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { delegatePrivateKey, alice, BaseAllocation.Allocation({ - tokenContract: address(paymentToken), + tokenContract: address(vestingToken), // 100k ZK total, the first half unlocks with a cliff and the second half unlocks over an year tokenStreamTotal: 60 ether, vestingCliffCredit: 30 ether, diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 7915bbb..0843f79 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -16,6 +16,7 @@ import {MetaVestDealLib, MetaVestDeal} from "../../src/lib/MetaVestDealLib.sol"; contract MetaVesTControllerTestBase is Test { using MetaVestDealLib for MetaVestDeal; + MockERC20 vestingToken = new MockERC20("Vesting Token", "VEST", 18); MockERC20 paymentToken = new MockERC20("Payment Token", "PAY", 18); address deployer = address(0x2); @@ -93,13 +94,13 @@ contract MetaVesTControllerTestBase is Test { function _granteeWithdrawAndAsserts(VestingAllocation vestingAllocation, uint256 amount, string memory assertName) internal { address grantee = vestingAllocation.grantee(); - uint256 balanceBefore = paymentToken.balanceOf(grantee); + uint256 balanceBefore = vestingToken.balanceOf(grantee); vm.prank(grantee); vestingAllocation.withdraw(amount); - assertEq(paymentToken.balanceOf(grantee), balanceBefore + amount, string(abi.encodePacked(assertName, ": unexpected received amount"))); - assertEq(paymentToken.balanceOf(address(vestingAllocation)), 0, string(abi.encodePacked(assertName, ": vesting contract should not have any token (it mints on-demand)"))); + assertEq(vestingToken.balanceOf(grantee), balanceBefore + amount, string(abi.encodePacked(assertName, ": unexpected received amount"))); + assertEq(vestingToken.balanceOf(address(vestingAllocation)), 0, string(abi.encodePacked(assertName, ": vesting contract should not have any token (it mints on-demand)"))); } function _proposeAndSignDeal( From f8d87b6e4fd99a0745f6449d300279e8e8ada096 Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 26 Oct 2025 15:25:43 -0700 Subject: [PATCH 11/25] test: re-enable tests for token options and restricted tokens (contd) --- src/lib/MetaVestDealLib.sol | 2 +- test/controller.t.sol | 820 +++++++++++++++++------------------- 2 files changed, 396 insertions(+), 426 deletions(-) diff --git a/src/lib/MetaVestDealLib.sol b/src/lib/MetaVestDealLib.sol index ba68706..d484df2 100644 --- a/src/lib/MetaVestDealLib.sol +++ b/src/lib/MetaVestDealLib.sol @@ -110,7 +110,7 @@ library MetaVestDealLib { BaseAllocation.Allocation memory allocation, BaseAllocation.Milestone[] memory milestones ) internal pure returns (MetaVestDeal memory) { - deal.metavestType = MetaVestType.TokenOption; + deal.metavestType = MetaVestType.RestrictedTokenAward; deal.grantee = grantee; deal.paymentToken = paymentToken; deal.exercisePrice = exercisePrice; diff --git a/test/controller.t.sol b/test/controller.t.sol index fb6611c..1f83688 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -504,47 +504,57 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertTrue(BaseAllocation(vestingAllocation).terminated()); } -// function testRepurchaseTokens() public { -// uint256 startingBalance = paymentToken.balanceOf(grantee); -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// uint256 repurchaseAmount = 5e18; -// uint256 snapshot = token.balanceOf(authority); -// uint256 payment = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(repurchaseAmount); -// controller.terminateMetavestVesting(restrictedTokenAward); -// paymentToken.approve(address(restrictedTokenAward), payment); -// vm.warp(block.timestamp + 20 days); -// vm.prank(authority); -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(repurchaseAmount); -// -// assertEq(token.balanceOf(authority), snapshot+repurchaseAmount); -// -// vm.prank(grantee); -// RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); -// assertEq(paymentToken.balanceOf(grantee), startingBalance + payment); -// } - -// function testRepurchaseTokensFuture() public { -// uint256 startingBalance = paymentToken.balanceOf(grantee); -// address restrictedTokenAward = createDummyRestrictedTokenAwardFuture(); -// -// uint256 snapshot = token.balanceOf(authority); -// -// controller.terminateMetavestVesting(restrictedTokenAward); -// uint256 repurchaseAmount = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); -// uint256 payment = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(repurchaseAmount); -// paymentToken.approve(address(restrictedTokenAward), payment); -// vm.warp(block.timestamp + 20 days); -// vm.prank(authority); -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(repurchaseAmount); -// -// assertEq(token.balanceOf(authority), snapshot+repurchaseAmount); -// -// vm.prank(grantee); -// RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); -// console.log(token.balanceOf(restrictedTokenAward)); -// assertEq(paymentToken.balanceOf(grantee), startingBalance + payment); -// -// } + function testRepurchaseTokens() public { + uint256 startingPaymentTokenBalance = paymentToken.balanceOf(grantee); + address restrictedTokenAward = createDummyRestrictedTokenAward(); + uint256 repurchaseAmount = 5e18; + uint256 startingVestingTokenBalance = vestingToken.balanceOf(authority); + uint256 payment = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(repurchaseAmount); + + vm.startPrank(authority); + + controller.terminateMetavestVesting(restrictedTokenAward); + paymentToken.approve(address(restrictedTokenAward), payment); + + vm.warp(block.timestamp + 20 days); + + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(repurchaseAmount); + + vm.stopPrank(); + + assertEq(vestingToken.balanceOf(authority), startingVestingTokenBalance + repurchaseAmount); + + vm.prank(grantee); + RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); + assertEq(paymentToken.balanceOf(grantee), startingPaymentTokenBalance + payment); + } + + function testRepurchaseTokensFuture() public { + uint256 startingPaymentTokenBalance = paymentToken.balanceOf(grantee); + address restrictedTokenAward = createDummyRestrictedTokenAwardFuture(); + + uint256 startingVestingTokenBalance = vestingToken.balanceOf(authority); + + vm.startPrank(authority); + + controller.terminateMetavestVesting(restrictedTokenAward); + uint256 repurchaseAmount = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); + uint256 payment = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(repurchaseAmount); + paymentToken.approve(address(restrictedTokenAward), payment); + vm.warp(block.timestamp + 20 days); + + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(repurchaseAmount); + + vm.stopPrank(); + + assertEq(vestingToken.balanceOf(authority), startingVestingTokenBalance +repurchaseAmount); + + vm.prank(grantee); + RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); + console.log(vestingToken.balanceOf(restrictedTokenAward)); + assertEq(paymentToken.balanceOf(grantee), startingPaymentTokenBalance + payment); + + } function testTerminateTokensFuture() public { address vestingAllocation = createDummyVestingAllocationLargeFuture(); @@ -834,7 +844,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - MetaVestDealLib.draft().setTokenOption( + MetaVestDealLib.draft().setRestrictedToken( alice, address(paymentToken), 1e18, // exercisePrice @@ -865,63 +875,65 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { ); } -// function createDummyRestrictedTokenAwardFuture() internal returns (address) { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(token), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp+1000), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp+1000) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 1000e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 2100e18); -// -// return controller.createMetavest( -// MetaVestType.RestrictedTokenAward, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 1 days, -// 0 -// -// ); -// } - - - function testGetMetaVestType() public { - address vestingAllocation = createDummyVestingAllocation(); -// address tokenOptionAllocation = createDummyTokenOptionAllocation(); -// address restrictedTokenAward = createDummyRestrictedTokenAward(); - - assertEq(controller.getMetaVestType(vestingAllocation), 1); -// assertEq(controller.getMetaVestType(tokenOptionAllocation), 2); -// assertEq(controller.getMetaVestType(restrictedTokenAward), 3); - } - -// function testWithdrawFromController() public { -// uint256 amount = 100e18; -// token.transfer(address(controller), amount); -// -// uint256 initialBalance = token.balanceOf(authority); -// controller.withdrawFromController(address(token)); -// uint256 finalBalance = token.balanceOf(authority); -// -// assertEq(finalBalance - initialBalance, amount); -// assertEq(token.balanceOf(address(controller)), 0); -// } + function createDummyRestrictedTokenAwardFuture() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setRestrictedToken( + alice, + address(paymentToken), + 1e18, // exercisePrice + 1 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp + 1000), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp + 1000) + }), + milestones + ), + "Alice", + cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + function testWithdrawFromController() public { + uint256 amount = 100e18; + vm.startPrank(authority); + + paymentToken.transfer(address(controller), amount); + + uint256 initialBalance = paymentToken.balanceOf(authority); + controller.withdrawFromController(address(paymentToken)); + uint256 finalBalance = paymentToken.balanceOf(authority); + + vm.stopPrank(); + + assertEq(finalBalance - initialBalance, amount); + assertEq(paymentToken.balanceOf(address(controller)), 0); + } function test_RevertIf_CreateMetavestWithZeroAddress() public { BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); @@ -1037,162 +1049,178 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertEq(vestingToken.balanceOf(vestingAllocation), 0); } -// function testConfirmingMilestoneRestrictedTokenAllocation() public { -// address vestingAllocation = createDummyRestrictedTokenAward(); -// uint256 snapshot = token.balanceOf(authority); -// VestingAllocation(vestingAllocation).confirmMilestone(0); -// vm.warp(block.timestamp + 50 seconds); -// vm.startPrank(grantee); -// VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// } -// -// function testConfirmingMilestoneTokenOption() public { -// address vestingAllocation = createDummyTokenOptionAllocation(); -// uint256 snapshot = token.balanceOf(authority); -// TokenOptionAllocation(vestingAllocation).confirmMilestone(0); -// vm.warp(block.timestamp + 50 seconds); -// vm.startPrank(grantee); -// //exercise max available -// ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(TokenOptionAllocation(vestingAllocation).getAmountExercisable())); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(TokenOptionAllocation(vestingAllocation).getAmountExercisable()); -// TokenOptionAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// } + function testConfirmingMilestoneRestrictedTokenAllocation() public { + address metavest = createDummyRestrictedTokenAward(); + RestrictedTokenAward(metavest).confirmMilestone(0); + vm.warp(block.timestamp + 50 seconds); + vm.startPrank(grantee); + RestrictedTokenAward(metavest).withdraw(RestrictedTokenAward(metavest).getAmountWithdrawable()); + vm.stopPrank(); + } + + function testConfirmingMilestoneTokenOption() public { + address metavest = createDummyTokenOptionAllocation(); + TokenOptionAllocation(metavest).confirmMilestone(0); + vm.warp(block.timestamp + 50 seconds); + + // Fund grantee + uint256 vestingTokenExercisable = TokenOptionAllocation(metavest).getAmountExercisable(); + uint256 paymentTokenAmount = TokenOptionAllocation(metavest).getPaymentAmount(vestingTokenExercisable); + paymentToken.mint(grantee, paymentTokenAmount); + + vm.startPrank(grantee); + //exercise max available + paymentToken.approve(metavest, TokenOptionAllocation(metavest).getPaymentAmount(TokenOptionAllocation(metavest).getAmountExercisable())); + TokenOptionAllocation(metavest).exerciseTokenOption(vestingTokenExercisable); + TokenOptionAllocation(metavest).withdraw(VestingAllocation(metavest).getAmountWithdrawable()); + vm.stopPrank(); + } function testUnlockMilestoneNotUnlocked() public { - address vestingAllocation = createDummyVestingAllocationNoUnlock(); - uint256 snapshot = vestingToken.balanceOf(authority); - VestingAllocation(vestingAllocation).confirmMilestone(0); + address metavest = createDummyVestingAllocationNoUnlock(); + VestingAllocation(metavest).confirmMilestone(0); vm.warp(block.timestamp + 50 seconds); vm.startPrank(grantee); - VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); + VestingAllocation(metavest).withdraw(VestingAllocation(metavest).getAmountWithdrawable()); vm.warp(block.timestamp + 1050 seconds); - VestingAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); + VestingAllocation(metavest).withdraw(VestingAllocation(metavest).getAmountWithdrawable()); + vm.stopPrank(); + } + + function testTerminateTokenOptionAndRecover() public { + address tokenOptionAllocation = createDummyTokenOptionAllocation(); + vm.warp(block.timestamp + 25 seconds); + + // Fund grantee + paymentToken.mint(grantee, 350e18); + + vm.prank(grantee); + paymentToken.approve(tokenOptionAllocation, 350e18); + + vm.prank(grantee); + TokenOptionAllocation(tokenOptionAllocation).exerciseTokenOption(350e18); + + vm.prank(authority); + controller.terminateMetavestVesting(tokenOptionAllocation); + + vm.startPrank(grantee); + vm.warp(block.timestamp + 1 days + 25 seconds); + assertEq(TokenOptionAllocation(tokenOptionAllocation).getAmountExercisable(), 0); + TokenOptionAllocation(tokenOptionAllocation).withdraw(TokenOptionAllocation(tokenOptionAllocation).getAmountWithdrawable()); + vm.stopPrank(); + assertEq(vestingToken.balanceOf(tokenOptionAllocation), 0); + vm.warp(block.timestamp + 365 days); + vm.prank(authority); + TokenOptionAllocation(tokenOptionAllocation).recoverForfeitTokens(); + } + + function testTerminateEarlyTokenOptionAndRecover() public { + address tokenOptionAllocation = createDummyTokenOptionAllocation(); + vm.warp(block.timestamp + 5 seconds); + + vm.startPrank(authority); + + controller.terminateMetavestVesting(tokenOptionAllocation); + vm.warp(block.timestamp + 365 days); + TokenOptionAllocation(tokenOptionAllocation).recoverForfeitTokens(); + + vm.stopPrank(); + } + + function testTerminateRestrictedTokenAwardAndRecover() public { + address restrictedTokenAward = createDummyRestrictedTokenAward(); + vm.warp(block.timestamp + 25 seconds); + + vm.prank(authority); + controller.terminateMetavestVesting(restrictedTokenAward); + + vm.startPrank(grantee); + RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); + vm.stopPrank(); + + uint256 amt = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); + uint256 payamt = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(amt); + vm.warp(block.timestamp + 20 days); + + vm.startPrank(authority); + + paymentToken.approve(address(restrictedTokenAward), payamt); + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(amt); + + vm.stopPrank(); + + vm.prank(grantee); + RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); + + assertEq(vestingToken.balanceOf(restrictedTokenAward), 0); + assertEq(paymentToken.balanceOf(restrictedTokenAward), 0); + } + + function testChangeVestingAndUnlockingRate() public { + address restrictedTokenAward = createDummyRestrictedTokenAward(); + vm.warp(block.timestamp + 25 seconds); + + bytes4 msgSig = bytes4(keccak256("updateMetavestUnlockRate(address,uint160)")); + bytes memory callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 50e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); + + vm.prank(authority); + controller.updateMetavestUnlockRate(restrictedTokenAward, 50e18); + + msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); + callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 50e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); + + vm.prank(authority); + controller.updateMetavestVestingRate(restrictedTokenAward, 50e18); + + vm.startPrank(grantee); + RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); vm.stopPrank(); + } -// function testTerminateTokenOptionAndRecover() public { -// address tokenOptionAllocation = createDummyTokenOptionAllocation(); -// uint256 snapshot = token.balanceOf(authority); -// vm.warp(block.timestamp + 25 seconds); -// vm.prank(grantee); -// ERC20Stable(paymentToken).approve(tokenOptionAllocation, 350e18); -// vm.prank(grantee); -// TokenOptionAllocation(tokenOptionAllocation).exerciseTokenOption(350e18); -// controller.terminateMetavestVesting(tokenOptionAllocation); -// vm.startPrank(grantee); -// vm.warp(block.timestamp + 1 days + 25 seconds); -// assertEq(TokenOptionAllocation(tokenOptionAllocation).getAmountExercisable(), 0); -// TokenOptionAllocation(tokenOptionAllocation).withdraw(TokenOptionAllocation(tokenOptionAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// assertEq(token.balanceOf(tokenOptionAllocation), 0); -// vm.warp(block.timestamp + 365 days); -// vm.prank(authority); -// TokenOptionAllocation(tokenOptionAllocation).recoverForfeitTokens(); -// } - -// function testTerminateEarlyTokenOptionAndRecover() public { -// address tokenOptionAllocation = createDummyTokenOptionAllocation(); -// uint256 snapshot = token.balanceOf(authority); -// vm.warp(block.timestamp + 5 seconds); -// // vm.prank(grantee); -// /* ERC20Stable(paymentToken).approve(tokenOptionAllocation, 350e18); -// vm.prank(grantee); -// TokenOptionAllocation(tokenOptionAllocation).exerciseTokenOption(350e18);*/ -// controller.terminateMetavestVesting(tokenOptionAllocation); -// vm.warp(block.timestamp + 365 days); -// vm.prank(authority); -// TokenOptionAllocation(tokenOptionAllocation).recoverForfeitTokens(); -// } - - -// function testTerminateRestrictedTokenAwardAndRecover() public { -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// uint256 snapshot = token.balanceOf(authority); -// vm.warp(block.timestamp + 25 seconds); -// controller.terminateMetavestVesting(restrictedTokenAward); -// vm.startPrank(grantee); -// RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); -// vm.stopPrank(); -// uint256 amt = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); -// uint256 payamt = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(amt); -// vm.warp(block.timestamp + 20 days); -// paymentToken.approve(address(restrictedTokenAward), payamt); -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(amt); -// -// vm.startPrank(grantee); -// RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); -// assertEq(token.balanceOf(restrictedTokenAward), 0); -// assertEq(paymentToken.balanceOf(restrictedTokenAward), 0); -// } - -// function testChangeVestingAndUnlockingRate() public { -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// uint256 snapshot = token.balanceOf(authority); -// vm.warp(block.timestamp + 25 seconds); -// -// bytes4 msgSig = bytes4(keccak256("updateMetavestUnlockRate(address,uint160)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 50e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestUnlockRate(restrictedTokenAward, 50e18); -// -// msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); -// callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 50e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestVestingRate(restrictedTokenAward, 50e18); -// -// vm.startPrank(grantee); -// RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); -// vm.stopPrank(); -// -// } - -// function testZeroReclaim() public { -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// vm.warp(block.timestamp + 15 seconds); -// vm.startPrank(grantee); -// RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); -// vm.stopPrank(); -// //create call data to propose setting vesting to 0 -// bytes4 msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 0); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestVestingRate(restrictedTokenAward, 0); -// -// vm.startPrank(authority); -// controller.terminateMetavestVesting(restrictedTokenAward); -// vm.warp(block.timestamp + 155 days); -// uint256 amt = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); -// uint256 payamt = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(amt); -// paymentToken.approve(address(restrictedTokenAward), payamt); -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(amt); -// vm.stopPrank(); -// vm.prank(grantee); -// RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); -// console.log(token.balanceOf(restrictedTokenAward)); -// } + function testZeroReclaim() public { + address restrictedTokenAward = createDummyRestrictedTokenAward(); + vm.warp(block.timestamp + 15 seconds); + vm.startPrank(grantee); + RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); + vm.stopPrank(); + //create call data to propose setting vesting to 0 + bytes4 msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); + bytes memory callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 0); + + vm.prank(authority); + controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); + + vm.prank(authority); + controller.updateMetavestVestingRate(restrictedTokenAward, 0); + + vm.startPrank(authority); + controller.terminateMetavestVesting(restrictedTokenAward); + vm.warp(block.timestamp + 155 days); + uint256 amt = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); + uint256 payamt = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(amt); + paymentToken.approve(address(restrictedTokenAward), payamt); + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(amt); + vm.stopPrank(); + vm.prank(grantee); + RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); + console.log(vestingToken.balanceOf(restrictedTokenAward)); + } function testZeroReclaimVesting() public { address vestingAllocation = createDummyVestingAllocation(); @@ -1274,84 +1302,126 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.stopPrank(); } -// function testLargeReducOption() public { -// address restrictedTokenAward = createDummyTokenOptionAllocation(); -// vm.warp(block.timestamp + 5 seconds); -// vm.startPrank(grantee); -// //approve amount to exercise by getting amount to exercise and price -// ERC20Stable(paymentToken).approve(restrictedTokenAward, TokenOptionAllocation(restrictedTokenAward).getPaymentAmount(TokenOptionAllocation(restrictedTokenAward).getAmountExercisable())); -// TokenOptionAllocation(restrictedTokenAward).exerciseTokenOption(TokenOptionAllocation(restrictedTokenAward).getAmountExercisable()); -// RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); -// vm.stopPrank(); -// //create call data to propose setting vesting to 0 -// bytes4 msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 10e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestVestingRate(restrictedTokenAward, 10e18); -// vm.warp(block.timestamp + 5 seconds); -// vm.startPrank(authority); -// controller.terminateMetavestVesting(restrictedTokenAward); -// vm.stopPrank(); -// vm.warp(block.timestamp + 155 seconds); -// vm.startPrank(grantee); -// ERC20Stable(paymentToken).approve(restrictedTokenAward, TokenOptionAllocation(restrictedTokenAward).getPaymentAmount(TokenOptionAllocation(restrictedTokenAward).getAmountExercisable())); -// TokenOptionAllocation(restrictedTokenAward).exerciseTokenOption(TokenOptionAllocation(restrictedTokenAward).getAmountExercisable()); -// RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); -// vm.stopPrank(); -// console.log(token.balanceOf(restrictedTokenAward)); -// } - - - -// function testReclaim() public { -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// vm.warp(block.timestamp + 15 seconds); -// vm.startPrank(grantee); -// RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); -// vm.stopPrank(); -// -// vm.startPrank(authority); -// controller.terminateMetavestVesting(restrictedTokenAward); -// vm.warp(block.timestamp + 155 days); -// uint256 amt = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); -// uint256 payamt = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(amt); -// paymentToken.approve(address(restrictedTokenAward), payamt); -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(amt); -// vm.stopPrank(); -// vm.prank(grantee); -// RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); -// console.log(token.balanceOf(restrictedTokenAward)); -// } - - - -// function test_RevertIf_UpdateExercisePriceForVesting() public { -// address vestingAllocation = createDummyVestingAllocation(); -// controller.updateExerciseOrRepurchasePrice(vestingAllocation, 2e18); -// } - -// function test_RevertIf_RepurchaseTokensAfterExpiry() public { -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// -// // Fast forward time to after the short stop date -// vm.warp(block.timestamp + 366 days); -// -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(500e18); -// } - -// function test_RevertIf_RepurchaseTokensInsufficientAllowance() public { -// address restrictedTokenAward = createDummyRestrictedTokenAward(); -// -// // Not approving any tokens -// RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(500e18); -// } + function testLargeReducOption() public { + address restrictedTokenAward = createDummyTokenOptionAllocation(); + vm.warp(block.timestamp + 5 seconds); + + { + // Fund grantee + uint256 vestingTokenExercisableAmount = TokenOptionAllocation(restrictedTokenAward).getAmountExercisable(); + uint256 paymentTokenAmount = TokenOptionAllocation(restrictedTokenAward).getPaymentAmount(vestingTokenExercisableAmount); + deal(address(paymentToken), grantee, paymentTokenAmount); + uint256 vestingTokenBalanceBefore = vestingToken.balanceOf(grantee); + uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(grantee); + + vm.startPrank(grantee); + //approve amount to exercise by getting amount to exercise and price + paymentToken.approve(restrictedTokenAward, paymentTokenAmount); + TokenOptionAllocation(restrictedTokenAward).exerciseTokenOption(vestingTokenExercisableAmount); + RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); + vm.stopPrank(); + + assertEq(vestingToken.balanceOf(grantee) - vestingTokenBalanceBefore, 150 ether, "grantee should have exercised 100 + 10 * 5 = 150 tokens"); + assertEq(paymentTokenBalanceBefore - paymentToken.balanceOf(grantee), 75 ether, "grantee should have paid 150 * 0.5 = 75 tokens"); + } + + //create call data to propose setting vesting to 0 + bytes4 msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); + bytes memory callData = abi.encodeWithSelector(msgSig, restrictedTokenAward, 20e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(restrictedTokenAward, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(restrictedTokenAward, msgSig, true); + + vm.prank(authority); + controller.updateMetavestVestingRate(restrictedTokenAward, 20e18); + vm.warp(block.timestamp + 5 seconds); + vm.startPrank(authority); + controller.terminateMetavestVesting(restrictedTokenAward); + vm.stopPrank(); + vm.warp(block.timestamp + 155 seconds); + + { + // Fund grantee + uint256 vestingTokenExercisableAmount = TokenOptionAllocation(restrictedTokenAward).getAmountExercisable(); + uint256 paymentTokenAmount = TokenOptionAllocation(restrictedTokenAward).getPaymentAmount(vestingTokenExercisableAmount); + deal(address(paymentToken), grantee, paymentTokenAmount); + uint256 vestingTokenBalanceBefore = vestingToken.balanceOf(grantee); + uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(grantee); + + vm.startPrank(grantee); + + paymentToken.approve(restrictedTokenAward, paymentTokenAmount); + TokenOptionAllocation(restrictedTokenAward).exerciseTokenOption(vestingTokenExercisableAmount); + RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); + vm.stopPrank(); + + assertEq(vestingToken.balanceOf(grantee) - vestingTokenBalanceBefore, 150 ether, "grantee should have exercised 100 + 20 * (5 + 5) - 150 = 150 tokens"); + assertEq(paymentTokenBalanceBefore - paymentToken.balanceOf(grantee), 75 ether, "grantee should have paid 150 * 0.5 = 75 tokens"); + } + } + + function testReclaim() public { + address restrictedTokenAward = createDummyRestrictedTokenAward(); + vm.warp(block.timestamp + 15 seconds); + vm.startPrank(grantee); + RestrictedTokenAward(restrictedTokenAward).withdraw(RestrictedTokenAward(restrictedTokenAward).getAmountWithdrawable()); + assertEq(vestingToken.balanceOf(grantee), 250 ether, "grantee should receive 100 + 10 * 15 = 250 tokens"); + vm.stopPrank(); + + vm.startPrank(authority); + controller.terminateMetavestVesting(restrictedTokenAward); + vm.warp(block.timestamp + 155 days); + uint256 amt = RestrictedTokenAward(restrictedTokenAward).getAmountRepurchasable(); + uint256 payamt = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(amt); + + uint256 authorityVestingTokenBalanceBefore = vestingToken.balanceOf(authority); + paymentToken.approve(address(restrictedTokenAward), payamt); + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(amt); + assertEq(vestingToken.balanceOf(authority) - authorityVestingTokenBalanceBefore, 1750 ether, "authority should have repurchased 1000 + 1000 - 250 = 1750 token"); + vm.stopPrank(); + + vm.prank(grantee); + RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); + assertEq(paymentToken.balanceOf(grantee), 1750 ether, "grantee should receive repurchase payment of 1750 * 1 = 1750 tokens"); + } + + function test_RevertIf_UpdateExercisePriceForVesting() public { + address vestingAllocation = createDummyVestingAllocation(); + + vm.prank(authority); + vm.expectRevert(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector); + controller.updateExerciseOrRepurchasePrice(vestingAllocation, 2e18); + } + + function test_RevertIf_RepurchaseTokensBeforeShortStop() public { + address restrictedTokenAward = createDummyRestrictedTokenAward(); + + // Terminate, then immediate repurchase before short stop date + vm.startPrank(authority); + controller.terminateMetavestVesting(restrictedTokenAward); + vm.expectRevert(BaseAllocation.MetaVesT_ShortStopTimeNotReached.selector); + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(500e18); + vm.stopPrank(); + } + + function test_RevertIf_RepurchaseTokensInsufficientAllowance() public { + address restrictedTokenAward = createDummyRestrictedTokenAward(); + + vm.startPrank(authority); + + // Terminate, then fast forward time to after the short stop date + controller.terminateMetavestVesting(restrictedTokenAward); + vm.warp(block.timestamp + 1 days); + + // Not approving any tokens + vm.expectRevert(SafeTransferLib.TransferFromFailed.selector); + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(500e18); + + vm.stopPrank(); + } function test_RevertIf_InitiateAuthorityUpdateNonAuthority() public { vm.prank(address(0x1234)); @@ -1469,41 +1539,6 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.updateFunctionCondition(address(condition), functionSig); } - // TODO deprecated: do we still need this? -// function test_RevertIf_ExceedCap() public { -// // Add a large grant that exceeds the cap -// bytes32 contractIdChad = _proposeAndSignDeal( -// templateId, -// block.timestamp, // salt -// delegatePrivateKey, -// chad, -// BaseAllocation.Allocation({ -// tokenContract: address(paymentToken), -// tokenStreamTotal: 2001 ether, -// vestingCliffCredit: 2001 ether, -// unlockingCliffCredit: 2001 ether, -// vestingRate: 0, -// vestingStartTime: 0, -// unlockRate: 0, -// unlockStartTime: 0 -// }), -// new BaseAllocation.Milestone[](0), -// "Chad", -// cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible -// ); -// VestingAllocation vestingAllocationChad = VestingAllocation(_granteeSignDeal( -// contractIdChad, -// chad, // grantee -// chad, // recipient -// chadPrivateKey, -// "Chad" -// )); -// -// vm.prank(chad); -// vm.expectRevert(abi.encodeWithSelector(IZkCappedMinterV2.ZkCappedMinterV2__CapExceeded.selector, address(controller), 2001 ether)); -// vestingAllocationChad.withdraw(2001 ether); -// } - function test_RevertIf_IncorrectGrantorSignature() public { // Should not be able to propose a deal without grantor's authorization _proposeAndSignDeal( @@ -1602,71 +1637,6 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertFalse(registry.isValidDelegate(alice, bob), "Bob should no longer be Alice's delegate"); } - // TODO WIP: re-purpose it for withdrawing funds from active metavest -// function test_TogglePauseMinting() public { -// IZkCappedMinterV2 controllerOwnedMinter = IZkCappedMinterV2(zkCappedMinterFactory.createCappedMinter( -// address(paymentToken), -// address(controller), // Note MetaVesTController being the admin -// cap, -// cappedMinterStartTime, -// cappedMinterExpirationTime, -// uint256(salt) -// )); -// vm.prank(authority); -// controller.setZkCappedMinter(address(controllerOwnedMinter)); -// -// assertFalse(controllerOwnedMinter.paused(), "minter should not be paused yet"); -// -// // Authority should be able to pause minting through controller -// vm.prank(authority); -// controller.pauseZkCappedMinter(); -// assertTrue(controllerOwnedMinter.paused(), "minter should be paused now"); -// -// vm.prank(authority); -// controller.unpauseZkCappedMinter(); -// assertFalse(controllerOwnedMinter.paused(), "minter should be unpaused now"); -// } -// -// function test_RevertIf_PauseMintingNonAuthority() public { -// // Non-authority should not be able to pause minting through controller -// vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); -// controller.pauseZkCappedMinter(); -// } -// -// function test_CloseMinting() public { -// IZkCappedMinterV2 controllerOwnedMinter = IZkCappedMinterV2(zkCappedMinterFactory.createCappedMinter( -// address(paymentToken), -// address(controller), // Note MetaVesTController being the admin -// cap, -// cappedMinterStartTime, -// cappedMinterExpirationTime, -// uint256(salt) -// )); -// vm.prank(authority); -// controller.setZkCappedMinter(address(controllerOwnedMinter)); -// -// assertFalse(controllerOwnedMinter.closed(), "minter should not be closed yet"); -// -// // Authority should be able to close minting through controller -// vm.prank(authority); -// controller.closeZkCappedMinter(); -// assertTrue(controllerOwnedMinter.closed(), "minter should be closed now"); -// } -// -// function test_RevertIf_CloseMintingNonAuthority() public { -// // Non-authority should not be able to close minting through controller -// vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); -// controller.closeZkCappedMinter(); -// } - - // TODO deprecated: can we re-purpose it? -// function test_RevertIf_MintUnauthorized() public { -// // Should not be able to mint arbitrarily -// vm.prank(alice); -// vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_UnauthorizedToMint.selector)); -// controller.mint(alice, 1 ether); -// } - function test_UpgradeMetaVesTController() public { // Deploy new implementation address newImplementation = address(new metavestController()); From 350ee127c3acf4b4e3c4bbefe0be1ec884bc92af Mon Sep 17 00:00:00 2001 From: detoo Date: Mon, 27 Oct 2025 10:18:18 -0700 Subject: [PATCH 12/25] fix: add extra parties verification. Allow external agreement signing. Remove unused comments --- src/MetaVesTController.sol | 4 +- src/storage/MetaVesTControllerStorage.sol | 21 +++- test/controller.t.sol | 145 +++++++++++++++++++--- test/lib/MetaVesTControllerTestBase.sol | 30 ++++- 4 files changed, 172 insertions(+), 28 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index a7308d4..44c0b31 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -175,8 +175,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) external returns (bytes32) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - // TODO validate parties against deal // Check: verify inputs + // TODO check metavest type (and other party values) + if (parties[1] != dealDraft.grantee) revert MetaVesTControllerStorage.MetaVesTController_GranteeNotDirectParty(); if (partyValues.length < 2) revert MetaVesTControllerStorage.MetaVesTController_CounterPartyNotFound(); if (partyValues[1].length != partyValues[0].length) revert MetaVesTControllerStorage.MetaVesTController_PartyValuesLengthMismatch(); @@ -200,7 +201,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { return dealProposed.agreementId; } - // TODO handle cases when agreement is signed externally function signDealAndCreateMetavest( address grantee, address recipient, diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index bc3f9dd..0faf06a 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -143,7 +143,7 @@ library MetaVesTControllerStorage { error MetaVesTController_ZeroAddress(); error MetaVesTController_ZeroAmount(); error MetaVesTController_ZeroPrice(); - error MetaVesT_AmountNotApprovedForTransferFrom(); + error MetaVesT_AmountNotApprovedForTransferFrom(); // TODO review needed: should we move it elsewhere? error MetaVesTController_SetDoesNotExist(); error MetaVestController_MetaVestNotInSet(); error MetaVesTController_SetAlreadyExists(); @@ -152,9 +152,9 @@ library MetaVesTControllerStorage { error MetaVesTController_DealAlreadyFinalized(); error MetaVesTController_DealVoided(); error MetaVesTController_CounterPartyNotFound(); + error MetaVesTController_GranteeNotDirectParty(); error MetaVesTController_PartyValuesLengthMismatch(); error MetaVesTController_CounterPartyValueMismatch(); - error MetaVesTController_UnauthorizedToMint(); error MetaVesTController_UnknownError(bytes4 error, bytes data); function getStorage() internal pure returns (MetaVesTControllerData storage st) { @@ -164,7 +164,6 @@ library MetaVesTControllerStorage { } } - // TODO why doesn't it need conditionCheck? function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address){ MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); address vestingAllocation = IAllocationFactory(st.vestingFactory).createAllocation( @@ -182,7 +181,6 @@ library MetaVesTControllerStorage { return vestingAllocation; } - // TODO where should we put conditionCheck instead since it will emit events? function createTokenOptionAllocation(MetaVestDeal storage deal, address recipient) internal returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); address tokenOptionAllocation = IAllocationFactory(st.tokenOptionFactory).createAllocation( @@ -200,7 +198,6 @@ library MetaVesTControllerStorage { return tokenOptionAllocation; } - // TODO where should we put conditionCheck instead since it will emit events? function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal returns (address){ MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); address restrictedTokenAward = IAllocationFactory(st.restrictedTokenFactory).createAllocation( @@ -320,7 +317,19 @@ library MetaVesTControllerStorage { } // Interaction: Finalize agreement - ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); + + // If the grantee signed the agreement externally (ex. by directly interacting with CyberAgreementRegistry), + // we will skip the signing step. + if (!ICyberAgreementRegistry(st.registry).hasSigned(agreementId, grantee)) { + ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); + } else { + // Already signed in registry; fetch values recorded in the registry and ensure consistency. + // Theoretically, since the agreement is always close-ended, we could've safely assumed + // the counterparty values stored in CyberAgreementRegistry vs here are always consistent, + // but we check it anyways just in case + string[] memory registryValues = ICyberAgreementRegistry(st.registry).getSignerValues(agreementId, grantee); + if (keccak256(abi.encode(registryValues)) != keccak256(abi.encode(partyValues))) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); + } ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); // Interaction: Create and provision MetaVesT diff --git a/test/controller.t.sol b/test/controller.t.sol index 1f83688..bf9b8be 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -22,9 +22,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { address transferee = address(0x101); // Parameters - uint256 cap = 2000 ether; - uint48 cappedMinterStartTime = uint48(block.timestamp); // Minter start now - uint48 cappedMinterExpirationTime = uint48(cappedMinterStartTime + 1600); // Minter expired 1600 seconds after start + uint48 metavestExpiry = uint48(block.timestamp + 1600); // MetaVest expires 1600 seconds later function setUp() public override { MetaVesTControllerTestBase.setUp(); @@ -96,7 +94,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), new BaseAllocation.Milestone[](0), "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); VestingAllocation vestingAllocationAlice = VestingAllocation(_granteeSignDeal( @@ -145,7 +143,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { milestones ), "Alice", - cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry, "" ); @@ -191,7 +189,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { milestones ), "Alice", - cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry, "" ); @@ -618,7 +616,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), milestones, "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); return _granteeSignDeal( @@ -659,7 +657,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), milestones, "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); return _granteeSignDeal( @@ -699,7 +697,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), milestones, "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); return _granteeSignDeal( @@ -733,7 +731,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), milestones, "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); return _granteeSignDeal( @@ -767,7 +765,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), milestones, "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); return _granteeSignDeal( @@ -814,7 +812,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { milestones ), "Alice", - cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry, "" ); @@ -862,7 +860,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { milestones ), "Alice", - cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry, "" ); @@ -906,7 +904,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { milestones ), "Alice", - cappedMinterExpirationTime, // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry, "" ); @@ -956,7 +954,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), milestones, "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); _granteeSignDeal( @@ -1539,6 +1537,38 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.updateFunctionCondition(address(condition), functionSig); } + function test_RevertIf_GranteeNotDirectParty() public { + // Proposal should fail if the grantee is not listed as a direct party (non-delegate). + // This is to prevent accidentally signing an agreement for other's grant + address[] memory parties = new address[](2); + parties[0] = authority; + parties[1] = bob; // not Alice the grantee + + _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + parties, + MetaVestDealLib.draft().setVesting( + grantee, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + block.timestamp + 7 days, + abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_GranteeNotDirectParty.selector) // Expected revert + ); + } + function test_RevertIf_IncorrectGrantorSignature() public { // Should not be able to propose a deal without grantor's authorization _proposeAndSignDeal( @@ -1619,7 +1649,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), new BaseAllocation.Milestone[](0), "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); VestingAllocation vestingAllocation = VestingAllocation(_granteeSignDeal( contractId, @@ -1636,6 +1666,87 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Bob should no longer be able to sign for Alice assertFalse(registry.isValidDelegate(alice, bob), "Bob should no longer be Alice's delegate"); } + + function test_GranteeSignedExternally() public { + // It should still be able to create metavest if the grantee has signed externally by interacting directly with + // CyberAgreementRegistry + + bytes32 contractId = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0), + "Alice", + metavestExpiry + ); + + // Alice to sign the agreement externally + + MetaVestDeal memory deal = controller.getDeal(contractId); + + string[] memory globalValues = new string[](11); + globalValues[0] = vm.toString(uint256(MetaVestType.Vesting)); + globalValues[1] = vm.toString(address(guardianSafe)); // grantor + globalValues[2] = vm.toString(grantee); // grantee + globalValues[3] = vm.toString(deal.allocation.tokenContract); // tokenContract + globalValues[4] = vm.toString(deal.allocation.tokenStreamTotal / 1 ether); //tokenStreamTotal (human-readable) + globalValues[5] = vm.toString(deal.allocation.vestingCliffCredit / 1 ether); // vestingCliffCredit (human-readable) + globalValues[6] = vm.toString(deal.allocation.unlockingCliffCredit / 1 ether); // unlockingCliffCredit (human-readable) + globalValues[7] = vm.toString(deal.allocation.vestingRate * 365 days / 1 ether); // vestingRate (annually) (human-readable) + globalValues[8] = vm.toString(deal.allocation.vestingStartTime); // vestingStartTime + globalValues[9] = vm.toString(deal.allocation.unlockRate * 365 days / 1 ether); // unlockRate (annually) (human-readable) + globalValues[10] = vm.toString(deal.allocation.unlockStartTime); // unlockStartTime + + string[] memory partyValues = new string[](4); + partyValues[0] = "Alice"; + partyValues[1] = vm.toString(grantee); // evmAddress + partyValues[2] = "email@company.com"; // Make sure it matches the proposed deal + partyValues[3] = "individual"; // Make sure it matches the proposed deal + + registry.signContractFor( + alice, + contractId, + partyValues, + CyberAgreementUtils.signAgreementTypedData( + vm, + registry.DOMAIN_SEPARATOR(), + registry.SIGNATUREDATA_TYPEHASH(), + contractId, + agreementUri, + globalFields, + partyFields, + globalValues, + partyValues, + alicePrivateKey + ), + false, // fillUnallocated + "" // secret + ); + assertTrue(registry.hasSigned(contractId, alice), "Alice should've signed"); + + // Should still be able to create metavest for Alice + + VestingAllocation metavest = VestingAllocation(controller.signDealAndCreateMetavest( + alice, + alice, + contractId, + partyValues, + "", // signature no longer needed since Alice has signed externally + "" // no secrets + )); + assertEq(metavest.grantee(), alice, "Alice should be the grantee"); + } function test_UpgradeMetaVesTController() public { // Deploy new implementation @@ -1672,7 +1783,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { }), new BaseAllocation.Milestone[](0), "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); VestingAllocation vestingAllocationAlice = VestingAllocation(_granteeSignDeal( diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 0843f79..69bbe8b 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -146,6 +146,7 @@ contract MetaVesTControllerTestBase is Test { ); } + /// @notice Shortcut for automatically generate the correct parties function _proposeAndSignDeal( bytes32 templateId, uint256 agreementSalt, @@ -154,6 +155,32 @@ contract MetaVesTControllerTestBase is Test { string memory partyName, uint256 expiry, bytes memory expectRevertData + ) internal returns(bytes32) { + address[] memory parties = new address[](2); + parties[0] = address(guardianSafe); + parties[1] = dealDraft.grantee; + + return _proposeAndSignDeal( + templateId, + agreementSalt, + grantorOrDelegatePrivateKey, + parties, + dealDraft, + partyName, + expiry, + expectRevertData + ); + } + + function _proposeAndSignDeal( + bytes32 templateId, + uint256 agreementSalt, + uint256 grantorOrDelegatePrivateKey, + address[] memory parties, + MetaVestDeal memory dealDraft, + string memory partyName, + uint256 expiry, + bytes memory expectRevertData ) internal returns(bytes32) { string[] memory globalValues = new string[](11); globalValues[0] = "0"; // metavestType: Vesting @@ -182,9 +209,6 @@ contract MetaVesTControllerTestBase is Test { partyValues[1][2] = "email@company.com"; partyValues[1][3] = "individual"; - address[] memory parties = new address[](2); - parties[0] = address(guardianSafe); - parties[1] = dealDraft.grantee; bytes32 expectedContractId = keccak256( abi.encode( templateId, From f8b0944c8c05e6e0de1a7cfcb7a09cb8e590164b Mon Sep 17 00:00:00 2001 From: detoo Date: Mon, 27 Oct 2025 13:49:26 -0700 Subject: [PATCH 13/25] fix: unify metavest type reporting --- src/BaseAllocation.sol | 4 +++- src/MetaVesTController.sol | 6 ++---- src/RestrictedTokenAllocation.sol | 9 +++++---- src/TokenOptionAllocation.sol | 9 +++++---- src/VestingAllocation.sol | 9 +++++---- test/lib/MetaVesTControllerTestBase.sol | 4 ++-- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/BaseAllocation.sol b/src/BaseAllocation.sol index f08e07b..1b98467 100644 --- a/src/BaseAllocation.sol +++ b/src/BaseAllocation.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.28; +import {MetaVestType} from "./lib/MetaVestDealLib.sol"; + /// @notice interface to a MetaLeX condition contract /// @dev see https://github.com/MetaLex-Tech/BORG-CORE/tree/main/src/libs/conditions interface IConditionM { @@ -190,7 +192,7 @@ abstract contract BaseAllocation is ReentrancyGuard, SafeTransferLib{ govType = GovType.vested; } - function getVestingType() external view virtual returns (uint256); + function getVestingType() external view virtual returns (MetaVestType); function getGoverningPower() external virtual returns (uint256); function updateStopTimes(uint48 _shortStopTime) external virtual;// onlyController; function terminate() external virtual;// onlyController; diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index 44c0b31..e7a4ab7 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -176,7 +176,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // Check: verify inputs - // TODO check metavest type (and other party values) if (parties[1] != dealDraft.grantee) revert MetaVesTControllerStorage.MetaVesTController_GranteeNotDirectParty(); if (partyValues.length < 2) revert MetaVesTControllerStorage.MetaVesTController_CounterPartyNotFound(); if (partyValues[1].length != partyValues[0].length) revert MetaVesTControllerStorage.MetaVesTController_PartyValuesLengthMismatch(); @@ -231,7 +230,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { return newMetavest; } - function getMetaVestType(address _grant) public view returns (uint256) { + function getMetaVestType(address _grant) public view returns (MetaVestType) { return BaseAllocation(_grant).getVestingType(); } @@ -256,7 +255,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { BaseAllocation(_grant).updateTransferability(_isTransferable); } - // TODO review needed /// @notice for the controller to update either exercisePrice or repurchasePrice for a '_grantee' and their transferees, as applicable depending on the '_grantee''s MetaVesTType /// @param _grant address of grantee whose applicable price is being updated /// @param _newPrice new exercisePrice (if token option) or (repurchase price if restricted token award) as 'paymentToken' per 1 metavested token in vesting token decimals but only up to payment decimal precision @@ -266,7 +264,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ) external onlyAuthority conditionCheck consentCheck(_grant, msg.data) { if (_newPrice == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroPrice(); IPriceAllocation grant = IPriceAllocation(_grant); - // TODO review needed: it is supposed to be TokenOption, RestrictedTokenAward right? + // The price is only meaningful for TokenOption and RestrictedTokenAward types if (grant.getVestingType() != uint256(MetaVestType.TokenOption) && grant.getVestingType() != uint256(MetaVestType.RestrictedTokenAward)) revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); _resetAmendmentParams(_grant, msg.sig); grant.updatePrice(_newPrice); diff --git a/src/RestrictedTokenAllocation.sol b/src/RestrictedTokenAllocation.sol index e23f251..401c799 100644 --- a/src/RestrictedTokenAllocation.sol +++ b/src/RestrictedTokenAllocation.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import "./BaseAllocation.sol"; +import {MetaVestType} from "./lib/MetaVestDealLib.sol"; pragma solidity ^0.8.24; @@ -62,10 +63,10 @@ contract RestrictedTokenAward is BaseAllocation { } } - /// @notice returns the vesting type for RestrictedTokenAward - /// @return uint256 type 3 - function getVestingType() external pure override returns (uint256) { - return 3; + /// @notice returns the vesting type + /// @return MetaVestType + function getVestingType() external pure override returns (MetaVestType) { + return MetaVestType.RestrictedTokenAward; } /// @notice returns the governing power for RestrictedTokenAward based on the govType diff --git a/src/TokenOptionAllocation.sol b/src/TokenOptionAllocation.sol index 99d52a5..0f1ef7a 100644 --- a/src/TokenOptionAllocation.sol +++ b/src/TokenOptionAllocation.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import "./BaseAllocation.sol"; +import {MetaVestType} from "./lib/MetaVestDealLib.sol"; pragma solidity ^0.8.24; @@ -63,10 +64,10 @@ contract TokenOptionAllocation is BaseAllocation { } } - /// @notice returns the contract vesting type 2 for TokenOptionAllocation - /// @return 2 - function getVestingType() external pure override returns (uint256) { - return 2; + /// @notice returns the vesting type + /// @return MetaVestType + function getVestingType() external pure override returns (MetaVestType) { + return MetaVestType.TokenOption; } /// @notice returns the governing power of the TokenOptionAllocation diff --git a/src/VestingAllocation.sol b/src/VestingAllocation.sol index 9d8726e..b5c2993 100644 --- a/src/VestingAllocation.sol +++ b/src/VestingAllocation.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import "./BaseAllocation.sol"; +import {MetaVestType} from "./lib/MetaVestDealLib.sol"; pragma solidity ^0.8.24; @@ -45,10 +46,10 @@ contract VestingAllocation is BaseAllocation { } } - /// @notice returns the contract vesting type 1 for VestingAllocation - /// @return 1 for VestingAllocation - function getVestingType() external pure override returns (uint256) { - return 1; + /// @notice returns the vesting type + /// @return MetaVestType + function getVestingType() external pure override returns (MetaVestType) { + return MetaVestType.Vesting; } /// @notice returns the governing power of the VestingAllocation diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 69bbe8b..15c4249 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -183,7 +183,7 @@ contract MetaVesTControllerTestBase is Test { bytes memory expectRevertData ) internal returns(bytes32) { string[] memory globalValues = new string[](11); - globalValues[0] = "0"; // metavestType: Vesting + globalValues[0] = vm.toString(uint256(dealDraft.metavestType)); // metavestType globalValues[1] = vm.toString(address(guardianSafe)); // grantor globalValues[2] = vm.toString(dealDraft.grantee); // grantee globalValues[3] = vm.toString(dealDraft.allocation.tokenContract); // tokenContract @@ -278,7 +278,7 @@ contract MetaVesTControllerTestBase is Test { MetaVestDeal memory deal = controller.getDeal(contractId); string[] memory globalValues = new string[](11); - globalValues[0] = "0"; // metavestType: Vesting + globalValues[0] = vm.toString(uint256(deal.metavestType)); // metavestType globalValues[1] = vm.toString(address(guardianSafe)); // grantor globalValues[2] = vm.toString(grantee); // grantee globalValues[3] = vm.toString(deal.allocation.tokenContract); // tokenContract From e09926d0539b49834a2fa94bcb1752453fe48cd1 Mon Sep 17 00:00:00 2001 From: detoo Date: Mon, 27 Oct 2025 14:07:19 -0700 Subject: [PATCH 14/25] feat: TokenOption and RestrictedToken to support recipient. Remove duplicate codes --- src/RestrictedTokenAllocation.sol | 19 ++++++-------- src/RestrictedTokenFactory.sol | 2 +- src/TokenOptionAllocation.sol | 19 ++++++-------- src/TokenOptionFactory.sol | 2 +- src/VestingAllocation.sol | 16 +++--------- test/controller.t.sol | 41 +++++++++++++++++++++++++++---- 6 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/RestrictedTokenAllocation.sol b/src/RestrictedTokenAllocation.sol index 401c799..e866ef0 100644 --- a/src/RestrictedTokenAllocation.sol +++ b/src/RestrictedTokenAllocation.sol @@ -16,6 +16,7 @@ contract RestrictedTokenAward is BaseAllocation { /// @notice Constructor to deploy a new RestrictedTokenAward /// @param _grantee - address of the grantee + /// @param _recipient address of the fund recipient /// @param _controller - address of the controller /// @param _paymentToken - address of the payment token /// @param _repurchasePrice - price at which the restricted tokens can be repurchased in vesting token decimals but only up to payment decimal precision @@ -24,7 +25,7 @@ contract RestrictedTokenAward is BaseAllocation { /// @param _milestones - milestones with their conditions and awards constructor ( address _grantee, - address _recipient, // TODO review needed + address _recipient, address _controller, address _paymentToken, uint256 _repurchasePrice, @@ -40,16 +41,10 @@ contract RestrictedTokenAward is BaseAllocation { if (_allocation.tokenContract == address(0)) revert MetaVesT_ZeroAddress(); if (_allocation.tokenStreamTotal == 0) revert MetaVesT_ZeroAmount(); if (_allocation.vestingRate > 1000*1e18 || _allocation.unlockRate > 1000*1e18) revert MetaVesT_RateTooHigh(); + if (_allocation.vestingRate < 100 || _allocation.unlockRate < 100) revert MetaVesT_RateTooLow(); - //set vesting allocation variables - allocation.tokenContract = _allocation.tokenContract; - allocation.tokenStreamTotal = _allocation.tokenStreamTotal; - allocation.vestingCliffCredit = _allocation.vestingCliffCredit; - allocation.unlockingCliffCredit = _allocation.unlockingCliffCredit; - allocation.vestingRate = _allocation.vestingRate; - allocation.vestingStartTime = _allocation.vestingStartTime; - allocation.unlockRate = _allocation.unlockRate; - allocation.unlockStartTime = _allocation.unlockStartTime; + // set vesting allocation variables + allocation = _allocation; // set token option variables repurchasePrice = _repurchasePrice; @@ -155,9 +150,9 @@ contract RestrictedTokenAward is BaseAllocation { function claimRepurchasedTokens() external onlyGrantee nonReentrant { if(IERC20M(paymentToken).balanceOf(address(this)) == 0) revert MetaVesT_MoreThanAvailable(); uint256 _amount = IERC20M(paymentToken).balanceOf(address(this)); - safeTransfer(paymentToken, msg.sender, _amount); + safeTransfer(paymentToken, recipient, _amount); tokensRepurchasedWithdrawn += _amount; - emit MetaVesT_Withdrawn(msg.sender, recipient, paymentToken, _amount); // TODO review needed + emit MetaVesT_Withdrawn(grantee, recipient, paymentToken, _amount); } /// @notice Allows the controller to terminate the RestrictedTokenAward diff --git a/src/RestrictedTokenFactory.sol b/src/RestrictedTokenFactory.sol index 614bf0a..db0bfc1 100644 --- a/src/RestrictedTokenFactory.sol +++ b/src/RestrictedTokenFactory.sol @@ -9,7 +9,7 @@ contract RestrictedTokenFactory is IAllocationFactory { function createAllocation( AllocationType _allocationType, address _grantee, - address _recipient, // TODO review needed + address _recipient, address _controller, RestrictedTokenAward.Allocation memory _allocation, RestrictedTokenAward.Milestone[] memory _milestones, diff --git a/src/TokenOptionAllocation.sol b/src/TokenOptionAllocation.sol index 0f1ef7a..b0495ed 100644 --- a/src/TokenOptionAllocation.sol +++ b/src/TokenOptionAllocation.sol @@ -17,6 +17,7 @@ contract TokenOptionAllocation is BaseAllocation { /// @notice Constructor to create a TokenOptionAllocation /// @param _grantee - address of the grantee + /// @param _recipient address of the fund recipient /// @param _controller - address of the controller /// @param _paymentToken - address of the payment token /// @param _exercisePrice - price of the token option exercise in vesting token decimals but only up to payment decimal precision @@ -25,7 +26,7 @@ contract TokenOptionAllocation is BaseAllocation { /// @param _milestones - milestones with conditions and awards constructor ( address _grantee, - address _recipient, // TODO review needed + address _recipient, address _controller, address _paymentToken, uint256 _exercisePrice, @@ -41,16 +42,10 @@ contract TokenOptionAllocation is BaseAllocation { if (_allocation.tokenContract == address(0)) revert MetaVesT_ZeroAddress(); if (_allocation.tokenStreamTotal == 0) revert MetaVesT_ZeroAmount(); if (_allocation.vestingRate > 1000*1e18 || _allocation.unlockRate > 1000*1e18) revert MetaVesT_RateTooHigh(); + if (_allocation.vestingRate < 100 || _allocation.unlockRate < 100) revert MetaVesT_RateTooLow(); - //set vesting allocation variables - allocation.tokenContract = _allocation.tokenContract; - allocation.tokenStreamTotal = _allocation.tokenStreamTotal; - allocation.vestingCliffCredit = _allocation.vestingCliffCredit; - allocation.unlockingCliffCredit = _allocation.unlockingCliffCredit; - allocation.vestingRate = _allocation.vestingRate; - allocation.vestingStartTime = _allocation.vestingStartTime; - allocation.unlockRate = _allocation.unlockRate; - allocation.unlockStartTime = _allocation.unlockStartTime; + // set vesting allocation variables + allocation = _allocation; // set token option variables exercisePrice = _exercisePrice; @@ -143,9 +138,9 @@ contract TokenOptionAllocation is BaseAllocation { uint256 paymentAmount = getPaymentAmount(_tokensToExercise); if(paymentAmount == 0) revert MetaVesT_TooSmallAmount(); - safeTransferFrom(paymentToken, msg.sender, getAuthority(), paymentAmount); + safeTransferFrom(paymentToken, grantee, getAuthority(), paymentAmount); tokensExercised += _tokensToExercise; - emit MetaVesT_TokenOptionExercised(msg.sender, _tokensToExercise, paymentAmount); + emit MetaVesT_TokenOptionExercised(grantee, _tokensToExercise, paymentAmount); } /// @notice Allows the controller to terminate the TokenOptionAllocation diff --git a/src/TokenOptionFactory.sol b/src/TokenOptionFactory.sol index 9e9d67d..10025e3 100644 --- a/src/TokenOptionFactory.sol +++ b/src/TokenOptionFactory.sol @@ -9,7 +9,7 @@ contract TokenOptionFactory is IAllocationFactory { function createAllocation( AllocationType _allocationType, address _grantee, - address _recipient, // TODO review needed + address _recipient, address _controller, TokenOptionAllocation.Allocation memory _allocation, TokenOptionAllocation.Milestone[] memory _milestones, diff --git a/src/VestingAllocation.sol b/src/VestingAllocation.sol index b5c2993..302dcd1 100644 --- a/src/VestingAllocation.sol +++ b/src/VestingAllocation.sol @@ -23,23 +23,15 @@ contract VestingAllocation is BaseAllocation { _recipient, _controller ) { - //perform input validation + // perform input validation if (_allocation.tokenContract == address(0)) revert MetaVesT_ZeroAddress(); if (_allocation.tokenStreamTotal == 0) revert MetaVesT_ZeroAmount(); - if (_grantee == address(0)) revert MetaVesT_ZeroAddress(); - if (_recipient == address(0)) revert MetaVesT_ZeroAddress(); if (_allocation.vestingRate > 1000*1e18 || _allocation.unlockRate > 1000*1e18) revert MetaVesT_RateTooHigh(); if (_allocation.vestingRate < 100 || _allocation.unlockRate < 100) revert MetaVesT_RateTooLow(); - //set vesting allocation variables - allocation.tokenContract = _allocation.tokenContract; - allocation.tokenStreamTotal = _allocation.tokenStreamTotal; - allocation.vestingCliffCredit = _allocation.vestingCliffCredit; - allocation.unlockingCliffCredit = _allocation.unlockingCliffCredit; - allocation.vestingRate = _allocation.vestingRate; - allocation.vestingStartTime = _allocation.vestingStartTime; - allocation.unlockRate = _allocation.unlockRate; - allocation.unlockStartTime = _allocation.unlockStartTime; + // set vesting allocation variables + allocation = _allocation; + // manually copy milestones for (uint256 i; i < _milestones.length; ++i) { milestones.push(_milestones[i]); diff --git a/test/controller.t.sol b/test/controller.t.sol index bf9b8be..5010d38 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -527,6 +527,33 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertEq(paymentToken.balanceOf(grantee), startingPaymentTokenBalance + payment); } + function testRepurchaseTokensSpecifiedRecipient() public { + uint256 startingPaymentTokenBalance = paymentToken.balanceOf(grantee); + address restrictedTokenAward = createDummyRestrictedTokenAward(bob); // set bob as the recipient + uint256 repurchaseAmount = 5e18; + uint256 startingVestingTokenBalance = vestingToken.balanceOf(authority); + uint256 payment = RestrictedTokenAward(restrictedTokenAward).getPaymentAmount(repurchaseAmount); + + vm.startPrank(authority); + + controller.terminateMetavestVesting(restrictedTokenAward); + paymentToken.approve(address(restrictedTokenAward), payment); + + vm.warp(block.timestamp + 20 days); + + RestrictedTokenAward(restrictedTokenAward).repurchaseTokens(repurchaseAmount); + + vm.stopPrank(); + + assertEq(vestingToken.balanceOf(authority), startingVestingTokenBalance + repurchaseAmount); + + vm.prank(grantee); + vm.expectEmit(true, true, true, true); + emit BaseAllocation.MetaVesT_Withdrawn(grantee, bob, address(paymentToken), payment); + RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); + assertEq(paymentToken.balanceOf(bob) - startingPaymentTokenBalance, payment, "Bob should receive the payment as the specified recipient"); + } + function testRepurchaseTokensFuture() public { uint256 startingPaymentTokenBalance = paymentToken.balanceOf(grantee); address restrictedTokenAward = createDummyRestrictedTokenAwardFuture(); @@ -825,11 +852,15 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { ); } - function createDummyRestrictedTokenAward() internal returns (address) { - return createDummyRestrictedTokenAward(""); - } + function createDummyRestrictedTokenAward() internal returns (address) { + return createDummyRestrictedTokenAward(alice, ""); + } + + function createDummyRestrictedTokenAward(address recipient) internal returns (address) { + return createDummyRestrictedTokenAward(recipient, ""); + } - function createDummyRestrictedTokenAward(bytes memory expectRevertData) internal returns (address) { + function createDummyRestrictedTokenAward(address recipient, bytes memory expectRevertData) internal returns (address) { BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); milestones[0] = BaseAllocation.Milestone({ milestoneAward: 1000e18, @@ -867,7 +898,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { return _granteeSignDeal( contractIdAlice, alice, // grantee - alice, // recipient + recipient, alicePrivateKey, "Alice" ); From 25b73574c41e46c4acf3594689e0991c91a36135 Mon Sep 17 00:00:00 2001 From: detoo Date: Mon, 27 Oct 2025 14:21:10 -0700 Subject: [PATCH 15/25] chore: simplify utility functions for testing --- test/amendement.t.sol | 28 ++- test/controller.t.sol | 314 +++++++++++++----------- test/lib/MetaVesTControllerTestBase.sol | 36 +-- 3 files changed, 193 insertions(+), 185 deletions(-) diff --git a/test/amendement.t.sol b/test/amendement.t.sol index 78e4b00..adbb90a 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -12,6 +12,8 @@ import "./lib/MetaVesTControllerTestBase.sol"; // TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry contract MetaVestControllerTest is MetaVesTControllerTestBase { + using MetaVestDealLib for MetaVestDeal; + address public authority = guardianSafe; address public dao = guardianSafe; address public grantee = alice; @@ -376,18 +378,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, agreementSaltCounter++, delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), "Alice", cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible ); diff --git a/test/controller.t.sol b/test/controller.t.sol index 5010d38..9005794 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -81,18 +81,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 60 ether, - vestingCliffCredit: 30 ether, - unlockingCliffCredit: 30 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0), + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 60 ether, + vestingCliffCredit: 30 ether, + unlockingCliffCredit: 30 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), "Alice", metavestExpiry ); @@ -630,18 +632,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), "Alice", metavestExpiry ); @@ -671,18 +675,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), "Alice", metavestExpiry ); @@ -711,18 +717,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 5 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 5 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), "Alice", metavestExpiry ); @@ -745,18 +753,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 0 ether, - unlockingCliffCredit: 0 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 0 ether, + unlockingCliffCredit: 0 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), "Alice", metavestExpiry ); @@ -779,18 +789,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 0 ether, - unlockingCliffCredit: 0 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp + 2000), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp + 2000) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 0 ether, + unlockingCliffCredit: 0 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp + 2000), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp + 2000) + }), + milestones + ), "Alice", metavestExpiry ); @@ -972,18 +984,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(0), // zero address - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(0), // zero address + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), "Alice", metavestExpiry ); @@ -1606,18 +1620,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt alicePrivateKey, // Should fail because Alice is not delegated by the grantor - alice, // grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0), + MetaVestDealLib.draft().setVesting( + alice, // grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), "Alice", block.timestamp + 7 days, abi.encodeWithSelector(CyberAgreementRegistry.SignatureVerificationFailed.selector) // Expected revert @@ -1629,18 +1645,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0), + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), "Alice", block.timestamp + 7 days ); @@ -1667,18 +1685,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0), + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), "Alice", metavestExpiry ); @@ -1706,18 +1726,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0), + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), "Alice", metavestExpiry ); @@ -1800,19 +1822,21 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - // 100k ZK total, the first half unlocks with a cliff and the second half unlocks over an year - tokenStreamTotal: 60 ether, - vestingCliffCredit: 30 ether, - unlockingCliffCredit: 30 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0), + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + // 100k ZK total, the first half unlocks with a cliff and the second half unlocks over an year + tokenStreamTotal: 60 ether, + vestingCliffCredit: 30 ether, + unlockingCliffCredit: 30 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), "Alice", metavestExpiry ); diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 15c4249..f4434fc 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -103,50 +103,30 @@ contract MetaVesTControllerTestBase is Test { assertEq(vestingToken.balanceOf(address(vestingAllocation)), 0, string(abi.encodePacked(assertName, ": vesting contract should not have any token (it mints on-demand)"))); } + /// @notice Shortcut for: + /// - no revert + /// - automatically generate the correct parties function _proposeAndSignDeal( bytes32 templateId, uint256 agreementSalt, uint256 grantorOrDelegatePrivateKey, - address grantee, - BaseAllocation.Allocation memory allocation, - BaseAllocation.Milestone[] memory milestones, + MetaVestDeal memory dealDraft, string memory partyName, uint256 expiry - ) internal returns(bytes32) { - return _proposeAndSignDeal( - templateId, agreementSalt, grantorOrDelegatePrivateKey, grantee, allocation, milestones, partyName, expiry, - "" // Not expecting revert - ); - } - - // TODO WIP: temporarily for backwards compatibility - function _proposeAndSignDeal( - bytes32 templateId, - uint256 agreementSalt, - uint256 grantorOrDelegatePrivateKey, - address grantee, - BaseAllocation.Allocation memory allocation, - BaseAllocation.Milestone[] memory milestones, - string memory partyName, - uint256 expiry, - bytes memory expectRevertData ) internal returns(bytes32) { return _proposeAndSignDeal( templateId, agreementSalt, grantorOrDelegatePrivateKey, - MetaVestDealLib.draft().setVesting( - grantee, - allocation, - milestones - ), + dealDraft, partyName, expiry, - expectRevertData + "" ); } - /// @notice Shortcut for automatically generate the correct parties + /// @notice Shortcut for: + /// - automatically generate the correct parties function _proposeAndSignDeal( bytes32 templateId, uint256 agreementSalt, From 429291547bd9f1be4852255c8bb9be33a40f25b1 Mon Sep 17 00:00:00 2001 From: detoo Date: Mon, 27 Oct 2025 15:09:28 -0700 Subject: [PATCH 16/25] test: re-enable more TokenOption and RestrictedToken tests --- test/AuditBaseA2.t.sol | 95 +-- test/AuditBaseC.t.sol | 70 ++- test/AuditBaseC3.t.sol | 115 ++-- test/amendement.t.sol | 772 ++++++++++++------------ test/lib/MetaVesTControllerTestBase.sol | 1 - 5 files changed, 551 insertions(+), 502 deletions(-) diff --git a/test/AuditBaseA2.t.sol b/test/AuditBaseA2.t.sol index 61d6fb4..b1bea03 100644 --- a/test/AuditBaseA2.t.sol +++ b/test/AuditBaseA2.t.sol @@ -14,7 +14,6 @@ contract EvilGrant { } } -// TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry contract Audit is MetaVestControllerTest { function test_RevertIf_AuditArbitraryVote() public { // template from testVoteOnMetavestAmendment @@ -82,51 +81,55 @@ contract Audit is MetaVestControllerTest { controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); } -// function testAuditModifiedCalldataProposal() public { -// // template from testCreateSetWithThreeTokenOptionsAndChangeExercisePrice -// address allocation1 = createDummyTokenOptionAllocation(); -// address allocation2 = createDummyTokenOptionAllocation(); -// address allocation3 = createDummyTokenOptionAllocation(); -// -// vm.prank(authority); -// controller.addMetaVestToSet("testSet", allocation1); -// controller.addMetaVestToSet("testSet", allocation2); -// controller.addMetaVestToSet("testSet", allocation3); -// assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 1e18); -// vm.warp(block.timestamp + 25 seconds); -// -// -// vm.startPrank(grantee); -// ERC20(paymentToken).approve(address(allocation1), 2000e18); -// ERC20(paymentToken).approve(address(allocation2), 2000e18); -// -// TokenOptionAllocation(allocation1).exerciseTokenOption(TokenOptionAllocation(allocation1).getAmountExercisable()); -// -// TokenOptionAllocation(allocation2).exerciseTokenOption(TokenOptionAllocation(allocation2).getAmountExercisable()); -// vm.stopPrank(); -// bytes4 msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, allocation1, 2e18); -// -// vm.prank(authority); -// controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); -// -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(allocation1, "testSet", msgSig, true); -// -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(allocation2, "testSet", msgSig, true); -// -// vm.prank(authority); -// vm.expectRevert(); -// controller.updateExerciseOrRepurchasePrice(allocation1, 999999999999999999999e18); -// -// // Bypass MetaVesTController_AmendmentNeitherMutualNorMajorityConsented -// vm.prank(authority); -// bytes memory p = abi.encodeWithSelector(msgSig, allocation1, 999999999999999999999e18, 2e18); -// address(controller).call(p); -// -// console.log('Modified excercise price: ', TokenOptionAllocation(allocation1).exercisePrice()); -// } + function testAuditModifiedCalldataProposal() public { + // template from testCreateSetWithThreeTokenOptionsAndChangeExercisePrice + address allocation1 = createDummyTokenOptionAllocation(); + address allocation2 = createDummyTokenOptionAllocation(); + address allocation3 = createDummyTokenOptionAllocation(); + + vm.startPrank(authority); + controller.addMetaVestToSet("testSet", allocation1); + controller.addMetaVestToSet("testSet", allocation2); + controller.addMetaVestToSet("testSet", allocation3); + vm.stopPrank(); + assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 1e18); + + vm.warp(block.timestamp + 25 seconds); + + paymentToken.mint(grantee, 4000e18); + + vm.startPrank(grantee); + paymentToken.approve(address(allocation1), 2000e18); + paymentToken.approve(address(allocation2), 2000e18); + TokenOptionAllocation(allocation1).exerciseTokenOption(TokenOptionAllocation(allocation1).getAmountExercisable()); + TokenOptionAllocation(allocation2).exerciseTokenOption(TokenOptionAllocation(allocation2).getAmountExercisable()); + vm.stopPrank(); + + bytes4 msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); + bytes memory callData = abi.encodeWithSelector(msgSig, allocation1, 2e18); + + vm.prank(authority); + controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); + + vm.prank(grantee); + controller.voteOnMetavestAmendment(allocation1, "testSet", msgSig, true); + + vm.prank(grantee); + controller.voteOnMetavestAmendment(allocation2, "testSet", msgSig, true); + + // Call function with a different value from the consent should fail + vm.prank(authority); + vm.expectRevert(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector); + controller.updateExerciseOrRepurchasePrice(allocation1, 999999999999999999999e18); + + // Using lower-level call would still fail internally + vm.prank(authority); + bytes memory p = abi.encodeWithSelector(msgSig, allocation1, 999999999999999999999e18); + address(controller).call(p); + + // Verify exercise price is still not changed + assertEq(TokenOptionAllocation(allocation1).exercisePrice(), 1e18, "exercise price should not change"); + } function testAuditConsentToMetavestAmendmentInFlavor() public { // template from testRemoveMilestone diff --git a/test/AuditBaseC.t.sol b/test/AuditBaseC.t.sol index 7f2d29d..f4afe3c 100644 --- a/test/AuditBaseC.t.sol +++ b/test/AuditBaseC.t.sol @@ -4,7 +4,6 @@ import "forge-std/Test.sol"; import "../test/controller.t.sol"; -// TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry contract Audit is MetaVestControllerTest { function test_RevertIf_AuditTerminateFailAfterWithdraw() public { @@ -45,34 +44,43 @@ contract Audit is MetaVestControllerTest { vm.stopPrank(); } -// function testAuditTerminateFailAfterWithdrawFixCheckOptions() public { -// // template from testTerminateVestAndRecoverSlowUnlock -// address vestingAllocation = createDummyTokenOptionAllocation(); -// uint256 snapshot = token.balanceOf(authority); -// VestingAllocation(vestingAllocation).confirmMilestone(0); -// vm.warp(block.timestamp + 5 seconds); -// vm.startPrank(grantee); -// ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(TokenOptionAllocation(vestingAllocation).getAmountExercisable())); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(TokenOptionAllocation(vestingAllocation).getAmountExercisable()); -// TokenOptionAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.warp(block.timestamp + 5 seconds); -// ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(TokenOptionAllocation(vestingAllocation).getAmountExercisable())); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(TokenOptionAllocation(vestingAllocation).getAmountExercisable()); -// TokenOptionAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// vm.warp(block.timestamp + 5 seconds); -// controller.terminateMetavestVesting(vestingAllocation); -// -// vm.startPrank(grantee); -// ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(TokenOptionAllocation(vestingAllocation).getAmountExercisable())); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(TokenOptionAllocation(vestingAllocation).getAmountExercisable()); -// TokenOptionAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// vm.warp(block.timestamp + 365 days); -// -// vm.prank(authority); -// TokenOptionAllocation(vestingAllocation).recoverForfeitTokens(); -// //check balance of the vesting contract -// assertEq(token.balanceOf(vestingAllocation), 0); -// } + function testAuditTerminateFailAfterWithdrawFixCheckOptions() public { + // template from testTerminateVestAndRecoverSlowUnlock + address metavest = createDummyTokenOptionAllocation(); + TokenOptionAllocation(metavest).confirmMilestone(0); + + paymentToken.mint(grantee, 2000e18); + + vm.warp(block.timestamp + 5 seconds); + + vm.startPrank(grantee); + paymentToken.approve(metavest, TokenOptionAllocation(metavest).getPaymentAmount(TokenOptionAllocation(metavest).getAmountExercisable())); + TokenOptionAllocation(metavest).exerciseTokenOption(TokenOptionAllocation(metavest).getAmountExercisable()); + TokenOptionAllocation(metavest).withdraw(TokenOptionAllocation(metavest).getAmountWithdrawable()); + + vm.warp(block.timestamp + 5 seconds); + + paymentToken.approve(metavest, TokenOptionAllocation(metavest).getPaymentAmount(TokenOptionAllocation(metavest).getAmountExercisable())); + TokenOptionAllocation(metavest).exerciseTokenOption(TokenOptionAllocation(metavest).getAmountExercisable()); + TokenOptionAllocation(metavest).withdraw(TokenOptionAllocation(metavest).getAmountWithdrawable()); + vm.stopPrank(); + + vm.warp(block.timestamp + 5 seconds); + vm.prank(authority); + controller.terminateMetavestVesting(metavest); + + vm.startPrank(grantee); + paymentToken.approve(metavest, TokenOptionAllocation(metavest).getPaymentAmount(TokenOptionAllocation(metavest).getAmountExercisable())); + TokenOptionAllocation(metavest).exerciseTokenOption(TokenOptionAllocation(metavest).getAmountExercisable()); + TokenOptionAllocation(metavest).withdraw(TokenOptionAllocation(metavest).getAmountWithdrawable()); + vm.stopPrank(); + + vm.warp(block.timestamp + 365 days); + + vm.prank(authority); + TokenOptionAllocation(metavest).recoverForfeitTokens(); + + //check balance of the vesting contract + assertEq(paymentToken.balanceOf(metavest), 0); + } } \ No newline at end of file diff --git a/test/AuditBaseC3.t.sol b/test/AuditBaseC3.t.sol index cfe44fa..d18d5b7 100644 --- a/test/AuditBaseC3.t.sol +++ b/test/AuditBaseC3.t.sol @@ -4,7 +4,6 @@ import "forge-std/Test.sol"; import "../test/controller.t.sol"; -// TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry contract Audit is MetaVestControllerTest { function test_RevertIf_AuditTerminateFailAfterWithdraw() public { @@ -51,55 +50,67 @@ contract Audit is MetaVestControllerTest { // assertEq(token.balanceOf(vestingAllocation), 0); } -// function test_RevertIf_AuditRounding() public { -// // template from testConfirmingMilestoneTokenOption -// address vestingAllocation = createDummyTokenOptionAllocation(); -// uint256 snapshot = token.balanceOf(authority); -// TokenOptionAllocation(vestingAllocation).confirmMilestone(0); -// vm.warp(block.timestamp + 50 seconds); -// vm.startPrank(grantee); -// //exercise max available -// ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(TokenOptionAllocation(vestingAllocation).getAmountExercisable())); -// -// console.log('before amount of payment token:', ERC20Stable(paymentToken).balanceOf(grantee)); -// console.log('before tokensExercised: ', TokenOptionAllocation(vestingAllocation).tokensExercised()); -// console.log('small amount payment: ', TokenOptionAllocation(vestingAllocation).getPaymentAmount(1e6)); -// console.log('small amount payment: ', TokenOptionAllocation(vestingAllocation).getPaymentAmount(1e11)); -// console.log('small amount payment: ', TokenOptionAllocation(vestingAllocation).getPaymentAmount(9.99e11)); -// console.log('small amount payment: ', TokenOptionAllocation(vestingAllocation).getPaymentAmount(1e12)); -// console.log('small amount payment: ', TokenOptionAllocation(vestingAllocation).getPaymentAmount(1.1e12)); -// console.log('small amount payment: ', TokenOptionAllocation(vestingAllocation).getPaymentAmount(1e13)); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1.1e12); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e6); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e6); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e6); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e6); -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e6); -// -// console.log('after amount of payment token: ', ERC20Stable(paymentToken).balanceOf(grantee)); -// console.log('after tokensExercised: ', TokenOptionAllocation(vestingAllocation).tokensExercised()); -// // TokenOptionAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// } -// -// function testAuditExcercisePrice() public { -// // template from testConfirmingMilestoneTokenOption -// address vestingAllocation = createDummyTokenOptionAllocation(); -// uint256 snapshot = token.balanceOf(authority); -// TokenOptionAllocation(vestingAllocation).confirmMilestone(0); -// vm.warp(block.timestamp + 50 seconds); -// vm.startPrank(grantee); -// //exercise max available -// ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(TokenOptionAllocation(vestingAllocation).getAmountExercisable())); -// -// console.log('before amount of payment token:', ERC20Stable(paymentToken).balanceOf(grantee)); -// console.log('before tokensExercised: ', TokenOptionAllocation(vestingAllocation).tokensExercised()); -// -// TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e18); -// -// console.log('after amount of payment token: ', ERC20Stable(paymentToken).balanceOf(grantee)); -// console.log('after tokensExercised: ', TokenOptionAllocation(vestingAllocation).tokensExercised()); -// // TokenOptionAllocation(vestingAllocation).withdraw(VestingAllocation(vestingAllocation).getAmountWithdrawable()); -// vm.stopPrank(); -// } + function test_AuditRounding() public { + // template from testConfirmingMilestoneTokenOption + address metavest = createDummyTokenOptionAllocation(); + TokenOptionAllocation(metavest).confirmMilestone(0); + + vm.warp(block.timestamp + 50 seconds); + + paymentToken.mint(grantee, 550_002_500_000); + + vm.startPrank(grantee); + //exercise max available + paymentToken.approve(metavest, TokenOptionAllocation(metavest).getPaymentAmount(TokenOptionAllocation(metavest).getAmountExercisable())); + + console2.log('before amount of payment token:', paymentToken.balanceOf(grantee)); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 0, "no token exercised yet"); + assertEq(TokenOptionAllocation(metavest).getPaymentAmount(1e6), 5e5); + assertEq(TokenOptionAllocation(metavest).getPaymentAmount(1e11), 5e10); + assertEq(TokenOptionAllocation(metavest).getPaymentAmount(9.99e11), 4.995e11); + assertEq(TokenOptionAllocation(metavest).getPaymentAmount(1e12), 5e11); + assertEq(TokenOptionAllocation(metavest).getPaymentAmount(1.1e12), 5.5e11); + assertEq(TokenOptionAllocation(metavest).getPaymentAmount(1e13), 5e12); + TokenOptionAllocation(metavest).exerciseTokenOption(1.1e12); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1.1e12); + TokenOptionAllocation(metavest).exerciseTokenOption(1e6); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1100001e6); + TokenOptionAllocation(metavest).exerciseTokenOption(1e6); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1100002e6); + TokenOptionAllocation(metavest).exerciseTokenOption(1e6); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1100003e6); + TokenOptionAllocation(metavest).exerciseTokenOption(1e6); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1100004e6); + TokenOptionAllocation(metavest).exerciseTokenOption(1e6); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1100005e6); + + console2.log('after amount of payment token: ', paymentToken.balanceOf(grantee)); + assertEq(TokenOptionAllocation(metavest).getAmountWithdrawable(), 1100005e6, "should be able to withdraw all exercised"); + TokenOptionAllocation(metavest).withdraw(TokenOptionAllocation(metavest).getAmountWithdrawable()); + vm.stopPrank(); + } + + function testAuditExercisePrice() public { + // template from testConfirmingMilestoneTokenOption + address metavest = createDummyTokenOptionAllocation(); + TokenOptionAllocation(metavest).confirmMilestone(0); + + vm.warp(block.timestamp + 50 seconds); + + paymentToken.mint(grantee, 0.5e18); + + vm.startPrank(grantee); + //exercise max available + paymentToken.approve(metavest, TokenOptionAllocation(metavest).getPaymentAmount(TokenOptionAllocation(metavest).getAmountExercisable())); + + console2.log('before amount of payment token:', paymentToken.balanceOf(grantee)); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 0, "no token exercised yet"); + + TokenOptionAllocation(metavest).exerciseTokenOption(1e18); + + console2.log('after amount of payment token: ', paymentToken.balanceOf(grantee)); + assertEq(TokenOptionAllocation(metavest).tokensExercised(), 1e18, "should have exercised"); + TokenOptionAllocation(metavest).withdraw(TokenOptionAllocation(metavest).getAmountWithdrawable()); + vm.stopPrank(); + } } diff --git a/test/amendement.t.sol b/test/amendement.t.sol index adbb90a..fc2d9a2 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/BaseAllocation.sol"; -//import "../src/RestrictedTokenAllocation.sol"; +import "../src/TokenOptionAllocation.sol"; +import "../src/RestrictedTokenAllocation.sol"; import "../src/interfaces/IAllocationFactory.sol"; import "../src/VestingAllocationFactory.sol"; -//import "../src/TokenOptionFactory.sol"; -//import "../src/RestrictedTokenFactory.sol"; +import "../src/TokenOptionFactory.sol"; +import "../src/RestrictedTokenFactory.sol"; import "./lib/MetaVesTControllerTestBase.sol"; -// TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry contract MetaVestControllerTest is MetaVesTControllerTestBase { using MetaVestDealLib for MetaVestDeal; @@ -19,8 +19,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { address public grantee = alice; uint256 cap = 2000 ether; - uint48 cappedMinterStartTime = uint48(block.timestamp); // Minter start now - uint48 cappedMinterExpirationTime = uint48(cappedMinterStartTime + 1600); // Minter expired 1600 seconds after start + uint48 metavestExpiry = uint48(block.timestamp + 1600); // Expired 1600 seconds later address public vestingAllocation; @@ -34,6 +33,8 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Deploy MetaVesT controller vestingAllocationFactory = new VestingAllocationFactory(); + tokenOptionFactory = new TokenOptionFactory(); + restrictedTokenFactory = new RestrictedTokenFactory(); controller = metavestController(address(new ERC1967Proxy{salt: salt}( address(new metavestController{salt: salt}()), @@ -142,64 +143,77 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.updateMetavestTransferability(mockAllocation2, true); } + function testMajorityPowerMetavestAmendment() public { + address mockAllocation2 = createDummyTokenOptionAllocation(); + address mockAllocation3 = createDummyTokenOptionAllocation(); + address mockAllocation4 = createDummyTokenOptionAllocation(); + bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); + bytes memory callData = abi.encodeWithSelector(msgSig, mockAllocation2, true); + + vm.startPrank(authority); + controller.addMetaVestToSet("testSet", mockAllocation2); + controller.addMetaVestToSet("testSet", mockAllocation3); + controller.addMetaVestToSet("testSet", mockAllocation4); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 days); -// function testMajorityPowerMetavestAmendment() public { -// address mockAllocation2 = createDummyTokenOptionAllocation(); -// address mockAllocation3 = createDummyTokenOptionAllocation(); -// address mockAllocation4 = createDummyTokenOptionAllocation(); -// bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, mockAllocation2, true); -// -// vm.prank(authority); -// controller.addMetaVestToSet("testSet", mockAllocation2); -// controller.addMetaVestToSet("testSet", mockAllocation3); -// controller.addMetaVestToSet("testSet", mockAllocation4); -// vm.warp(block.timestamp + 1 days); -// vm.startPrank(grantee); -// ERC20(paymentToken).approve(address(mockAllocation2), 2000e18); -// TokenOptionAllocation(mockAllocation2).exerciseTokenOption(TokenOptionAllocation(mockAllocation2).getAmountExercisable()); -// vm.stopPrank(); -// vm.prank(authority); -// controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); -// -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(mockAllocation3, "testSet", msgSig, true); -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(mockAllocation4, "testSet", msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestTransferability(mockAllocation2, true); -// } - -// function test_RevertIf_MajorityPowerMetavestAmendment() public { -// address mockAllocation2 = createDummyTokenOptionAllocation(); -// address mockAllocation3 = createDummyTokenOptionAllocation(); -// address mockAllocation4 = createDummyTokenOptionAllocation(); -// bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, mockAllocation2, true); -// -// vm.prank(authority); -// controller.addMetaVestToSet("testSet", mockAllocation2); -// controller.addMetaVestToSet("testSet", mockAllocation3); -// controller.addMetaVestToSet("testSet", mockAllocation4); -// vm.warp(block.timestamp + 1 days); -// vm.startPrank(grantee); -// ERC20(paymentToken).approve(address(mockAllocation2), 2000e18); -// TokenOptionAllocation(mockAllocation2).exerciseTokenOption(TokenOptionAllocation(mockAllocation2).getAmountExercisable()); -// vm.stopPrank(); -// vm.prank(authority); -// controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); -// -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(mockAllocation3, "testSet", msgSig, true); -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(mockAllocation4, "testSet", msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestTransferability(mockAllocation2, true); -// vm.prank(authority); -// controller.updateMetavestTransferability(mockAllocation2, true); -// } + paymentToken.mint(grantee, 2000e18); + + vm.startPrank(grantee); + paymentToken.approve(address(mockAllocation2), 2000e18); + TokenOptionAllocation(mockAllocation2).exerciseTokenOption(TokenOptionAllocation(mockAllocation2).getAmountExercisable()); + vm.stopPrank(); + + vm.prank(authority); + controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); + + vm.prank(grantee); + controller.voteOnMetavestAmendment(mockAllocation3, "testSet", msgSig, true); + vm.prank(grantee); + controller.voteOnMetavestAmendment(mockAllocation4, "testSet", msgSig, true); + + vm.prank(authority); + controller.updateMetavestTransferability(mockAllocation2, true); + } + + function test_RevertIf_MajorityPowerMetavestAmendmentMoreThanOnce() public { + address mockAllocation2 = createDummyTokenOptionAllocation(); + address mockAllocation3 = createDummyTokenOptionAllocation(); + address mockAllocation4 = createDummyTokenOptionAllocation(); + bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); + bytes memory callData = abi.encodeWithSelector(msgSig, mockAllocation2, true); + + vm.startPrank(authority); + controller.addMetaVestToSet("testSet", mockAllocation2); + controller.addMetaVestToSet("testSet", mockAllocation3); + controller.addMetaVestToSet("testSet", mockAllocation4); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 days); + + paymentToken.mint(grantee, 2000e18); + + vm.startPrank(grantee); + paymentToken.approve(address(mockAllocation2), 2000e18); + TokenOptionAllocation(mockAllocation2).exerciseTokenOption(TokenOptionAllocation(mockAllocation2).getAmountExercisable()); + vm.stopPrank(); + + vm.prank(authority); + controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); + + vm.prank(grantee); + controller.voteOnMetavestAmendment(mockAllocation3, "testSet", msgSig, true); + vm.prank(grantee); + controller.voteOnMetavestAmendment(mockAllocation4, "testSet", msgSig, true); + + vm.prank(authority); + controller.updateMetavestTransferability(mockAllocation2, true); + + vm.prank(authority); + vm.expectRevert(MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector); + controller.updateMetavestTransferability(mockAllocation2, true); + } function testProposeMajorityMetavestAmendment() public { address mockAllocation2 = createDummyVestingAllocation(); @@ -393,7 +407,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { milestones ), "Alice", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry // Same expiry as the minter so grantee can defer vesting contract creation as much as possible ); return _granteeSignDeal( @@ -406,74 +420,97 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { ); } -// function createDummyTokenOptionAllocation() internal returns (address) { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(token), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 100e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 1100e18); -// -// return controller.createMetavest( -// MetaVestType.TokenOption, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 365 days, -// 0 -// ); -// } -// -// function createDummyRestrictedTokenAward() internal returns (address) { -// BaseAllocation.Allocation memory allocation = BaseAllocation.Allocation({ -// tokenContract: address(token), -// tokenStreamTotal: 1000e18, -// vestingCliffCredit: 100e18, -// unlockingCliffCredit: 100e18, -// vestingRate: 10e18, -// vestingStartTime: uint48(block.timestamp), -// unlockRate: 10e18, -// unlockStartTime: uint48(block.timestamp) -// }); -// -// BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); -// milestones[0] = BaseAllocation.Milestone({ -// milestoneAward: 100e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 1100e18); -// -// return controller.createMetavest( -// MetaVestType.RestrictedTokenAward, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 365 days, -// 0 -// -// ); -// } + function createDummyTokenOptionAllocation() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 100e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + vestingToken.approve(address(controller), 1100e18); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + agreementSaltCounter++, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setTokenOption( + alice, + address(paymentToken), + 1e18, // exercisePrice + 365 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry, + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + function createDummyRestrictedTokenAward() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 100e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + vestingToken.approve(address(controller), 1100e18); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + agreementSaltCounter++, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setRestrictedToken( + alice, + address(paymentToken), + 1e18, // exercisePrice + 365 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry, + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } //write a test for every consentcheck function in metavest controller function testConsentCheck() public { @@ -556,97 +593,88 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.voteOnMetavestAmendment(allocation, "testSet", msgSig, true); } -// function testCreateSetWithThreeTokenOptionsAndChangeExercisePrice() public { -// address allocation1 = createDummyTokenOptionAllocation(); -// address allocation2 = createDummyTokenOptionAllocation(); -// address allocation3 = createDummyTokenOptionAllocation(); -// -// vm.prank(authority); -// controller.addMetaVestToSet("testSet", allocation1); -// controller.addMetaVestToSet("testSet", allocation2); -// controller.addMetaVestToSet("testSet", allocation3); -// assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 1e18); -// vm.warp(block.timestamp + 25 seconds); -// -// -// vm.startPrank(grantee); -// ERC20(paymentToken).approve(address(allocation1), 2000e18); -// ERC20(paymentToken).approve(address(allocation2), 2000e18); -// -// TokenOptionAllocation(allocation1).exerciseTokenOption(TokenOptionAllocation(allocation1).getAmountExercisable()); -// -// TokenOptionAllocation(allocation2).exerciseTokenOption(TokenOptionAllocation(allocation2).getAmountExercisable()); -// vm.stopPrank(); -// bytes4 msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, allocation1, 2e18); -// -// vm.prank(authority); -// controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); -// -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(allocation1, "testSet", msgSig, true); -// -// vm.prank(grantee); -// controller.voteOnMetavestAmendment(allocation2, "testSet", msgSig, true); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation1, 2e18); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation2, 2e18); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation3, 2e18); -// -// // Check that the exercise price was updated -// assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 2e18); -// } - -// function test_RevertIf_CreateSetWithThreeTokenOptionsAndChangeExercisePrice() public { -// address allocation1 = createDummyTokenOptionAllocation(); -// address allocation2 = createDummyTokenOptionAllocation(); -// address allocation3 = createDummyTokenOptionAllocation(); -// -// vm.prank(authority); -// controller.addMetaVestToSet("testSet", allocation1); -// controller.addMetaVestToSet("testSet", allocation2); -// controller.addMetaVestToSet("testSet", allocation3); -// assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 1e18); -// vm.warp(block.timestamp + 25 seconds); -// -// -// vm.startPrank(grantee); -// ERC20(paymentToken).approve(address(allocation1), 2000e18); -// ERC20(paymentToken).approve(address(allocation2), 2000e18); -// -// TokenOptionAllocation(allocation1).exerciseTokenOption(TokenOptionAllocation(allocation1).getAmountExercisable()); -// -// TokenOptionAllocation(allocation2).exerciseTokenOption(TokenOptionAllocation(allocation2).getAmountExercisable()); -// vm.stopPrank(); -// bytes4 msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, allocation1, 2e18); -// -// vm.prank(authority); -// controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); -// -// //vm.prank(grantee); -// // controller.voteOnMetavestAmendment(allocation1, "testSet", msgSig, true); -// -// // vm.prank(grantee); -// // controller.voteOnMetavestAmendment(allocation2, "testSet", msgSig, true); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation1, 2e18); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation2, 2e18); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation3, 2e18); -// -// // Check that the exercise price was updated -// assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 2e18); -// } + function testCreateSetWithThreeTokenOptionsAndChangeExercisePrice() public { + address allocation1 = createDummyTokenOptionAllocation(); + address allocation2 = createDummyTokenOptionAllocation(); + address allocation3 = createDummyTokenOptionAllocation(); + + vm.startPrank(authority); + controller.addMetaVestToSet("testSet", allocation1); + controller.addMetaVestToSet("testSet", allocation2); + controller.addMetaVestToSet("testSet", allocation3); + vm.stopPrank(); + assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 1e18); + + vm.warp(block.timestamp + 25 seconds); + + paymentToken.mint(grantee, 4000e18); + + vm.startPrank(grantee); + paymentToken.approve(address(allocation1), 2000e18); + paymentToken.approve(address(allocation2), 2000e18); + TokenOptionAllocation(allocation1).exerciseTokenOption(TokenOptionAllocation(allocation1).getAmountExercisable()); + TokenOptionAllocation(allocation2).exerciseTokenOption(TokenOptionAllocation(allocation2).getAmountExercisable()); + vm.stopPrank(); + + bytes4 msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); + bytes memory callData = abi.encodeWithSelector(msgSig, allocation1, 2e18); + + vm.prank(authority); + controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); + + vm.prank(grantee); + controller.voteOnMetavestAmendment(allocation1, "testSet", msgSig, true); + + vm.prank(grantee); + controller.voteOnMetavestAmendment(allocation2, "testSet", msgSig, true); + + vm.prank(authority); + controller.updateExerciseOrRepurchasePrice(allocation1, 2e18); + + vm.prank(authority); + controller.updateExerciseOrRepurchasePrice(allocation2, 2e18); + + vm.prank(authority); + controller.updateExerciseOrRepurchasePrice(allocation3, 2e18); + + // Check that the exercise price was updated + assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 2e18); + } + + function test_RevertIf_CreateSetWithThreeTokenOptionsAndChangeExercisePriceNoConsent() public { + address allocation1 = createDummyTokenOptionAllocation(); + address allocation2 = createDummyTokenOptionAllocation(); + address allocation3 = createDummyTokenOptionAllocation(); + + vm.startPrank(authority); + controller.addMetaVestToSet("testSet", allocation1); + controller.addMetaVestToSet("testSet", allocation2); + controller.addMetaVestToSet("testSet", allocation3); + vm.stopPrank(); + assertTrue(TokenOptionAllocation(allocation1).exercisePrice() == 1e18); + + vm.warp(block.timestamp + 25 seconds); + + paymentToken.mint(grantee, 4000e18); + + vm.startPrank(grantee); + paymentToken.approve(address(allocation1), 2000e18); + paymentToken.approve(address(allocation2), 2000e18); + TokenOptionAllocation(allocation1).exerciseTokenOption(TokenOptionAllocation(allocation1).getAmountExercisable()); + TokenOptionAllocation(allocation2).exerciseTokenOption(TokenOptionAllocation(allocation2).getAmountExercisable()); + vm.stopPrank(); + + bytes4 msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); + bytes memory callData = abi.encodeWithSelector(msgSig, allocation1, 2e18); + + vm.prank(authority); + controller.proposeMajorityMetavestAmendment("testSet", msgSig, callData); + + // Straight to change without consent should fail + vm.prank(authority); + vm.expectRevert(MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector); + controller.updateExerciseOrRepurchasePrice(allocation1, 2e18); + } function test_RevertIf_consentToNoPendingAmendment() public { address allocation = createDummyVestingAllocation(); @@ -658,155 +686,155 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { controller.consentToMetavestAmendment(allocation, msgSig, true); } -// function testEveryUpdateAmendmentFunction() public { -// address allocation = createDummyTokenOptionAllocation(); -// bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, allocation, true); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestTransferability(allocation, true); -// -// msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 2e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation, 2e18); -// -// msgSig = bytes4(keccak256("removeMetavestMilestone(address,uint256)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 0); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.removeMetavestMilestone(allocation, 0); -// -// msgSig = bytes4(keccak256("updateMetavestUnlockRate(address,uint160)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 20e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestUnlockRate(allocation, 20e18); -// -// msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 20e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestVestingRate(allocation, 20e18); -// -// msgSig = bytes4(keccak256("setMetaVestGovVariables(address,uint8)")); -// callData = abi.encodeWithSelector(msgSig, allocation, BaseAllocation.GovType.vested); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.setMetaVestGovVariables(allocation, BaseAllocation.GovType.vested); -// } - -// function testEveryUpdateAmendmentFunctionRestricted() public { -// address allocation = createDummyRestrictedTokenAward(); -// bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); -// bytes memory callData = abi.encodeWithSelector(msgSig, allocation, true); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestTransferability(allocation, true); -// -// msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 2e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateExerciseOrRepurchasePrice(allocation, 2e18); -// -// msgSig = bytes4(keccak256("removeMetavestMilestone(address,uint256)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 0); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.removeMetavestMilestone(allocation, 0); -// -// msgSig = bytes4(keccak256("updateMetavestUnlockRate(address,uint160)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 20e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestUnlockRate(allocation, 20e18); -// -// msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); -// callData = abi.encodeWithSelector(msgSig, allocation, 20e18); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.updateMetavestVestingRate(allocation, 20e18); -// -// msgSig = bytes4(keccak256("setMetaVestGovVariables(address,uint8)")); -// callData = abi.encodeWithSelector(msgSig, allocation, BaseAllocation.GovType.vested); -// -// vm.prank(authority); -// controller.proposeMetavestAmendment(allocation, msgSig, callData); -// -// vm.prank(grantee); -// controller.consentToMetavestAmendment(allocation, msgSig, true); -// -// vm.prank(authority); -// controller.setMetaVestGovVariables(allocation, BaseAllocation.GovType.vested); -// } + function testEveryUpdateAmendmentFunction() public { + address allocation = createDummyTokenOptionAllocation(); + bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); + bytes memory callData = abi.encodeWithSelector(msgSig, allocation, true); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateMetavestTransferability(allocation, true); + + msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); + callData = abi.encodeWithSelector(msgSig, allocation, 2e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateExerciseOrRepurchasePrice(allocation, 2e18); + + msgSig = bytes4(keccak256("removeMetavestMilestone(address,uint256)")); + callData = abi.encodeWithSelector(msgSig, allocation, 0); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.removeMetavestMilestone(allocation, 0); + + msgSig = bytes4(keccak256("updateMetavestUnlockRate(address,uint160)")); + callData = abi.encodeWithSelector(msgSig, allocation, 20e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateMetavestUnlockRate(allocation, 20e18); + + msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); + callData = abi.encodeWithSelector(msgSig, allocation, 20e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateMetavestVestingRate(allocation, 20e18); + + msgSig = bytes4(keccak256("setMetaVestGovVariables(address,uint8)")); + callData = abi.encodeWithSelector(msgSig, allocation, BaseAllocation.GovType.vested); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.setMetaVestGovVariables(allocation, BaseAllocation.GovType.vested); + } + + function testEveryUpdateAmendmentFunctionRestricted() public { + address allocation = createDummyRestrictedTokenAward(); + bytes4 msgSig = bytes4(keccak256("updateMetavestTransferability(address,bool)")); + bytes memory callData = abi.encodeWithSelector(msgSig, allocation, true); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateMetavestTransferability(allocation, true); + + msgSig = bytes4(keccak256("updateExerciseOrRepurchasePrice(address,uint256)")); + callData = abi.encodeWithSelector(msgSig, allocation, 2e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateExerciseOrRepurchasePrice(allocation, 2e18); + + msgSig = bytes4(keccak256("removeMetavestMilestone(address,uint256)")); + callData = abi.encodeWithSelector(msgSig, allocation, 0); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.removeMetavestMilestone(allocation, 0); + + msgSig = bytes4(keccak256("updateMetavestUnlockRate(address,uint160)")); + callData = abi.encodeWithSelector(msgSig, allocation, 20e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateMetavestUnlockRate(allocation, 20e18); + + msgSig = bytes4(keccak256("updateMetavestVestingRate(address,uint160)")); + callData = abi.encodeWithSelector(msgSig, allocation, 20e18); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.updateMetavestVestingRate(allocation, 20e18); + + msgSig = bytes4(keccak256("setMetaVestGovVariables(address,uint8)")); + callData = abi.encodeWithSelector(msgSig, allocation, BaseAllocation.GovType.vested); + + vm.prank(authority); + controller.proposeMetavestAmendment(allocation, msgSig, callData); + + vm.prank(grantee); + controller.consentToMetavestAmendment(allocation, msgSig, true); + + vm.prank(authority); + controller.setMetaVestGovVariables(allocation, BaseAllocation.GovType.vested); + } function testEveryUpdateAmendmentFunctionVesting() public { address allocation = createDummyVestingAllocation(); diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index f4434fc..8e255bb 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -52,7 +52,6 @@ contract MetaVesTControllerTestBase is Test { // Deploy CyberAgreementRegistry and prepare templates - // TODO who should be the owner of auth? auth = new BorgAuth{salt: salt}(deployer); registry = CyberAgreementRegistry(address(new ERC1967Proxy{salt: salt}( address(new CyberAgreementRegistry{salt: salt}()), From df54c6f8710cf044bbb88267661eb6ecb0e0e5d4 Mon Sep 17 00:00:00 2001 From: detoo Date: Mon, 27 Oct 2025 16:33:43 -0700 Subject: [PATCH 17/25] chore: update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f22450..e09fa62 100644 --- a/README.md +++ b/README.md @@ -206,5 +206,7 @@ forge script scripts/proposeAllGuardiansMetavestDeals.s.sol --rpc-url -vvv +# Excluding acceptance tests because deployment may have not happened yet +# Use Ethereum mainnet RPC for tests because YearnBorgCompensation integration tests depends on it +forge test --via-ir --fork-url -vvv --nmc YearnBorgCompensationAcceptanceTest ``` From 5e503ef44049d1e0386c7d05359dfc0c71e0fa32 Mon Sep 17 00:00:00 2001 From: detoo Date: Fri, 31 Oct 2025 16:15:57 -0700 Subject: [PATCH 18/25] wip: feat: add MetaVesTControllerFactory --- .gitignore | 1 - foundry.toml | 4 ++ src/MetaVesTController.sol | 6 +++ src/MetaVesTControllerFactory.sol | 55 ++++++++++++++++++++++++++++ test/MetaVesTControllerFactory.t.sol | 14 +++++++ test/controller.t.sol | 14 +++++++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 foundry.toml create mode 100644 src/MetaVesTControllerFactory.sol create mode 100644 test/MetaVesTControllerFactory.t.sol diff --git a/.gitignore b/.gitignore index 359e274..60ba91b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ out/ test-command.txt broadcast/ .DS_Store -foundry.toml diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..c294e79 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,4 @@ +[fmt] +sort_imports = true + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index e7a4ab7..fc6fbef 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -94,6 +94,11 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { _; } + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /// @param _authority address of the authority who can call the functions in this contract and update each MetaVesT in '_metavest', such as a BORG /// @param _dao DAO governance contract address which exercises control over ability of 'authority' to call certain functions via imposing /// conditions through 'updateFunctionCondition'. @@ -700,6 +705,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address newImplementation ) internal virtual override onlyAuthority {} + // TODO deprecated: do we still need this? // Avoid "Address: low-level delegate call failed" due to `UUPSUpgradeable.upgradeToAndCall()` runs with `forceCall=true` fallback() external {} } diff --git a/src/MetaVesTControllerFactory.sol b/src/MetaVesTControllerFactory.sol new file mode 100644 index 0000000..b9c0884 --- /dev/null +++ b/src/MetaVesTControllerFactory.sol @@ -0,0 +1,55 @@ +//SPDX-License-Identifier: AGPL-3.0-only + +/* +************************************ + MetaVesTFactory + ************************************ + */ + +pragma solidity ^0.8.24; + +import {metavestController} from "./MetaVesTController.sol"; +import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @title MetaVesT Controller Factory + * + * @notice Deploy a new instance of MetaVesTController, which in turn deploys a new MetaVesT it controls + * + * + */ +contract MetaVesTControllerFactory is UUPSUpgradeable { + event MetaVesT_Deployment( + address newMetaVesT, + address authority, + address controller, + address dao, + address vestingAllocationFactory, + address tokenOptionFactory, + address restrictedTokenFactory + ); + + error ZeroAddress(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() public initializer {} + + /// @notice constructs a MetaVesT framework specifying authority address, DAO staking/voting contract address + /// each individual grantee's MetaVesT will be initiated in the newly deployed MetaVesT contract, and deployed MetaVesTs are amendable by 'authority' via the controller contract + /// @dev conditionals are contained in the deployed MetaVesT, which is deployed in the MetaVesTController's constructor(); the MetaVesT within the MetaVesTController is immutable, but the 'authority' which has access control within the controller may replace itself + // TODO WIP + // function deployMetavestAndController(address _authority, address _dao, address _vestingAllocationFactory, address _tokenOptionFactory, address _restrictedTokenFactory ) external returns(address) { + // if(_vestingAllocationFactory == address(0) || _tokenOptionFactory == address(0) || _restrictedTokenFactory == address(0)) + // revert MetaVesTFactory_ZeroAddress(); + // metavestController _controller = new metavestController(_authority, _dao, _vestingAllocationFactory, _tokenOptionFactory, _restrictedTokenFactory); + // emit MetaVesT_Deployment(address(0), _authority, address(_controller), _dao, _vestingAllocationFactory, _tokenOptionFactory, _restrictedTokenFactory); + // return address(_controller); + // } + + // TODO WIP: use BorgAuth + function _authorizeUpgrade(address newImplementation) internal virtual override {} +} diff --git a/test/MetaVesTControllerFactory.t.sol b/test/MetaVesTControllerFactory.t.sol new file mode 100644 index 0000000..72bdf5c --- /dev/null +++ b/test/MetaVesTControllerFactory.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +import {MetaVesTControllerFactory} from "../src/MetaVesTControllerFactory.sol"; +import {Test} from "forge-std/Test.sol"; +import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; + +contract MetaVesTControllerFactoryTest is Test { + function test_RevertIf_InitializeImplementation() public { + MetaVesTControllerFactory impl = new MetaVesTControllerFactory(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(); + } +} diff --git a/test/controller.t.sol b/test/controller.t.sol index 9005794..b7e07f6 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.20; +import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; import "../src/RestrictedTokenAllocation.sol"; import "../src/RestrictedTokenFactory.sol"; import "../src/TokenOptionAllocation.sol"; @@ -76,6 +77,19 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertTrue(registry.isValidDelegate(guardianSafe, delegate), "delegate should be Guardian SAFE's delegate"); } + function test_RevertIf_InitializeImplementation() public { + metavestController controllerImpl = new metavestController(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + controllerImpl.initialize( + address(123), // no-op + address(123), // no-op + address(123), // no-op + address(123), // no-op + address(123), // no-op + address(123) // no-op + ); + } + function testCreateVestingAllocation() public { bytes32 contractIdAlice = _proposeAndSignDeal( templateId, From 3bc2eef7eaf831a9426553041c2da99623b714c3 Mon Sep 17 00:00:00 2001 From: detoo Date: Fri, 31 Oct 2025 17:02:16 -0700 Subject: [PATCH 19/25] chore: remove unnecessary patch for pre-v5 openzeppelin --- src/MetaVesTController.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index fc6fbef..aa555cf 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -704,8 +704,4 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { function _authorizeUpgrade( address newImplementation ) internal virtual override onlyAuthority {} - - // TODO deprecated: do we still need this? - // Avoid "Address: low-level delegate call failed" due to `UUPSUpgradeable.upgradeToAndCall()` runs with `forceCall=true` - fallback() external {} } From 856d5e4c4b8eac8bca7f157393a04132ad95f8e2 Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 2 Nov 2025 20:28:45 -0800 Subject: [PATCH 20/25] wip: feat: consolidate all metavest types creation into MetaVesTControllerFactory --- scripts/createAllTemplates.s.sol | 1 - scripts/createSafeTx.s.sol | 1 - scripts/deployYearnBorgCompensation.s.sol | 7 +- ...oyYearnBorgCompensationPrerequisites.s.sol | 22 +- scripts/executeSafeTx.s.sol | 1 - .../lib/YearnBorgCompensation2025_2026.sol | 12 +- .../YearnBorgCompensationSepolia2025_2026.sol | 8 +- .../proposeAllGuardiansMetavestDeals.s.sol | 3 +- scripts/proposeMetavestDeal.s.sol | 3 +- scripts/signDealAndCreateMetavest.s.sol | 1 - scripts/voidAgreement.s.sol | 1 - src/MetaVesTController.sol | 38 ++-- src/MetaVesTControllerFactory.sol | 105 ++++++--- src/RestrictedTokenFactory.sol | 26 --- src/TokenOptionFactory.sol | 26 --- src/VestingAllocationFactory.sol | 26 --- src/interfaces/IMetaVesTControllerFactory.sol | 11 + .../MetaVesTControllerFactoryStorage.sol | 61 ++++++ src/storage/MetaVesTControllerStorage.sol | 200 +++++++++--------- test/MetaVesTControllerFactory.t.sol | 6 +- test/YearnBorgCompensation.t.sol | 10 +- test/YearnBorgCompensationAcceptance.t.sol | 1 - test/amendement.t.sol | 11 +- test/controller.t.sol | 18 +- test/lib/MetaVesTControllerTestBase.sol | 18 +- 25 files changed, 326 insertions(+), 291 deletions(-) delete mode 100644 src/RestrictedTokenFactory.sol delete mode 100644 src/TokenOptionFactory.sol delete mode 100644 src/VestingAllocationFactory.sol create mode 100644 src/interfaces/IMetaVesTControllerFactory.sol create mode 100644 src/storage/MetaVesTControllerFactoryStorage.sol diff --git a/scripts/createAllTemplates.s.sol b/scripts/createAllTemplates.s.sol index 74df36d..9959f42 100644 --- a/scripts/createAllTemplates.s.sol +++ b/scripts/createAllTemplates.s.sol @@ -9,7 +9,6 @@ import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementReg import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; diff --git a/scripts/createSafeTx.s.sol b/scripts/createSafeTx.s.sol index 576f686..189dad8 100644 --- a/scripts/createSafeTx.s.sol +++ b/scripts/createSafeTx.s.sol @@ -11,7 +11,6 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s import {IZkCappedMinterV2} from "../src/interfaces/zk-governance/IZkCappedMinterV2.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; diff --git a/scripts/deployYearnBorgCompensation.s.sol b/scripts/deployYearnBorgCompensation.s.sol index e585345..92586ea 100644 --- a/scripts/deployYearnBorgCompensation.s.sol +++ b/scripts/deployYearnBorgCompensation.s.sol @@ -10,7 +10,6 @@ import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.so import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; @@ -47,7 +46,7 @@ contract DeployYearnBorgCompensationScript is SafeTxHelper, Script { console2.log("Salt string: ", saltStr); console2.log("Guardian Safe: ", address(config.borgSafe)); console2.log("CyberAgreementRegistry: ", address(config.registry)); - console2.log("VestingAllocationFactory: ", address(config.vestingAllocationFactory)); + console2.log("MetavestControllerFactory: ", address(config.metavestControllerFactory)); console2.log(""); bytes32 salt = keccak256(bytes(saltStr)); @@ -63,9 +62,7 @@ contract DeployYearnBorgCompensationScript is SafeTxHelper, Script { address(config.borgSafe), address(config.borgSafe), address(config.registry), - address(config.vestingAllocationFactory), - address(config.tokenOptionFactory), - address(config.restrictedTokenFactory) + address(config.metavestControllerFactory) ) ))); diff --git a/scripts/deployYearnBorgCompensationPrerequisites.s.sol b/scripts/deployYearnBorgCompensationPrerequisites.s.sol index 73e6b1f..26fcfd6 100644 --- a/scripts/deployYearnBorgCompensationPrerequisites.s.sol +++ b/scripts/deployYearnBorgCompensationPrerequisites.s.sol @@ -9,7 +9,7 @@ import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementReg import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; +import {MetaVesTControllerFactory} from "../src/MetaVesTControllerFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; @@ -37,14 +37,18 @@ contract DeployYearnBorgCompensationPrerequisitesScript is SafeTxHelper, Script string memory saltStr, YearnBorgCompensation2025_2026.Config memory config ) public virtual returns( - VestingAllocationFactory + MetaVesTControllerFactory ) { address deployer = vm.addr(deployerPrivateKey); + BorgAuth auth = config.registry.AUTH(); + console2.log(""); console2.log("=== DeployYearnBorgCompensationPrerequisitesScript ==="); console2.log("Deployer: ", deployer); console2.log("Salt string: ", saltStr); + console2.log("CyberAgreementRegistry: ", address(config.registry)); + console2.log("Auth: ", address(auth)); console2.log(""); bytes32 salt = keccak256(bytes(saltStr)); @@ -53,16 +57,24 @@ contract DeployYearnBorgCompensationPrerequisitesScript is SafeTxHelper, Script // Deploy MetaVesT pre-requisites - VestingAllocationFactory vestingAllocationFactory = new VestingAllocationFactory{salt: salt}(); + MetaVesTControllerFactory metavestControllerFactory = MetaVesTControllerFactory(address(new ERC1967Proxy{salt: salt}( + address(new MetaVesTControllerFactory{salt: salt}()), + abi.encodeWithSelector( + MetaVesTControllerFactory.initialize.selector, + address(auth), + address(config.registry), + new metavestController{salt: salt}() + ) + ))); vm.stopBroadcast(); // Output logs console2.log("Deployed addresses:"); - console2.log(" VestingAllocationFactory: ", address(vestingAllocationFactory)); + console2.log(" MetaVesTControllerFactory: ", address(metavestControllerFactory)); console2.log(""); - return vestingAllocationFactory; + return metavestControllerFactory; } } diff --git a/scripts/executeSafeTx.s.sol b/scripts/executeSafeTx.s.sol index d7829ac..ebcd9e6 100644 --- a/scripts/executeSafeTx.s.sol +++ b/scripts/executeSafeTx.s.sol @@ -11,7 +11,6 @@ import {ZkCappedMinterV2} from "zk-governance/l2-contracts/src/ZkCappedMinterV2. import {IZkCappedMinterV2Factory} from "../src/interfaces/zk-governance/IZkCappedMinterV2Factory.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; diff --git a/scripts/lib/YearnBorgCompensation2025_2026.sol b/scripts/lib/YearnBorgCompensation2025_2026.sol index aa4b60b..299b4df 100644 --- a/scripts/lib/YearnBorgCompensation2025_2026.sol +++ b/scripts/lib/YearnBorgCompensation2025_2026.sol @@ -6,10 +6,8 @@ import {CommonBase} from "forge-std/Base.sol"; import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; import {IGnosisSafe} from "../../test/lib/safe.sol"; import {BaseAllocation} from "../../src/BaseAllocation.sol"; -import {VestingAllocationFactory} from "../../src/VestingAllocationFactory.sol"; -import {TokenOptionFactory} from "../../src/TokenOptionFactory.sol"; -import {RestrictedTokenFactory} from "../../src/RestrictedTokenFactory.sol"; import {metavestController} from "../../src/MetaVesTController.sol"; +import {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; library YearnBorgCompensation2025_2026 { @@ -29,9 +27,7 @@ library YearnBorgCompensation2025_2026 { IGnosisSafe metalexSafe; CyberAgreementRegistry registry; - VestingAllocationFactory vestingAllocationFactory; - TokenOptionFactory tokenOptionFactory; - RestrictedTokenFactory restrictedTokenFactory; + MetaVesTControllerFactory metavestControllerFactory; metavestController controller; // Yearn BORG Director Compensation Agreement (one template per director for now) @@ -85,9 +81,7 @@ library YearnBorgCompensation2025_2026 { metalexSafe: metalexSafe, registry: CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134), - vestingAllocationFactory: VestingAllocationFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD - tokenOptionFactory: TokenOptionFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD - restrictedTokenFactory: RestrictedTokenFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD + metavestControllerFactory: MetaVesTControllerFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD controller: metavestController(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD // Yearn BORG Compensation Agreement diff --git a/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol b/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol index 9046a4a..0508a5a 100644 --- a/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol +++ b/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol @@ -6,9 +6,7 @@ import {CommonBase} from "forge-std/Base.sol"; import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; import {IGnosisSafe} from "../../test/lib/safe.sol"; import {BaseAllocation} from "../../src/BaseAllocation.sol"; -import {VestingAllocationFactory} from "../../src/VestingAllocationFactory.sol"; -import {TokenOptionFactory} from "../../src/TokenOptionFactory.sol"; -import {RestrictedTokenFactory} from "../../src/RestrictedTokenFactory.sol"; +import {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; import {metavestController} from "../../src/MetaVesTController.sol"; import {YearnBorgCompensation2025_2026} from "./YearnBorgCompensation2025_2026.sol"; @@ -37,9 +35,7 @@ library YearnBorgCompensationSepolia2025_2026 { metalexSafe: metalexSafe, registry: CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134), - vestingAllocationFactory: VestingAllocationFactory(0x87dC5e3FBFE8B5F2B74C64eE34da8bdc9fedCb0f), - tokenOptionFactory: TokenOptionFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD - restrictedTokenFactory: RestrictedTokenFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD + metavestControllerFactory: MetaVesTControllerFactory(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), // TODO TBD controller: metavestController(0xFa5Ab18bD5E02B1d6430e91C32C5CB5e7F43bB65), // Yearn BORG Compensation Agreement diff --git a/scripts/proposeAllGuardiansMetavestDeals.s.sol b/scripts/proposeAllGuardiansMetavestDeals.s.sol index 21433c1..3a06898 100644 --- a/scripts/proposeAllGuardiansMetavestDeals.s.sol +++ b/scripts/proposeAllGuardiansMetavestDeals.s.sol @@ -11,7 +11,6 @@ import {ISafeProxyFactory, IGnosisSafe} from "../test/lib/safe.sol"; import {ProposeMetaVestDealScript} from "./proposeMetavestDeal.s.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {YearnBorgCompensation2025_2026} from "./lib/YearnBorgCompensation2025_2026.sol"; import {YearnBorgCompensationSepolia2025_2026} from "./lib/YearnBorgCompensationSepolia2025_2026.sol"; import {console2} from "forge-std/console2.sol"; @@ -49,7 +48,7 @@ contract ProposeAllGuardiansMetaVestDealScript is ProposeMetaVestDealScript { console2.log("=== ProposeAllGuardiansMetaVestDealScript ==="); console2.log("Proposer: ", proposer); console2.log("CyberAgreementRegistry: ", address(config.registry)); - console2.log("VestingAllocationFactory: ", address(config.vestingAllocationFactory)); + console2.log("MetavestControllerFactory: ", address(config.metavestControllerFactory)); console2.log("MetavesTController: ", address(config.controller)); console2.log(""); diff --git a/scripts/proposeMetavestDeal.s.sol b/scripts/proposeMetavestDeal.s.sol index 92fa448..6ab5b84 100644 --- a/scripts/proposeMetavestDeal.s.sol +++ b/scripts/proposeMetavestDeal.s.sol @@ -10,7 +10,6 @@ import {ISafeProxyFactory, IGnosisSafe} from "../test/lib/safe.sol"; import {CyberAgreementUtils} from "./lib/CyberAgreementUtils.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; import {MetaVestDealLib, MetaVestDeal} from "../src/lib/MetaVestDealLib.sol"; @@ -69,7 +68,7 @@ contract ProposeMetaVestDealScript is SafeTxHelper, Script { console2.log("Guardian Safe: ", address(config.borgSafe)); console2.log("Payment token: ", address(config.paymentToken)); console2.log("CyberAgreementRegistry: ", address(config.registry)); - console2.log("VestingAllocationFactory: ", address(config.vestingAllocationFactory)); + console2.log("MetavestControllerFactory: ", address(config.metavestControllerFactory)); console2.log("MetavesTController: ", address(config.controller)); console2.log(""); diff --git a/scripts/signDealAndCreateMetavest.s.sol b/scripts/signDealAndCreateMetavest.s.sol index 305737f..3240101 100644 --- a/scripts/signDealAndCreateMetavest.s.sol +++ b/scripts/signDealAndCreateMetavest.s.sol @@ -10,7 +10,6 @@ import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.so import {ISafeProxyFactory, IGnosisSafe} from "../test/lib/safe.sol"; import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {console2} from "forge-std/console2.sol"; import {metavestController} from "../src/MetaVesTController.sol"; diff --git a/scripts/voidAgreement.s.sol b/scripts/voidAgreement.s.sol index 5f7f949..bb23b36 100644 --- a/scripts/voidAgreement.s.sol +++ b/scripts/voidAgreement.s.sol @@ -12,7 +12,6 @@ import {IZkCappedMinterV2Factory} from "../src/interfaces/zk-governance/IZkCappe import {SafeTxHelper} from "./lib/SafeTxHelper.sol"; import {CyberAgreementUtils} from "./lib/CyberAgreementUtils.sol"; import {Script} from "forge-std/Script.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {ZkCappedMinterV2} from "zk-governance/l2-contracts/src/ZkCappedMinterV2.sol"; import {ZkTokenV2} from "zk-governance/l2-contracts/src/ZkTokenV2.sol"; import {console2} from "forge-std/console2.sol"; diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index aa555cf..fb405df 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -12,6 +12,7 @@ import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICybe import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "./lib/MetaVestDealLib.sol"; import {MetaVesTControllerStorage} from "./storage/MetaVesTControllerStorage.sol"; +import {IMetaVesTControllerFactory} from "./interfaces/IMetaVesTControllerFactory.sol"; import "./BaseAllocation.sol"; import "./interfaces/IAllocationFactory.sol"; import "./interfaces/IPriceAllocation.sol"; @@ -27,6 +28,8 @@ import "./lib/EnumberableSet.sol"; * by an applicable affected grantee or a majority-in-governing power of similar token grantees **/ contract metavestController is UUPSUpgradeable, SafeTransferLib { + string public constant DEPLOY_VERSION = "1"; // For version-tracking on all deployment and future upgrades + using MetaVesTControllerStorage for MetaVesTControllerStorage.MetaVesTControllerData; using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; @@ -65,6 +68,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address metavest ); + error NotRefImplementation(); + /// /// FUNCTIONS /// @@ -106,9 +111,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address _authority, address _dao, address _registry, - address _vestingFactory, - address _tokenOptionFactory, - address _restrictedTokenFactory + address upgradeFactory ) public initializer { __UUPSUpgradeable_init(); @@ -117,10 +120,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); st.authority = _authority; st.registry = _registry; - st.vestingFactory = _vestingFactory; - st.tokenOptionFactory = _tokenOptionFactory; - st.restrictedTokenFactory = _restrictedTokenFactory; st.dao = _dao; + st.upgradeFactory = upgradeFactory; } /// @notice for a grantee to consent to an update to one of their metavestDetails by 'authority' corresponding to the applicable function in this controller @@ -618,16 +619,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { return MetaVesTControllerStorage.getStorage().registry; } - function vestingFactory() external view returns (address) { - return MetaVesTControllerStorage.getStorage().vestingFactory; - } - - function tokenOptionFactory() external view returns (address) { - return MetaVesTControllerStorage.getStorage().tokenOptionFactory; - } - - function restrictedTokenFactory() external view returns (address) { - return MetaVesTControllerStorage.getStorage().restrictedTokenFactory; + function upgradeFactory() external view returns (address) { + return MetaVesTControllerStorage.getStorage().upgradeFactory; } function functionToConditions(bytes4 sig, uint256 idx) external view returns (address) { @@ -701,7 +694,18 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } } + // ======================== + // UUPSUpgradeable + // ======================== + + /// @notice UUPS upgrade authorization + /// @dev MetaLeX releases new versions through the factory's reference implementation, + /// and the MetaVesTController authority can decide if or when he wants to perform the upgrade function _authorizeUpgrade( address newImplementation - ) internal virtual override onlyAuthority {} + ) internal override onlyAuthority { + if (IMetaVesTControllerFactory(MetaVesTControllerStorage.getStorage().upgradeFactory).getRefImplementation() != newImplementation) { + revert NotRefImplementation(); + } + } } diff --git a/src/MetaVesTControllerFactory.sol b/src/MetaVesTControllerFactory.sol index b9c0884..2335c6d 100644 --- a/src/MetaVesTControllerFactory.sol +++ b/src/MetaVesTControllerFactory.sol @@ -9,7 +9,10 @@ pragma solidity ^0.8.24; import {metavestController} from "./MetaVesTController.sol"; +import {MetaVesTControllerFactoryStorage} from "./storage/MetaVesTControllerFactoryStorage.sol"; +import {BorgAuthACL} from "cybercorps-contracts/src/libs/auth.sol"; import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; /** * @title MetaVesT Controller Factory @@ -18,38 +21,88 @@ import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UU * * */ -contract MetaVesTControllerFactory is UUPSUpgradeable { - event MetaVesT_Deployment( - address newMetaVesT, - address authority, +contract MetaVesTControllerFactory is BorgAuthACL, UUPSUpgradeable { + event RegistrySet(address registry); + event RefImplementationSet(address refImplementation, string version); + event MetaVesTControllerDeployed( address controller, - address dao, - address vestingAllocationFactory, - address tokenOptionFactory, - address restrictedTokenFactory + address authority, + address dao ); - error ZeroAddress(); - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } - function initialize() public initializer {} - - /// @notice constructs a MetaVesT framework specifying authority address, DAO staking/voting contract address - /// each individual grantee's MetaVesT will be initiated in the newly deployed MetaVesT contract, and deployed MetaVesTs are amendable by 'authority' via the controller contract - /// @dev conditionals are contained in the deployed MetaVesT, which is deployed in the MetaVesTController's constructor(); the MetaVesT within the MetaVesTController is immutable, but the 'authority' which has access control within the controller may replace itself - // TODO WIP - // function deployMetavestAndController(address _authority, address _dao, address _vestingAllocationFactory, address _tokenOptionFactory, address _restrictedTokenFactory ) external returns(address) { - // if(_vestingAllocationFactory == address(0) || _tokenOptionFactory == address(0) || _restrictedTokenFactory == address(0)) - // revert MetaVesTFactory_ZeroAddress(); - // metavestController _controller = new metavestController(_authority, _dao, _vestingAllocationFactory, _tokenOptionFactory, _restrictedTokenFactory); - // emit MetaVesT_Deployment(address(0), _authority, address(_controller), _dao, _vestingAllocationFactory, _tokenOptionFactory, _restrictedTokenFactory); - // return address(_controller); - // } - - // TODO WIP: use BorgAuth - function _authorizeUpgrade(address newImplementation) internal virtual override {} + function initialize(address auth, address registry, address refImplementation) public initializer { + // Initialize BorgAuthACL + __BorgAuthACL_init(auth); + + MetaVesTControllerFactoryStorage.StorageData storage s = MetaVesTControllerFactoryStorage.getStorageData(); + s.registry = registry; + s.refImplementation = refImplementation; + } + + /// @notice Deploy a MetaVesTController specifying authority address, DAO staking/voting contract address + /// each individual grantee's will have his own MetaVesT contract, and deployed MetaVesTs are amendable by 'authority' via the controller contract + /// @dev Each deployed MetaVesTController has its own set of conditions for admin operations such as MetaVesT creation/termination and parameters, etc. + /// the MetaVesT created by the MetaVesTController is immutable, but the 'authority' which has access control within the controller may replace itself + function deployMetavestController(bytes32 salt, address authority, address dao) external returns (address) { + MetaVesTControllerFactoryStorage.StorageData storage s = MetaVesTControllerFactoryStorage.getStorageData(); + metavestController controller = metavestController(address(new ERC1967Proxy{salt: salt}( + MetaVesTControllerFactoryStorage.getStorageData().refImplementation, + abi.encodeWithSelector( + metavestController.initialize.selector, + authority, + dao, + s.registry, + address(this) + ) + ))); + emit MetaVesTControllerDeployed( + address(controller), + authority, + dao + ); + return address(controller); + } + + // ======================== + // Getter / Setter + // ======================== + + /// @notice Get the CyberAgreementRegistry used for MetaVesT deals + /// @return CyberAgreementRegistry contract address + function getRegistry() public view returns (address) { + return MetaVesTControllerFactoryStorage.getStorageData().registry; + } + + /// @notice Set the CyberAgreementRegistry used for MetaVesT deals + /// @dev Only callable by addresses with the owner role + /// @param registry Address of the new implementation + function setRegistry(address registry) public onlyOwner { + MetaVesTControllerFactoryStorage.getStorageData().registry = registry; + emit RegistrySet(registry); + } + + /// @notice Get the reference implementation contract for the next deployments + /// @return Current reference implementation contract address + function getRefImplementation() public view returns (address) { + return MetaVesTControllerFactoryStorage.getStorageData().refImplementation; + } + + /// @notice Set the reference implementation contract for the next deployments + /// @dev Only callable by addresses with the admin role + /// @param newImplementation Address of the new implementation + function setRefImplementation(address newImplementation) public onlyOwner { + MetaVesTControllerFactoryStorage.getStorageData().refImplementation = newImplementation; + emit RefImplementationSet(newImplementation, metavestController(newImplementation).DEPLOY_VERSION()); + } + + // ======================== + // UUPSUpgradeable + // ======================== + + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {} } diff --git a/src/RestrictedTokenFactory.sol b/src/RestrictedTokenFactory.sol deleted file mode 100644 index db0bfc1..0000000 --- a/src/RestrictedTokenFactory.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "./RestrictedTokenAllocation.sol"; -import "./interfaces/IAllocationFactory.sol"; - -contract RestrictedTokenFactory is IAllocationFactory { - - function createAllocation( - AllocationType _allocationType, - address _grantee, - address _recipient, - address _controller, - RestrictedTokenAward.Allocation memory _allocation, - RestrictedTokenAward.Milestone[] memory _milestones, - address _paymentToken, - uint256 _exercisePrice, - uint256 _shortStopDuration - ) external returns (address) { - if (_allocationType == AllocationType.RestrictedToken) { - return address(new RestrictedTokenAward(_grantee, _recipient, _controller, _paymentToken, _exercisePrice, _shortStopDuration, _allocation, _milestones)); - } else { - revert("AllocationFactory: invalid allocation type"); - } - } -} diff --git a/src/TokenOptionFactory.sol b/src/TokenOptionFactory.sol deleted file mode 100644 index 10025e3..0000000 --- a/src/TokenOptionFactory.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "./TokenOptionAllocation.sol"; -import "./interfaces/IAllocationFactory.sol"; - -contract TokenOptionFactory is IAllocationFactory { - - function createAllocation( - AllocationType _allocationType, - address _grantee, - address _recipient, - address _controller, - TokenOptionAllocation.Allocation memory _allocation, - TokenOptionAllocation.Milestone[] memory _milestones, - address _paymentToken, - uint256 _exercisePrice, - uint256 _shortStopDuration - ) external returns (address) { - if (_allocationType == AllocationType.TokenOption) { - return address(new TokenOptionAllocation(_grantee, _recipient, _controller, _paymentToken, _exercisePrice, _shortStopDuration, _allocation, _milestones)); - } else { - revert("AllocationFactory: invalid allocation type"); - } - } -} diff --git a/src/VestingAllocationFactory.sol b/src/VestingAllocationFactory.sol deleted file mode 100644 index f01baaa..0000000 --- a/src/VestingAllocationFactory.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "./VestingAllocation.sol"; -import "./interfaces/IAllocationFactory.sol"; - -contract VestingAllocationFactory is IAllocationFactory { - - function createAllocation( - AllocationType _allocationType, - address _grantee, - address _recipient, - address _controller, - VestingAllocation.Allocation memory _allocation, - VestingAllocation.Milestone[] memory _milestones, - address _paymentToken, - uint256 _exercisePrice, - uint256 _shortStopDuration - ) external returns (address) { - if (_allocationType == AllocationType.Vesting) { - return address(new VestingAllocation(_grantee, _recipient, _controller, _allocation, _milestones)); - } else { - revert("AllocationFactory: invalid allocation type"); - } - } -} diff --git a/src/interfaces/IMetaVesTControllerFactory.sol b/src/interfaces/IMetaVesTControllerFactory.sol new file mode 100644 index 0000000..7a091db --- /dev/null +++ b/src/interfaces/IMetaVesTControllerFactory.sol @@ -0,0 +1,11 @@ +//SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity ^0.8.24; + +interface IMetaVesTControllerFactory { + function getRegistry() external view returns(address); + function setRegistry(address registry) external; + + function getRefImplementation() external view returns(address); + function setRefImplementation(address newImplementation) external; +} diff --git a/src/storage/MetaVesTControllerFactoryStorage.sol b/src/storage/MetaVesTControllerFactoryStorage.sol new file mode 100644 index 0000000..ab0b1b6 --- /dev/null +++ b/src/storage/MetaVesTControllerFactoryStorage.sol @@ -0,0 +1,61 @@ +/* .o. + .888. + .8"888. + .8' `888. + .88ooo8888. + .8' `888. +o88o o8888o + + + +ooo ooooo . ooooo ooooooo ooooo +`88. .888' .o8 `888' `8888 d8' + 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P + 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' + 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. + 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b +o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o + + + + .oooooo. .o8 .oooooo. + d8P' `Y8b "888 d8P' `Y8b +888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo. +888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b +888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 +`88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. + `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P + .o..P' 888 + `Y8P' o888o +_______________________________________________________________________________________________________ + +All software, documentation and other files and information in this repository (collectively, the "Software") +are copyright MetaLeX Labs, Inc., a Delaware corporation. + +All rights reserved. + +The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, +distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or +mechanical, including photocopying, recording, or by any information storage and retrieval system, +except with the express prior written permission of the copyright holder.*/ + +pragma solidity 0.8.28; + +library MetaVesTControllerFactoryStorage { + // Storage slot for our struct + bytes32 constant STORAGE_POSITION = keccak256("cybercorp.metavest.controller.factory.storage.v1"); + + // Main storage layout struct + struct StorageData { + address registry; + address refImplementation; // implementation contract to use for new deployments + } + + // Returns the storage layout + function getStorageData() internal pure returns (StorageData storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } +} diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index 0faf06a..1b4e03c 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -29,31 +29,33 @@ .o..P' 888 `Y8P' o888o _______________________________________________________________________________________________________ - + All software, documentation and other files and information in this repository (collectively, the "Software") are copyright MetaLeX Labs, Inc., a Delaware corporation. - + All rights reserved. - - The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, + + The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or - mechanical, including photocopying, recording, or by any information storage and retrieval system, + mechanical, including photocopying, recording, or by any information storage and retrieval system, except with the express prior written permission of the copyright holder.*/ - + pragma solidity 0.8.28; -import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; -import {BaseAllocation, IERC20M, IConditionM} from "../BaseAllocation.sol"; +import {BaseAllocation, IConditionM, IERC20M} from "../BaseAllocation.sol"; +import {RestrictedTokenAward} from "../RestrictedTokenAllocation.sol"; +import {TokenOptionAllocation} from "../TokenOptionAllocation.sol"; +import {VestingAllocation} from "../VestingAllocation.sol"; import {EnumerableSet} from "../lib/EnumberableSet.sol"; -import {MetaVestDealLib, MetaVestDeal, MetaVestType} from "../lib/MetaVestDealLib.sol"; -import {IAllocationFactory} from "../interfaces/IAllocationFactory.sol"; +import {MetaVestDeal, MetaVestDealLib, MetaVestType} from "../lib/MetaVestDealLib.sol"; +import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; library MetaVesTControllerStorage { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; // Storage slot for our struct - bytes32 constant internal STORAGE_POSITION = keccak256("cybercorp.metavest.controller.storage.v1"); + bytes32 internal constant STORAGE_POSITION = keccak256("cybercorp.metavest.controller.storage.v1"); uint256 internal constant ARRAY_LENGTH_LIMIT = 20; @@ -78,9 +80,7 @@ library MetaVesTControllerStorage { address authority; address dao; address registry; - address vestingFactory; - address tokenOptionFactory; - address restrictedTokenFactory; + address upgradeFactory; address _pendingAuthority; address _pendingDao; @@ -164,55 +164,41 @@ library MetaVesTControllerStorage { } } - function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address){ + function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address vestingAllocation = IAllocationFactory(st.vestingFactory).createAllocation( - IAllocationFactory.AllocationType.Vesting, - deal.grantee, - recipient, - address(this), - deal.allocation, - deal.milestones, - address(0), - 0, - 0 - ); - - return vestingAllocation; + return address(new VestingAllocation(deal.grantee, recipient, address(this), deal.allocation, deal.milestones)); } function createTokenOptionAllocation(MetaVestDeal storage deal, address recipient) internal returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address tokenOptionAllocation = IAllocationFactory(st.tokenOptionFactory).createAllocation( - IAllocationFactory.AllocationType.TokenOption, - deal.grantee, - recipient, - address(this), - deal.allocation, - deal.milestones, - deal.paymentToken, - deal.exercisePrice, - deal.shortStopDuration + return address( + new TokenOptionAllocation( + deal.grantee, + recipient, + address(this), + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration, + deal.allocation, + deal.milestones + ) ); - - return tokenOptionAllocation; } - function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal returns (address){ + function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal returns (address) { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - address restrictedTokenAward = IAllocationFactory(st.restrictedTokenFactory).createAllocation( - IAllocationFactory.AllocationType.RestrictedToken, - deal.grantee, - recipient, - address(this), - deal.allocation, - deal.milestones, - deal.paymentToken, - deal.exercisePrice, - deal.shortStopDuration + return address( + new RestrictedTokenAward( + deal.grantee, + recipient, + address(this), + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration, + deal.allocation, + deal.milestones + ) ); - - return restrictedTokenAward; } function proposeAndSignDeal( @@ -229,27 +215,20 @@ library MetaVesTControllerStorage { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // Call internal function to avoid stack-too-deep errors - dealDraft.agreementId = ICyberAgreementRegistry(st.registry).createContract( - templateId, - salt, - globalValues, - parties, - partyValues, - secretHash, - address(this), - expiry - ); + dealDraft.agreementId = ICyberAgreementRegistry(st.registry) + .createContract(templateId, salt, globalValues, parties, partyValues, secretHash, address(this), expiry); st.counterPartyValues[dealDraft.agreementId] = partyValues[1]; - ICyberAgreementRegistry(st.registry).signContractFor( - st.authority, // First party (grantor) should always be the authority - dealDraft.agreementId, - partyValues[0], - signature, - false, // Not meant for anyone else other than the signer - "" // Signer == proposer, no secret needed - ); + ICyberAgreementRegistry(st.registry) + .signContractFor( + st.authority, // First party (grantor) should always be the authority + dealDraft.agreementId, + partyValues[0], + signature, + false, // Not meant for anyone else other than the signer + "" // Signer == proposer, no secret needed + ); st.deals[dealDraft.agreementId] = dealDraft; st.dealIds.push(dealDraft.agreementId); @@ -269,24 +248,34 @@ library MetaVesTControllerStorage { // Check: verify inputs - if(ICyberAgreementRegistry(st.registry).isVoided(agreementId)) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealVoided.selector); - if(ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized.selector); + if (ICyberAgreementRegistry(st.registry).isVoided(agreementId)) { + return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealVoided.selector); + } + if (ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) { + return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized.selector); + } string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; - if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); - + if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) { + return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); + } + MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; if ( deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) - || (deal.metavestType == MetaVestType.TokenOption && deal.paymentToken == address(0) && deal.exercisePrice == 0) - || (deal.metavestType == MetaVestType.RestrictedTokenAward && deal.paymentToken == address(0) && deal.exercisePrice == 0) + || (deal.metavestType == MetaVestType.TokenOption + && deal.paymentToken == address(0) + && deal.exercisePrice == 0) + || (deal.metavestType == MetaVestType.RestrictedTokenAward + && deal.paymentToken == address(0) + && deal.exercisePrice == 0) ) { return (address(0), 0, MetaVesTController_ZeroAddress.selector); } if ( - deal.allocation.vestingCliffCredit > deal.allocation.tokenStreamTotal || - deal.allocation.unlockingCliffCredit > deal.allocation.tokenStreamTotal + deal.allocation.vestingCliffCredit > deal.allocation.tokenStreamTotal + || deal.allocation.unlockingCliffCredit > deal.allocation.tokenStreamTotal ) { return (address(0), 0, MetaVesTController_CliffGreaterThanTotal.selector); } @@ -310,8 +299,8 @@ library MetaVesTControllerStorage { return (address(0), 0, MetaVesTController_ZeroAmount.selector); } if ( - IERC20M(deal.allocation.tokenContract).allowance(st.authority, address(this)) < total || - IERC20M(deal.allocation.tokenContract).balanceOf(st.authority) < total + IERC20M(deal.allocation.tokenContract).allowance(st.authority, address(this)) < total + || IERC20M(deal.allocation.tokenContract).balanceOf(st.authority) < total ) { return (address(0), 0, MetaVesTController_AmountNotApprovedForTransferFrom.selector); } @@ -321,23 +310,26 @@ library MetaVesTControllerStorage { // If the grantee signed the agreement externally (ex. by directly interacting with CyberAgreementRegistry), // we will skip the signing step. if (!ICyberAgreementRegistry(st.registry).hasSigned(agreementId, grantee)) { - ICyberAgreementRegistry(st.registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); + ICyberAgreementRegistry(st.registry) + .signContractFor(grantee, agreementId, partyValues, signature, false, secret); } else { // Already signed in registry; fetch values recorded in the registry and ensure consistency. // Theoretically, since the agreement is always close-ended, we could've safely assumed // the counterparty values stored in CyberAgreementRegistry vs here are always consistent, // but we check it anyways just in case string[] memory registryValues = ICyberAgreementRegistry(st.registry).getSignerValues(agreementId, grantee); - if (keccak256(abi.encode(registryValues)) != keccak256(abi.encode(partyValues))) return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); + if (keccak256(abi.encode(registryValues)) != keccak256(abi.encode(partyValues))) { + return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); + } } ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); // Interaction: Create and provision MetaVesT - if(deal.metavestType == MetaVestType.Vesting) { + if (deal.metavestType == MetaVestType.Vesting) { deal.metavest = createVestingAllocation(deal, recipient); - } else if(deal.metavestType == MetaVestType.TokenOption) { + } else if (deal.metavestType == MetaVestType.TokenOption) { deal.metavest = createTokenOptionAllocation(deal, recipient); - } else if(deal.metavestType == MetaVestType.RestrictedTokenAward) { + } else if (deal.metavestType == MetaVestType.RestrictedTokenAward) { deal.metavest = createRestrictedTokenAward(deal, recipient); } else { return (address(0), 0, MetaVesTController_IncorrectMetaVesTType.selector); @@ -347,15 +339,15 @@ library MetaVesTControllerStorage { return (deal.metavest, total, 0); } - function proposeMajorityMetavestAmendment( - string memory setName, - bytes4 _msgSig, - bytes calldata _callData - ) external returns (uint256 totalVotingPower) { + function proposeMajorityMetavestAmendment(string memory setName, bytes4 _msgSig, bytes calldata _callData) + external + returns (uint256 totalVotingPower) + { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); bytes32 nameHash = keccak256(bytes(setName)); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][nameHash]; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = + st.functionToSetMajorityProposal[_msgSig][nameHash]; proposal.isPending = true; proposal.dataHash = keccak256(_callData[_callData.length - 32:]); proposal.time = block.timestamp; @@ -389,17 +381,25 @@ library MetaVesTControllerStorage { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); if (isMetavestInSet(_grant)) { bytes32 set = getSetOfMetavest(_grant); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msgSig][set]; - if(proposal.appliedProposalCreatedAt[_grant] == proposal.time) return MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector; - if (_data.length>32 && _data.length<69) - { - if (!proposal.isPending || proposal.totalVotingPower>proposal.currentVotingPower*2 || keccak256(_data[_data.length - 32:]) != proposal.dataHash ) { - return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; + MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = + st.functionToSetMajorityProposal[msgSig][set]; + if (proposal.appliedProposalCreatedAt[_grant] == proposal.time) { + return MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector; + } + if (_data.length > 32 && _data.length < 69) { + if ( + !proposal.isPending || proposal.totalVotingPower > proposal.currentVotingPower * 2 + || keccak256(_data[_data.length - 32:]) != proposal.dataHash + ) { + return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented + .selector; } + } else { + return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; } - else return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; } else { - MetaVesTControllerStorage.AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msgSig][_grant]; + MetaVesTControllerStorage.AmendmentProposal storage proposal = + st.functionToGranteeToAmendmentPending[msgSig][_grant]; if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; } diff --git a/test/MetaVesTControllerFactory.t.sol b/test/MetaVesTControllerFactory.t.sol index 72bdf5c..35e45e6 100644 --- a/test/MetaVesTControllerFactory.t.sol +++ b/test/MetaVesTControllerFactory.t.sol @@ -9,6 +9,10 @@ contract MetaVesTControllerFactoryTest is Test { function test_RevertIf_InitializeImplementation() public { MetaVesTControllerFactory impl = new MetaVesTControllerFactory(); vm.expectRevert(Initializable.InvalidInitialization.selector); - impl.initialize(); + impl.initialize( + address(0), // no-op + address(0), // no-op + address(0) // no-op + ); } } diff --git a/test/YearnBorgCompensation.t.sol b/test/YearnBorgCompensation.t.sol index 3893825..c114768 100644 --- a/test/YearnBorgCompensation.t.sol +++ b/test/YearnBorgCompensation.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.20; import "../src/MetaVesTController.sol"; -import "../src/VestingAllocationFactory.sol"; import "./lib/MetaVesTControllerTestBase.sol"; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; @@ -15,6 +14,7 @@ import {ProposeAllGuardiansMetaVestDealScript} from "../scripts/proposeAllGuardi import {SignDealAndCreateMetavestScript} from "../scripts/signDealAndCreateMetavest.s.sol"; import {YearnBorgCompensation2025_2026} from "../scripts/lib/YearnBorgCompensation2025_2026.sol"; import {GnosisTransaction} from "./lib/safe.sol"; +import {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; // Test with fresh deployment (except third-party dependencies) // - Use third-party dependencies on Ethereum mainnet @@ -54,7 +54,7 @@ contract YearnBorgCompensationTest is deal(borgDelegate, 1 ether); deal(chad, 1 ether); - VestingAllocationFactory vestingAllocationFactory; + MetaVesTControllerFactory metavestControllerFactory; metavestController controller; GnosisTransaction[] memory safeTxsCreateAllTemplates; GnosisTransaction[] memory safeTxs2025_2026; @@ -86,14 +86,14 @@ contract YearnBorgCompensationTest is auth = config2025_2026.registry.AUTH(); // Deploy prerequisites - vestingAllocationFactory = DeployYearnBorgCompensationPrerequisitesScript.deployPrerequisites( + metavestControllerFactory = DeployYearnBorgCompensationPrerequisitesScript.deployPrerequisites( deployerPrivateKey, saltStr, config2025_2026 ); // Update configs with deployed contracts - config2025_2026.vestingAllocationFactory = vestingAllocationFactory; + config2025_2026.metavestControllerFactory = metavestControllerFactory; // Update configs with test BORG delegate config2025_2026.borgAgreementDelegate = borgDelegate; @@ -159,7 +159,7 @@ contract YearnBorgCompensationTest is vm.assertEq(config2025_2026.controller.authority(), address(config2025_2026.borgSafe), "2025-2026 MetaVesTController's authority should be BORG SAFE"); vm.assertEq(config2025_2026.controller.dao(), address(config2025_2026.borgSafe), "2025-2026 MetaVesTController's DAO should be BORG SAFE"); vm.assertEq(config2025_2026.controller.registry(), address(config2025_2026.registry), "2025-2026 Unexpected MetaVesTController registry"); - vm.assertEq(config2025_2026.controller.vestingFactory(), address(config2025_2026.vestingAllocationFactory), "2025-2026 Unexpected MetaVesTController vesting allocation factory"); + vm.assertEq(config2025_2026.controller.upgradeFactory(), address(config2025_2026.metavestControllerFactory), "2025-2026 Unexpected MetaVesTControllerFactory"); // BORG provisioning assertTrue(config2025_2026.registry.isValidDelegate(address(config2025_2026.borgSafe), borgDelegate), "delegate should be BORG SAFE's delegate"); diff --git a/test/YearnBorgCompensationAcceptance.t.sol b/test/YearnBorgCompensationAcceptance.t.sol index aaaa2ca..f114cc4 100644 --- a/test/YearnBorgCompensationAcceptance.t.sol +++ b/test/YearnBorgCompensationAcceptance.t.sol @@ -5,7 +5,6 @@ import {console2} from "forge-std/console2.sol"; import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; import {YearnBorgCompensationTest} from "./YearnBorgCompensation.t.sol"; import {metavestController} from "../src/MetaVesTController.sol"; -import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol"; import {MetaVesTControllerTestBase} from "./lib/MetaVesTControllerTestBase.sol"; import {GnosisTransaction} from "./lib/safe.sol"; import {CreateAllTemplatesScript} from "../scripts/createAllTemplates.s.sol"; diff --git a/test/amendement.t.sol b/test/amendement.t.sol index fc2d9a2..222cc43 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -6,9 +6,6 @@ import "../src/BaseAllocation.sol"; import "../src/TokenOptionAllocation.sol"; import "../src/RestrictedTokenAllocation.sol"; import "../src/interfaces/IAllocationFactory.sol"; -import "../src/VestingAllocationFactory.sol"; -import "../src/TokenOptionFactory.sol"; -import "../src/RestrictedTokenFactory.sol"; import "./lib/MetaVesTControllerTestBase.sol"; contract MetaVestControllerTest is MetaVesTControllerTestBase { @@ -32,10 +29,6 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Deploy MetaVesT controller - vestingAllocationFactory = new VestingAllocationFactory(); - tokenOptionFactory = new TokenOptionFactory(); - restrictedTokenFactory = new RestrictedTokenFactory(); - controller = metavestController(address(new ERC1967Proxy{salt: salt}( address(new metavestController{salt: salt}()), abi.encodeWithSelector( @@ -43,9 +36,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { guardianSafe, guardianSafe, address(registry), - address(vestingAllocationFactory), - address(tokenOptionFactory), - address(restrictedTokenFactory) + address(metavestControllerFactory) ) ))); diff --git a/test/controller.t.sol b/test/controller.t.sol index b7e07f6..423d662 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -3,11 +3,8 @@ pragma solidity ^0.8.20; import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; import "../src/RestrictedTokenAllocation.sol"; -import "../src/RestrictedTokenFactory.sol"; import "../src/TokenOptionAllocation.sol"; -import "../src/TokenOptionFactory.sol"; import "../src/VestingAllocation.sol"; -import "../src/VestingAllocationFactory.sol"; import "../src/interfaces/IAllocationFactory.sol"; import "./lib/MetaVesTControllerTestBase.sol"; import "./mocks/MockCondition.sol"; @@ -32,10 +29,6 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Deploy MetaVesT controller - vestingAllocationFactory = new VestingAllocationFactory(); - tokenOptionFactory = new TokenOptionFactory(); - restrictedTokenFactory = new RestrictedTokenFactory(); - controller = metavestController(address(new ERC1967Proxy{salt: salt}( address(new metavestController{salt: salt}()), abi.encodeWithSelector( @@ -43,9 +36,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { guardianSafe, guardianSafe, address(registry), - address(vestingAllocationFactory), - address(tokenOptionFactory), - address(restrictedTokenFactory) + address(metavestControllerFactory) ) ))); @@ -81,8 +72,6 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { metavestController controllerImpl = new metavestController(); vm.expectRevert(Initializable.InvalidInitialization.selector); controllerImpl.initialize( - address(123), // no-op - address(123), // no-op address(123), // no-op address(123), // no-op address(123), // no-op @@ -1816,8 +1805,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { } function test_UpgradeMetaVesTController() public { - // Deploy new implementation + // MetaLeX to release new implementation + vm.startPrank(deployer); address newImplementation = address(new metavestController()); + metavestControllerFactory.setRefImplementation(newImplementation); + vm.stopPrank(); // Upgrade to new implementation without initialization data diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 8e255bb..e24252d 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -8,9 +8,7 @@ import {BorgAuth} from "cybercorps-contracts/src/libs/auth.sol"; import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; import {CyberAgreementUtils} from "cybercorps-contracts/test/libs/CyberAgreementUtils.sol"; import {MockERC20} from "../mocks/MockERC20.sol"; -import {VestingAllocationFactory} from "../../src/VestingAllocationFactory.sol"; -import {TokenOptionFactory} from "../../src/TokenOptionFactory.sol"; -import {RestrictedTokenFactory} from "../../src/RestrictedTokenFactory.sol"; +import {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; import {MetaVestDealLib, MetaVestDeal} from "../../src/lib/MetaVestDealLib.sol"; contract MetaVesTControllerTestBase is Test { @@ -41,9 +39,7 @@ contract MetaVesTControllerTestBase is Test { BorgAuth auth; CyberAgreementRegistry registry; - VestingAllocationFactory vestingAllocationFactory; - TokenOptionFactory tokenOptionFactory; - RestrictedTokenFactory restrictedTokenFactory; + MetaVesTControllerFactory metavestControllerFactory; metavestController controller; @@ -88,6 +84,16 @@ contract MetaVesTControllerTestBase is Test { partyFields ); + metavestControllerFactory = MetaVesTControllerFactory(address(new ERC1967Proxy{salt: salt}( + address(new MetaVesTControllerFactory{salt: salt}()), + abi.encodeWithSelector( + MetaVesTControllerFactory.initialize.selector, + address(auth), + address(registry), + new metavestController() + ) + ))); + vm.stopPrank(); } From 140029efa51d203500d31b5f773f6bb9b18f0a85 Mon Sep 17 00:00:00 2001 From: detoo Date: Sun, 2 Nov 2025 20:43:30 -0800 Subject: [PATCH 21/25] fix: contract sizes --- src/lib/RestrictedTokenFactory.sol | 62 +++++++++++++++++++++++ src/lib/TokenOptionFactory.sol | 62 +++++++++++++++++++++++ src/lib/VestingAllocationFactory.sol | 51 +++++++++++++++++++ src/storage/MetaVesTControllerStorage.sol | 49 +++--------------- test/YearnBorgCompensation.t.sol | 2 +- 5 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 src/lib/RestrictedTokenFactory.sol create mode 100644 src/lib/TokenOptionFactory.sol create mode 100644 src/lib/VestingAllocationFactory.sol diff --git a/src/lib/RestrictedTokenFactory.sol b/src/lib/RestrictedTokenFactory.sol new file mode 100644 index 0000000..7cb8357 --- /dev/null +++ b/src/lib/RestrictedTokenFactory.sol @@ -0,0 +1,62 @@ +/* .o. + .888. + .8"888. + .8' `888. + .88ooo8888. + .8' `888. + o88o o8888o + + + + ooo ooooo . ooooo ooooooo ooooo + `88. .888' .o8 `888' `8888 d8' + 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P + 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' + 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. + 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b + o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o + + + + .oooooo. .o8 .oooooo. + d8P' `Y8b "888 d8P' `Y8b + 888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo. + 888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b + 888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 + `88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. + `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P + .o..P' 888 + `Y8P' o888o + _______________________________________________________________________________________________________ + + All software, documentation and other files and information in this repository (collectively, the "Software") + are copyright MetaLeX Labs, Inc., a Delaware corporation. + + All rights reserved. + + The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, + distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or + mechanical, including photocopying, recording, or by any information storage and retrieval system, + except with the express prior written permission of the copyright holder.*/ + +pragma solidity 0.8.28; + +import {RestrictedTokenAward} from "../RestrictedTokenAllocation.sol"; +import {MetaVestDeal} from "./MetaVestDealLib.sol"; + +library RestrictedTokenFactory { + function createRestrictedTokenAward(MetaVestDeal memory deal, address recipient) external returns (address) { + return address( + new RestrictedTokenAward( + deal.grantee, + recipient, + address(this), + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration, + deal.allocation, + deal.milestones + ) + ); + } +} diff --git a/src/lib/TokenOptionFactory.sol b/src/lib/TokenOptionFactory.sol new file mode 100644 index 0000000..23998a3 --- /dev/null +++ b/src/lib/TokenOptionFactory.sol @@ -0,0 +1,62 @@ +/* .o. + .888. + .8"888. + .8' `888. + .88ooo8888. + .8' `888. + o88o o8888o + + + + ooo ooooo . ooooo ooooooo ooooo + `88. .888' .o8 `888' `8888 d8' + 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P + 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' + 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. + 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b + o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o + + + + .oooooo. .o8 .oooooo. + d8P' `Y8b "888 d8P' `Y8b + 888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo. + 888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b + 888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 + `88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. + `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P + .o..P' 888 + `Y8P' o888o + _______________________________________________________________________________________________________ + + All software, documentation and other files and information in this repository (collectively, the "Software") + are copyright MetaLeX Labs, Inc., a Delaware corporation. + + All rights reserved. + + The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, + distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or + mechanical, including photocopying, recording, or by any information storage and retrieval system, + except with the express prior written permission of the copyright holder.*/ + +pragma solidity 0.8.28; + +import {TokenOptionAllocation} from "../TokenOptionAllocation.sol"; +import {MetaVestDeal} from "./MetaVestDealLib.sol"; + +library TokenOptionFactory { + function createTokenOptionAllocation(MetaVestDeal memory deal, address recipient) external returns (address) { + return address( + new TokenOptionAllocation( + deal.grantee, + recipient, + address(this), + deal.paymentToken, + deal.exercisePrice, + deal.shortStopDuration, + deal.allocation, + deal.milestones + ) + ); + } +} diff --git a/src/lib/VestingAllocationFactory.sol b/src/lib/VestingAllocationFactory.sol new file mode 100644 index 0000000..1dcfb19 --- /dev/null +++ b/src/lib/VestingAllocationFactory.sol @@ -0,0 +1,51 @@ +/* .o. + .888. + .8"888. + .8' `888. + .88ooo8888. + .8' `888. + o88o o8888o + + + + ooo ooooo . ooooo ooooooo ooooo + `88. .888' .o8 `888' `8888 d8' + 888b d'888 .ooooo. .o888oo .oooo. 888 .ooooo. Y888..8P + 8 Y88. .P 888 d88' `88b 888 `P )88b 888 d88' `88b `8888' + 8 `888' 888 888ooo888 888 .oP"888 888 888ooo888 .8PY888. + 8 Y 888 888 .o 888 . d8( 888 888 o 888 .o d8' `888b + o8o o888o `Y8bod8P' "888" `Y888""8o o888ooooood8 `Y8bod8P' o888o o88888o + + + + .oooooo. .o8 .oooooo. + d8P' `Y8b "888 d8P' `Y8b + 888 oooo ooo 888oooo. .ooooo. oooo d8b 888 .ooooo. oooo d8b oo.ooooo. + 888 `88. .8' d88' `88b d88' `88b `888""8P 888 d88' `88b `888""8P 888' `88b + 888 `88..8' 888 888 888ooo888 888 888 888 888 888 888 888 + `88b ooo `888' 888 888 888 .o 888 `88b ooo 888 888 888 888 888 .o. + `Y8bood8P' .8' `Y8bod8P' `Y8bod8P' d888b `Y8bood8P' `Y8bod8P' d888b 888bod8P' Y8P + .o..P' 888 + `Y8P' o888o + _______________________________________________________________________________________________________ + + All software, documentation and other files and information in this repository (collectively, the "Software") + are copyright MetaLeX Labs, Inc., a Delaware corporation. + + All rights reserved. + + The Software is proprietary and shall not, in part or in whole, be used, copied, modified, merged, published, + distributed, transmitted, sublicensed, sold, or otherwise used in any form or by any means, electronic or + mechanical, including photocopying, recording, or by any information storage and retrieval system, + except with the express prior written permission of the copyright holder.*/ + +pragma solidity 0.8.28; + +import {VestingAllocation} from "../VestingAllocation.sol"; +import {MetaVestDeal} from "./MetaVestDealLib.sol"; + +library VestingAllocationFactory { + function createVestingAllocation(MetaVestDeal memory deal, address recipient) external returns (address) { + return address(new VestingAllocation(deal.grantee, recipient, address(this), deal.allocation, deal.milestones)); + } +} diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index 1b4e03c..a490f00 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -43,11 +43,11 @@ pragma solidity 0.8.28; import {BaseAllocation, IConditionM, IERC20M} from "../BaseAllocation.sol"; -import {RestrictedTokenAward} from "../RestrictedTokenAllocation.sol"; -import {TokenOptionAllocation} from "../TokenOptionAllocation.sol"; -import {VestingAllocation} from "../VestingAllocation.sol"; import {EnumerableSet} from "../lib/EnumberableSet.sol"; import {MetaVestDeal, MetaVestDealLib, MetaVestType} from "../lib/MetaVestDealLib.sol"; +import {RestrictedTokenFactory} from "../lib/RestrictedTokenFactory.sol"; +import {TokenOptionFactory} from "../lib/TokenOptionFactory.sol"; +import {VestingAllocationFactory} from "../lib/VestingAllocationFactory.sol"; import {ICyberAgreementRegistry} from "cybercorps-contracts/src/interfaces/ICyberAgreementRegistry.sol"; library MetaVesTControllerStorage { @@ -164,43 +164,6 @@ library MetaVesTControllerStorage { } } - function createVestingAllocation(MetaVestDeal storage deal, address recipient) internal returns (address) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - return address(new VestingAllocation(deal.grantee, recipient, address(this), deal.allocation, deal.milestones)); - } - - function createTokenOptionAllocation(MetaVestDeal storage deal, address recipient) internal returns (address) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - return address( - new TokenOptionAllocation( - deal.grantee, - recipient, - address(this), - deal.paymentToken, - deal.exercisePrice, - deal.shortStopDuration, - deal.allocation, - deal.milestones - ) - ); - } - - function createRestrictedTokenAward(MetaVestDeal storage deal, address recipient) internal returns (address) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - return address( - new RestrictedTokenAward( - deal.grantee, - recipient, - address(this), - deal.paymentToken, - deal.exercisePrice, - deal.shortStopDuration, - deal.allocation, - deal.milestones - ) - ); - } - function proposeAndSignDeal( bytes32 templateId, uint256 salt, @@ -326,11 +289,11 @@ library MetaVesTControllerStorage { // Interaction: Create and provision MetaVesT if (deal.metavestType == MetaVestType.Vesting) { - deal.metavest = createVestingAllocation(deal, recipient); + deal.metavest = VestingAllocationFactory.createVestingAllocation(deal, recipient); } else if (deal.metavestType == MetaVestType.TokenOption) { - deal.metavest = createTokenOptionAllocation(deal, recipient); + deal.metavest = TokenOptionFactory.createTokenOptionAllocation(deal, recipient); } else if (deal.metavestType == MetaVestType.RestrictedTokenAward) { - deal.metavest = createRestrictedTokenAward(deal, recipient); + deal.metavest = RestrictedTokenFactory.createRestrictedTokenAward(deal, recipient); } else { return (address(0), 0, MetaVesTController_IncorrectMetaVesTType.selector); } diff --git a/test/YearnBorgCompensation.t.sol b/test/YearnBorgCompensation.t.sol index c114768..74e09d2 100644 --- a/test/YearnBorgCompensation.t.sol +++ b/test/YearnBorgCompensation.t.sol @@ -14,7 +14,7 @@ import {ProposeAllGuardiansMetaVestDealScript} from "../scripts/proposeAllGuardi import {SignDealAndCreateMetavestScript} from "../scripts/signDealAndCreateMetavest.s.sol"; import {YearnBorgCompensation2025_2026} from "../scripts/lib/YearnBorgCompensation2025_2026.sol"; import {GnosisTransaction} from "./lib/safe.sol"; -import {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; +import {MetaVesTControllerFactory} from "../src/MetaVesTControllerFactory.sol"; // Test with fresh deployment (except third-party dependencies) // - Use third-party dependencies on Ethereum mainnet From 0510241b11a277992e73b150841ddd17d22b8d2d Mon Sep 17 00:00:00 2001 From: detoo Date: Wed, 5 Nov 2025 17:10:46 -0800 Subject: [PATCH 22/25] chore: minor refactoring --- test/controller.t.sol | 2 +- test/lib/ERC1967ProxyLib.sol | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/controller.t.sol b/test/controller.t.sol index 423d662..53cfb95 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -1820,7 +1820,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { // Owner should be able to upgrade it vm.prank(guardianSafe); controller.upgradeToAndCall(newImplementation, ""); - assertEq(address(controller).getErc1967Implementation(vm), newImplementation); + assertEq(address(controller).getErc1967Implementation(), newImplementation); // Verify the controller still works diff --git a/test/lib/ERC1967ProxyLib.sol b/test/lib/ERC1967ProxyLib.sol index 087302b..69b246a 100644 --- a/test/lib/ERC1967ProxyLib.sol +++ b/test/lib/ERC1967ProxyLib.sol @@ -42,12 +42,15 @@ except with the express prior written permission of the copyright holder.*/ pragma solidity 0.8.28; import {Vm} from "forge-std/Test.sol"; +import {ERC1967Utils} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Utils.sol"; library ERC1967ProxyLib { - function getErc1967Implementation(address proxy, Vm vm) internal view returns (address) { + Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function getErc1967Implementation(address proxy) internal view returns (address) { // Workaround since there is no public function to get the implementation address: // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/acd4ff74de833399287ed6b31b4debf6b2b35527/contracts/proxy/ERC1967/ERC1967Proxy.sol#L35 - return address(uint160(uint256(vm.load(proxy, 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)))); + return address(uint160(uint256(vm.load(proxy, ERC1967Utils.IMPLEMENTATION_SLOT)))); } } From c2531a9dc3939966093ed899717f557b1a1dce25 Mon Sep 17 00:00:00 2001 From: detoo Date: Thu, 11 Dec 2025 11:31:30 -0800 Subject: [PATCH 23/25] chore: refactor and remove the unnecessary event restrictions --- src/MetaVesTController.sol | 165 +++------------------- src/storage/MetaVesTControllerStorage.sol | 133 +++++++++++------ 2 files changed, 115 insertions(+), 183 deletions(-) diff --git a/src/MetaVesTController.sol b/src/MetaVesTController.sol index fb405df..85af111 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -34,58 +34,17 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; - /// @dev opinionated time limit for a MetaVesT amendment, one calendar week in seconds - uint256 internal constant AMENDMENT_TIME_LIMIT = 604800; - uint256 internal constant ARRAY_LENGTH_LIMIT = 20; - - /// - /// EVENTS - /// - - event MetaVesTController_AmendmentConsentUpdated(bytes4 indexed msgSig, address indexed grantee, bool inFavor); - event MetaVesTController_AmendmentProposed(address indexed grant, bytes4 msgSig); - event MetaVesTController_AuthorityUpdated(address indexed newAuthority); - event MetaVesTController_ConditionUpdated(address indexed condition, bytes4 functionSig); - event MetaVesTController_DaoUpdated(address newDao); - event MetaVesTController_MajorityAmendmentProposed(string indexed set, bytes4 msgSig, bytes callData, uint256 totalVotingPower); - event MetaVesTController_MajorityAmendmentVoted(string indexed set, bytes4 msgSig, address grantee, bool inFavor, uint256 votingPower, uint256 currentVotingPower, uint256 totalVotingPower); - event MetaVesTController_SetCreated(string indexed set); - event MetaVesTController_SetRemoved(string indexed set); - event MetaVesTController_AddressAddedToSet(string set, address indexed grantee); - event MetaVesTController_AddressRemovedFromSet(string set, address indexed grantee); - event MetaVesTController_DealProposed( - bytes32 indexed agreementId, - address indexed grantee, - MetaVestType MetaVestType, - BaseAllocation.Allocation allocation, - BaseAllocation.Milestone[] milestones, - bool hasSecret, - address registry - ); - event MetaVesTController_DealFinalizedAndMetaVestCreated( - bytes32 indexed agreementId, - address indexed recipient, - address metavest - ); - - error NotRefImplementation(); - /// /// FUNCTIONS /// modifier conditionCheck() { - address failedCondition = MetaVesTControllerStorage.conditionCheck(msg.sig); - if (failedCondition != address(0)) { - revert MetaVesTControllerStorage.MetaVesTController_ConditionNotSatisfied(failedCondition); - } + MetaVesTControllerStorage.conditionCheck(msg.sig); _; } modifier consentCheck(address _grant, bytes calldata _data) { - bytes4 error = MetaVesTControllerStorage.consentCheck(msg.sig, _grant, _data); - // This is a hack because libraries cannot emit events nor errors - _checkError(error); + MetaVesTControllerStorage.consentCheck(msg.sig, _grant, _data); _; } @@ -135,7 +94,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (msg.sender != grantee) revert MetaVesTControllerStorage.MetaVesTController_OnlyGranteeMayCall(); st.functionToGranteeToAmendmentPending[_msgSig][_grant].inFavor = _inFavor; - emit MetaVesTController_AmendmentConsentUpdated(_msgSig, msg.sender, _inFavor); + emit MetaVesTControllerStorage.MetaVesTController_AmendmentConsentUpdated(_msgSig, msg.sender, _inFavor); } /// @notice enables the DAO to toggle whether a function requires Condition contract calls (enabling time delays, signature conditions, etc.) @@ -151,7 +110,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (st.functionToConditions[_functionSig][i] == _condition) revert MetaVesTControllerStorage.MetaVestController_DuplicateCondition(); } st.functionToConditions[_functionSig].push(_condition); - emit MetaVesTController_ConditionUpdated(_condition, _functionSig); + emit MetaVesTControllerStorage.MetaVesTController_ConditionUpdated(_condition, _functionSig); } function removeFunctionCondition(address _condition, bytes4 _functionSig) external onlyDao { @@ -164,7 +123,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { break; } } - emit MetaVesTController_ConditionUpdated(_condition, _functionSig); + emit MetaVesTControllerStorage.MetaVesTController_ConditionUpdated(_condition, _functionSig); } // It can be called by anyone but must have DAO's or delegate's signature @@ -179,13 +138,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes32 secretHash, uint256 expiry ) external returns (bytes32) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - - // Check: verify inputs - if (parties[1] != dealDraft.grantee) revert MetaVesTControllerStorage.MetaVesTController_GranteeNotDirectParty(); - if (partyValues.length < 2) revert MetaVesTControllerStorage.MetaVesTController_CounterPartyNotFound(); - if (partyValues[1].length != partyValues[0].length) revert MetaVesTControllerStorage.MetaVesTController_PartyValuesLengthMismatch(); - MetaVestDeal memory dealProposed = MetaVesTControllerStorage.proposeAndSignDeal( templateId, salt, @@ -198,11 +150,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { expiry ); - emit MetaVesTController_DealProposed( - dealProposed.agreementId, dealProposed.grantee, dealProposed.metavestType, dealProposed.allocation, dealProposed.milestones, - secretHash > 0, - st.registry - ); return dealProposed.agreementId; } @@ -214,11 +161,8 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes memory signature, string memory secret ) external conditionCheck() returns (address) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; - // Interaction: finalize the deal and create metavest contract - (address newMetavest, uint256 total, bytes4 error) = MetaVesTControllerStorage.signDealAndCreateMetavest( + (address newMetavest, uint256 total) = MetaVesTControllerStorage.signDealAndCreateMetavest( grantee, recipient, agreementId, @@ -227,12 +171,12 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { secret ); - _checkError(error); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); // Interaction: transfer tokens to escrow - safeTransferFrom(deal.allocation.tokenContract, st.authority, newMetavest, total); + safeTransferFrom(st.deals[agreementId].allocation.tokenContract, st.authority, newMetavest, total); - emit MetaVesTController_DealFinalizedAndMetaVestCreated(agreementId, recipient, newMetavest); + emit MetaVesTControllerStorage.MetaVesTController_DealFinalizedAndMetaVestCreated(agreementId, recipient, newMetavest); return newMetavest; } @@ -296,7 +240,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address _tokenContract = BaseAllocation(_grant).getMetavestDetails().tokenContract; if (_milestone.milestoneAward == 0) revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); - if (_milestone.conditionContracts.length > ARRAY_LENGTH_LIMIT) revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); + if (_milestone.conditionContracts.length > MetaVesTControllerStorage.ARRAY_LENGTH_LIMIT) revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); if (_milestone.complete == true) revert MetaVesTControllerStorage.MetaVesTController_MilestoneIndexCompletedOrDoesNotExist(); if ( IERC20M(_tokenContract).allowance(msg.sender, address(this)) < _milestone.milestoneAward || @@ -385,7 +329,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (msg.sender != st._pendingAuthority) revert MetaVesTControllerStorage.MetaVesTController_OnlyPendingAuthority(); delete st._pendingAuthority; st.authority = msg.sender; - emit MetaVesTController_AuthorityUpdated(msg.sender); + emit MetaVesTControllerStorage.MetaVesTController_AuthorityUpdated(msg.sender); } /// @notice allows the 'dao' to propose a replacement to their address. First step in two-step address change, as '_newDao' will subsequently need to call 'acceptDaoRole()' @@ -404,7 +348,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (msg.sender != st._pendingDao) revert MetaVesTControllerStorage.MetaVesTController_OnlyPendingDao(); delete st._pendingDao; st.dao = msg.sender; - emit MetaVesTController_DaoUpdated(msg.sender); + emit MetaVesTControllerStorage.MetaVesTController_DaoUpdated(msg.sender); } /// @notice for 'authority' to propose a metavest detail amendment @@ -422,7 +366,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { keccak256(_callData), false ); - emit MetaVesTController_AmendmentProposed(_grant, _msgSig); + emit MetaVesTControllerStorage.MetaVesTController_AmendmentProposed(_grant, _msgSig); } /// @notice for 'authority' to propose a metavest detail amendment @@ -434,20 +378,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes4 _msgSig, bytes calldata _callData ) external onlyAuthority { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - - // Check: verify inputs - - if(!doesSetExist(setName)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); - if(_callData.length!=68) revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); - bytes32 nameHash = keccak256(bytes(setName)); - //if the majority proposal is already pending and not expired, revert - if ((st.functionToSetMajorityProposal[_msgSig][nameHash].isPending && block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) || st.setMajorityVoteActive[nameHash]) - revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); - - uint256 totalVotingPower = MetaVesTControllerStorage.proposeMajorityMetavestAmendment(setName, _msgSig, _callData); - - emit MetaVesTController_MajorityAmendmentProposed(setName, _msgSig, _callData, totalVotingPower); + MetaVesTControllerStorage.proposeMajorityMetavestAmendment(setName, _msgSig, _callData); } /// @notice for 'authority' to cancel a metavest majority amendment @@ -455,9 +386,9 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { /// @param _msgSig function signature of the function in this controller which (if successfully executed) will execute the metavest detail update function cancelExpiredMajorityMetavestAmendment(string memory _setName, bytes4 _msgSig) external onlyAuthority { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - if(!doesSetExist(_setName)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); + if(!MetaVesTControllerStorage.doesSetExist(_setName)) revert MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); bytes32 nameHash = keccak256(bytes(_setName)); - if (!st.setMajorityVoteActive[nameHash] || block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) revert MetaVesTControllerStorage.MetaVesTController_AmendmentCannotBeCanceled(); + if (!st.setMajorityVoteActive[nameHash] || block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + MetaVesTControllerStorage.AMENDMENT_TIME_LIMIT) revert MetaVesTControllerStorage.MetaVesTController_AmendmentCannotBeCanceled(); st.setMajorityVoteActive[nameHash] = false; } @@ -487,7 +418,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { proposal.voters.push(_grant); proposal.currentVotingPower += _callerPower; } - emit MetaVesTController_MajorityAmendmentVoted(_setName, _msgSig, _grant, _inFavor, _callerPower, proposal.currentVotingPower, proposal.totalVotingPower); + emit MetaVesTControllerStorage.MetaVesTController_MajorityAmendmentVoted(_setName, _msgSig, _grant, _inFavor, _callerPower, proposal.currentVotingPower, proposal.totalVotingPower); } /// @notice resets applicable amendment variables because either the applicable amending function has been successfully called or a pending amendment is being overridden with a new one @@ -508,7 +439,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); //check the majority proposal time bytes32 nameHash = keccak256(bytes(_setName)); - return (block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT); + return (block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + MetaVesTControllerStorage.AMENDMENT_TIME_LIMIT); } function createSet(string memory _name) external onlyAuthority { @@ -519,7 +450,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (bytes(_name).length > 512) revert MetaVesTControllerStorage.MetaVesTController_StringTooLong(); st.setNames.add(nameHash); - emit MetaVesTController_SetCreated(_name); + emit MetaVesTControllerStorage.MetaVesTController_SetCreated(_name); } function removeSet(string memory _name) external onlyAuthority { @@ -535,11 +466,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { } st.setNames.remove(nameHash); - emit MetaVesTController_SetRemoved(_name); - } - - function doesSetExist(string memory _name) internal view returns (bool) { - return MetaVesTControllerStorage.getStorage().setNames.contains(keccak256(bytes(_name))); + emit MetaVesTControllerStorage.MetaVesTController_SetRemoved(_name); } function isMetavestInSet(address _metavest) internal view returns (bool) { @@ -579,7 +506,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (st.setMajorityVoteActive[nameHash]) revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); st.sets[nameHash].add(_metaVest); - emit MetaVesTController_AddressAddedToSet(_name, _metaVest); + emit MetaVesTControllerStorage.MetaVesTController_AddressAddedToSet(_name, _metaVest); } function removeMetaVestFromSet(string memory _name, address _metaVest) external onlyAuthority { @@ -590,7 +517,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { if (!st.sets[nameHash].contains(_metaVest)) revert MetaVesTControllerStorage.MetaVestController_MetaVestNotInSet(); st.sets[nameHash].remove(_metaVest); - emit MetaVesTController_AddressRemovedFromSet(_name, _metaVest); + emit MetaVesTControllerStorage.MetaVesTController_AddressRemovedFromSet(_name, _metaVest); } function getDeal(bytes32 agreementId) public view returns (MetaVestDeal memory) { @@ -648,52 +575,6 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { ); } - /// @notice This is a hack because libraries cannot throw errors, so it returns error codes for the core contract - /// to throw accordingly - /// @dev As inefficient as it looks, it actually saves 3000+ bytes because otherwise logic that throws errors - /// would not be able to move to external libraries - function _checkError(bytes4 error) internal { - if (error == 0) { - return; - - } else if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAddress.selector) { - revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal.selector) { - revert MetaVesTControllerStorage.MetaVesTController_CliffGreaterThanTotal(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_LengthMismatch.selector) { - revert MetaVesTControllerStorage.MetaVesTController_LengthMismatch(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_ZeroAmount.selector) { - revert MetaVesTControllerStorage.MetaVesTController_ZeroAmount(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom.selector) { - revert MetaVesTControllerStorage.MetaVesTController_AmountNotApprovedForTransferFrom(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType.selector) { - revert MetaVesTControllerStorage.MetaVesTController_IncorrectMetaVesTType(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector) { - revert MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector) { - revert MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_DealVoided.selector) { - revert MetaVesTControllerStorage.MetaVesTController_DealVoided(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized.selector) { - revert MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized(); - - } else if (error == MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector) { - revert MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch(); - - } else { - revert MetaVesTControllerStorage.MetaVesTController_UnknownError(error, ""); - } - } - // ======================== // UUPSUpgradeable // ======================== @@ -705,7 +586,7 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address newImplementation ) internal override onlyAuthority { if (IMetaVesTControllerFactory(MetaVesTControllerStorage.getStorage().upgradeFactory).getRefImplementation() != newImplementation) { - revert NotRefImplementation(); + revert MetaVesTControllerStorage.NotRefImplementation(); } } } diff --git a/src/storage/MetaVesTControllerStorage.sol b/src/storage/MetaVesTControllerStorage.sol index a490f00..e5277f3 100644 --- a/src/storage/MetaVesTControllerStorage.sol +++ b/src/storage/MetaVesTControllerStorage.sol @@ -57,6 +57,8 @@ library MetaVesTControllerStorage { // Storage slot for our struct bytes32 internal constant STORAGE_POSITION = keccak256("cybercorp.metavest.controller.storage.v1"); + /// @dev opinionated time limit for a MetaVesT amendment, one calendar week in seconds + uint256 internal constant AMENDMENT_TIME_LIMIT = 604800; uint256 internal constant ARRAY_LENGTH_LIMIT = 20; struct AmendmentProposal { @@ -115,6 +117,36 @@ library MetaVesTControllerStorage { mapping(address => bytes32) metavestAgreementIds; } + /// + /// Events + /// + + event MetaVesTController_AmendmentConsentUpdated(bytes4 indexed msgSig, address indexed grantee, bool inFavor); + event MetaVesTController_AmendmentProposed(address indexed grant, bytes4 msgSig); + event MetaVesTController_AuthorityUpdated(address indexed newAuthority); + event MetaVesTController_ConditionUpdated(address indexed condition, bytes4 functionSig); + event MetaVesTController_DaoUpdated(address newDao); + event MetaVesTController_MajorityAmendmentProposed(string indexed set, bytes4 msgSig, bytes callData, uint256 totalVotingPower); + event MetaVesTController_MajorityAmendmentVoted(string indexed set, bytes4 msgSig, address grantee, bool inFavor, uint256 votingPower, uint256 currentVotingPower, uint256 totalVotingPower); + event MetaVesTController_SetCreated(string indexed set); + event MetaVesTController_SetRemoved(string indexed set); + event MetaVesTController_AddressAddedToSet(string set, address indexed grantee); + event MetaVesTController_AddressRemovedFromSet(string set, address indexed grantee); + event MetaVesTController_DealProposed( + bytes32 indexed agreementId, + address indexed grantee, + MetaVestType MetaVestType, + BaseAllocation.Allocation allocation, + BaseAllocation.Milestone[] milestones, + bool hasSecret, + address registry + ); + event MetaVesTController_DealFinalizedAndMetaVestCreated( + bytes32 indexed agreementId, + address indexed recipient, + address metavest + ); + /// /// ERRORS /// @@ -156,6 +188,7 @@ library MetaVesTControllerStorage { error MetaVesTController_PartyValuesLengthMismatch(); error MetaVesTController_CounterPartyValueMismatch(); error MetaVesTController_UnknownError(bytes4 error, bytes data); + error NotRefImplementation(); function getStorage() internal pure returns (MetaVesTControllerData storage st) { bytes32 position = STORAGE_POSITION; @@ -175,7 +208,12 @@ library MetaVesTControllerStorage { bytes32 secretHash, uint256 expiry ) external returns (MetaVestDeal memory) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + MetaVesTControllerData storage st = getStorage(); + + // Check: verify inputs + if (parties[1] != dealDraft.grantee) revert MetaVesTController_GranteeNotDirectParty(); + if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); + if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); // Call internal function to avoid stack-too-deep errors dealDraft.agreementId = ICyberAgreementRegistry(st.registry) @@ -196,6 +234,12 @@ library MetaVesTControllerStorage { st.deals[dealDraft.agreementId] = dealDraft; st.dealIds.push(dealDraft.agreementId); + emit MetaVesTController_DealProposed( + dealDraft.agreementId, dealDraft.grantee, dealDraft.metavestType, dealDraft.allocation, dealDraft.milestones, + secretHash > 0, + st.registry + ); + return dealDraft; } @@ -206,25 +250,24 @@ library MetaVesTControllerStorage { string[] memory partyValues, bytes memory signature, string memory secret - ) external returns (address newMetavest, uint256 total, bytes4 error) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + ) external returns (address newMetavest, uint256 total) { + MetaVesTControllerData storage st = getStorage(); + MetaVestDeal storage deal = getStorage().deals[agreementId]; // Check: verify inputs if (ICyberAgreementRegistry(st.registry).isVoided(agreementId)) { - return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealVoided.selector); + revert MetaVesTController_DealVoided(); } if (ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) { - return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_DealAlreadyFinalized.selector); + revert MetaVesTController_DealAlreadyFinalized(); } string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) { - return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); + revert MetaVesTController_CounterPartyValueMismatch(); } - MetaVestDeal storage deal = MetaVesTControllerStorage.getStorage().deals[agreementId]; - if ( deal.grantee == address(0) || recipient == address(0) || deal.allocation.tokenContract == address(0) || (deal.metavestType == MetaVestType.TokenOption @@ -234,38 +277,38 @@ library MetaVesTControllerStorage { && deal.paymentToken == address(0) && deal.exercisePrice == 0) ) { - return (address(0), 0, MetaVesTController_ZeroAddress.selector); + revert MetaVesTController_ZeroAddress(); } if ( deal.allocation.vestingCliffCredit > deal.allocation.tokenStreamTotal || deal.allocation.unlockingCliffCredit > deal.allocation.tokenStreamTotal ) { - return (address(0), 0, MetaVesTController_CliffGreaterThanTotal.selector); + revert MetaVesTController_CliffGreaterThanTotal(); } uint256 milestoneTotal = 0; if (deal.milestones.length != 0) { if (deal.milestones.length > ARRAY_LENGTH_LIMIT) { - return (address(0), 0, MetaVesTController_LengthMismatch.selector); + revert MetaVesTController_LengthMismatch(); } for (uint256 i; i < deal.milestones.length; ++i) { if (deal.milestones[i].conditionContracts.length > ARRAY_LENGTH_LIMIT) { - return (address(0), 0, MetaVesTController_LengthMismatch.selector); + revert MetaVesTController_LengthMismatch(); } if (deal.milestones[i].milestoneAward == 0) { - return (address(0), 0, MetaVesTController_ZeroAmount.selector); + revert MetaVesTController_ZeroAmount(); } milestoneTotal += deal.milestones[i].milestoneAward; } } total = deal.allocation.tokenStreamTotal + milestoneTotal; if (total == 0) { - return (address(0), 0, MetaVesTController_ZeroAmount.selector); + revert MetaVesTController_ZeroAmount(); } if ( IERC20M(deal.allocation.tokenContract).allowance(st.authority, address(this)) < total || IERC20M(deal.allocation.tokenContract).balanceOf(st.authority) < total ) { - return (address(0), 0, MetaVesTController_AmountNotApprovedForTransferFrom.selector); + revert MetaVesTController_AmountNotApprovedForTransferFrom(); } // Interaction: Finalize agreement @@ -282,7 +325,7 @@ library MetaVesTControllerStorage { // but we check it anyways just in case string[] memory registryValues = ICyberAgreementRegistry(st.registry).getSignerValues(agreementId, grantee); if (keccak256(abi.encode(registryValues)) != keccak256(abi.encode(partyValues))) { - return (address(0), 0, MetaVesTControllerStorage.MetaVesTController_CounterPartyValueMismatch.selector); + revert MetaVesTController_CounterPartyValueMismatch(); } } ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); @@ -295,21 +338,26 @@ library MetaVesTControllerStorage { } else if (deal.metavestType == MetaVestType.RestrictedTokenAward) { deal.metavest = RestrictedTokenFactory.createRestrictedTokenAward(deal, recipient); } else { - return (address(0), 0, MetaVesTController_IncorrectMetaVesTType.selector); + revert MetaVesTController_IncorrectMetaVesTType(); } st.metavestAgreementIds[deal.metavest] = agreementId; - return (deal.metavest, total, 0); + return (deal.metavest, total); } - function proposeMajorityMetavestAmendment(string memory setName, bytes4 _msgSig, bytes calldata _callData) - external - returns (uint256 totalVotingPower) - { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + function proposeMajorityMetavestAmendment(string memory setName, bytes4 _msgSig, bytes calldata _callData) external { + MetaVesTControllerData storage st = getStorage(); + + // Check: verify inputs + if(!doesSetExist(setName)) revert MetaVesTController_SetDoesNotExist(); + if(_callData.length!=68) revert MetaVesTController_LengthMismatch(); bytes32 nameHash = keccak256(bytes(setName)); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = + //if the majority proposal is already pending and not expired, revert + if ((st.functionToSetMajorityProposal[_msgSig][nameHash].isPending && block.timestamp < st.functionToSetMajorityProposal[_msgSig][nameHash].time + AMENDMENT_TIME_LIMIT) || st.setMajorityVoteActive[nameHash]) + revert MetaVesTController_AmendmentAlreadyPending(); + + MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[_msgSig][nameHash]; proposal.isPending = true; proposal.dataHash = keccak256(_callData[_callData.length - 32:]); @@ -317,6 +365,8 @@ library MetaVesTControllerStorage { proposal.voters = new address[](0); proposal.currentVotingPower = 0; + uint256 totalVotingPower = 0; + for (uint256 i; i < st.sets[nameHash].length(); ++i) { uint256 _votingPower = BaseAllocation(st.sets[nameHash].at(i)).getMajorityVotingPower(); totalVotingPower += _votingPower; @@ -326,52 +376,49 @@ library MetaVesTControllerStorage { st.setMajorityVoteActive[nameHash] = true; - return totalVotingPower; + emit MetaVesTController_MajorityAmendmentProposed(setName, _msgSig, _callData, totalVotingPower); } - function conditionCheck(bytes4 msgSig) external returns (address) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + function conditionCheck(bytes4 msgSig) external { + MetaVesTControllerData storage st = getStorage(); address[] memory conditions = st.functionToConditions[msgSig]; for (uint256 i; i < conditions.length; ++i) { if (!IConditionM(conditions[i]).checkCondition(address(this), msgSig, "")) { - return conditions[i]; + revert MetaVesTController_ConditionNotSatisfied(conditions[i]); } } - return address(0); } - function consentCheck(bytes4 msgSig, address _grant, bytes calldata _data) external returns (bytes4) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + function consentCheck(bytes4 msgSig, address _grant, bytes calldata _data) external { + MetaVesTControllerData storage st = getStorage(); if (isMetavestInSet(_grant)) { bytes32 set = getSetOfMetavest(_grant); - MetaVesTControllerStorage.MajorityAmendmentProposal storage proposal = + MajorityAmendmentProposal storage proposal = st.functionToSetMajorityProposal[msgSig][set]; if (proposal.appliedProposalCreatedAt[_grant] == proposal.time) { - return MetaVesTControllerStorage.MetaVesTController_AmendmentCanOnlyBeAppliedOnce.selector; + revert MetaVesTController_AmendmentCanOnlyBeAppliedOnce(); } if (_data.length > 32 && _data.length < 69) { if ( !proposal.isPending || proposal.totalVotingPower > proposal.currentVotingPower * 2 || keccak256(_data[_data.length - 32:]) != proposal.dataHash ) { - return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented - .selector; + revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); } } else { - return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; + revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); } } else { - MetaVesTControllerStorage.AmendmentProposal storage proposal = + AmendmentProposal storage proposal = st.functionToGranteeToAmendmentPending[msgSig][_grant]; if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { - return MetaVesTControllerStorage.MetaVesTController_AmendmentNeitherMutualNorMajorityConsented.selector; + revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); } } - return 0; } function isMetavestInSet(address _metavest) internal view returns (bool) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + MetaVesTControllerData storage st = getStorage(); uint256 length = st.setNames.length(); for (uint256 i = 0; i < length; i++) { bytes32 nameHash = st.setNames.at(i); @@ -383,7 +430,7 @@ library MetaVesTControllerStorage { } function getSetOfMetavest(address _metavest) internal view returns (bytes32) { - MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + MetaVesTControllerData storage st = getStorage(); uint256 length = st.setNames.length(); for (uint256 i = 0; i < length; i++) { bytes32 nameHash = st.setNames.at(i); @@ -393,4 +440,8 @@ library MetaVesTControllerStorage { } return ""; } + + function doesSetExist(string memory _name) internal view returns (bool) { + return getStorage().setNames.contains(keccak256(bytes(_name))); + } } From b4d6fa170ea4b6ddf374f041ed618731457f5c72 Mon Sep 17 00:00:00 2001 From: detoo Date: Fri, 12 Dec 2025 13:32:59 -0800 Subject: [PATCH 24/25] chore: cherry-pick test setup improvement from another branch --- test/AuditBaseA.t.sol | 2 - test/AuditBaseA2.t.sol | 2 - test/AuditBaseC.t.sol | 4 +- test/AuditBaseC3.t.sol | 4 +- test/amendement.t.sol | 2 +- test/controller.t.sol | 684 +----------------- test/controller2.t.sol | 289 ++++++++ test/lib/MetaVesTControllerTestBase.sol | 11 +- .../MetaVesTControllerTestBaseExtended.sol | 419 +++++++++++ 9 files changed, 726 insertions(+), 691 deletions(-) create mode 100644 test/controller2.t.sol create mode 100644 test/lib/MetaVesTControllerTestBaseExtended.sol diff --git a/test/AuditBaseA.t.sol b/test/AuditBaseA.t.sol index 6d7cb74..52a8e76 100644 --- a/test/AuditBaseA.t.sol +++ b/test/AuditBaseA.t.sol @@ -1,7 +1,5 @@ pragma solidity ^0.8.20; -import "forge-std/Test.sol"; - import "../test/amendement.t.sol"; contract EvilGrant { diff --git a/test/AuditBaseA2.t.sol b/test/AuditBaseA2.t.sol index b1bea03..c5bb4cc 100644 --- a/test/AuditBaseA2.t.sol +++ b/test/AuditBaseA2.t.sol @@ -1,7 +1,5 @@ pragma solidity ^0.8.20; -import "forge-std/Test.sol"; - import "../test/amendement.t.sol"; contract EvilGrant { diff --git a/test/AuditBaseC.t.sol b/test/AuditBaseC.t.sol index f4afe3c..35df7a5 100644 --- a/test/AuditBaseC.t.sol +++ b/test/AuditBaseC.t.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "../test/controller.t.sol"; +import "./lib/MetaVesTControllerTestBaseExtended.sol"; -contract Audit is MetaVestControllerTest { +contract Audit is MetaVesTControllerTestBaseExtended { function test_RevertIf_AuditTerminateFailAfterWithdraw() public { // template from testTerminateVestAndRecoverSlowUnlock diff --git a/test/AuditBaseC3.t.sol b/test/AuditBaseC3.t.sol index d18d5b7..d53ac3b 100644 --- a/test/AuditBaseC3.t.sol +++ b/test/AuditBaseC3.t.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "../test/controller.t.sol"; +import "./lib/MetaVesTControllerTestBaseExtended.sol"; -contract Audit is MetaVestControllerTest { +contract Audit is MetaVesTControllerTestBaseExtended { function test_RevertIf_AuditTerminateFailAfterWithdraw() public { // template from testTerminateVestAndRecoverSlowUnlock diff --git a/test/amendement.t.sol b/test/amendement.t.sol index 222cc43..c8d1f24 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.20; -import "forge-std/Test.sol"; +import {console} from "forge-std/Console.sol"; import "../src/BaseAllocation.sol"; import "../src/TokenOptionAllocation.sol"; import "../src/RestrictedTokenAllocation.sol"; diff --git a/test/controller.t.sol b/test/controller.t.sol index 53cfb95..4de04e4 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -1,73 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.20; +import {console2} from "forge-std/console2.sol"; import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; import "../src/RestrictedTokenAllocation.sol"; import "../src/TokenOptionAllocation.sol"; import "../src/VestingAllocation.sol"; import "../src/interfaces/IAllocationFactory.sol"; -import "./lib/MetaVesTControllerTestBase.sol"; +import "./lib/MetaVesTControllerTestBaseExtended.sol"; import "./mocks/MockCondition.sol"; import {ERC1967ProxyLib} from "./lib/ERC1967ProxyLib.sol"; -contract MetaVestControllerTest is MetaVesTControllerTestBase { - using ERC1967ProxyLib for address; +contract MetaVestControllerTest is MetaVesTControllerTestBaseExtended { using MetaVestDealLib for MetaVestDeal; - address authority = guardianSafe; - address dao = guardianSafe; - address grantee = alice; - address transferee = address(0x101); - - // Parameters - uint48 metavestExpiry = uint48(block.timestamp + 1600); // MetaVest expires 1600 seconds later - - function setUp() public override { - MetaVesTControllerTestBase.setUp(); - - vm.startPrank(deployer); - - // Deploy MetaVesT controller - - controller = metavestController(address(new ERC1967Proxy{salt: salt}( - address(new metavestController{salt: salt}()), - abi.encodeWithSelector( - metavestController.initialize.selector, - guardianSafe, - guardianSafe, - address(registry), - address(metavestControllerFactory) - ) - ))); - - vm.stopPrank(); - - // Prepare funds (vesting token) - vestingToken.mint( - address(guardianSafe), - 9999 ether - ); - vm.prank(address(guardianSafe)); - vestingToken.approve(address(controller), 9999 ether); - - // Prepare funds (payment token) - paymentToken.mint( - address(guardianSafe), - 9999 ether - ); - vm.prank(address(guardianSafe)); - paymentToken.approve(address(controller), 9999 ether); - - vm.startPrank(guardianSafe); - controller.createSet("testSet"); - vm.stopPrank(); - - // Guardian SAFE to delegate signing to an EOA - vm.prank(guardianSafe); - registry.setDelegation(delegate, block.timestamp + 365 days * 3); // This is a hack. One should not delegate signing for this long - assertTrue(registry.isValidDelegate(guardianSafe, delegate), "delegate should be Guardian SAFE's delegate"); - } - function test_RevertIf_InitializeImplementation() public { metavestController controllerImpl = new metavestController(); vm.expectRevert(Initializable.InvalidInitialization.selector); @@ -581,7 +527,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.prank(grantee); RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); - console.log(vestingToken.balanceOf(restrictedTokenAward)); + console2.log(vestingToken.balanceOf(restrictedTokenAward)); assertEq(paymentToken.balanceOf(grantee), startingPaymentTokenBalance + payment); } @@ -615,354 +561,6 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { assertEq(controller.dao(), newDao); } - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocation() internal returns (address) { - return createDummyVestingAllocation(""); // Expect no reverts - } - - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocation(bytes memory expectRevertData) internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); - milestones[0] = BaseAllocation.Milestone({ - milestoneAward: 1000 ether, - unlockOnCompletion: true, - complete: false, - conditionContracts: new address[](0) - }); - - // Guardians to sign agreements and register on MetaVesTController - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones - ), - "Alice", - metavestExpiry - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice", - expectRevertData - ); - } - - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocationNoUnlock() internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); - milestones[0] = BaseAllocation.Milestone({ - milestoneAward: 1000 ether, - unlockOnCompletion: false, - complete: false, - conditionContracts: new address[](0) - }); - - // Guardians to sign agreements and register on MetaVesTController - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones - ), - "Alice", - metavestExpiry - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - ); - } - - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocationSlowUnlock() internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); - milestones[0] = BaseAllocation.Milestone({ - milestoneAward: 1000 ether, - unlockOnCompletion: true, - complete: false, - conditionContracts: new address[](0) - }); - - // Guardians to sign agreements and register on MetaVesTController - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 100 ether, - unlockingCliffCredit: 100 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 5 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones - ), - "Alice", - metavestExpiry - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - ); - } - - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocationLarge() internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); - - // Guardians to sign agreements and register on MetaVesTController - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 0 ether, - unlockingCliffCredit: 0 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp) - }), - milestones - ), - "Alice", - metavestExpiry - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - ); - } - - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocationLargeFuture() internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); - - // Guardians to sign agreements and register on MetaVesTController - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000 ether, - vestingCliffCredit: 0 ether, - unlockingCliffCredit: 0 ether, - vestingRate: 10 ether, - vestingStartTime: uint48(block.timestamp + 2000), - unlockRate: 10 ether, - unlockStartTime: uint48(block.timestamp + 2000) - }), - milestones - ), - "Alice", - metavestExpiry - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - ); - } - - function createDummyTokenOptionAllocation() internal returns (address) { - return createDummyTokenOptionAllocation(""); // Expect no reverts - } - - function createDummyTokenOptionAllocation(bytes memory expectRevertData) internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); - milestones[0] = BaseAllocation.Milestone({ - milestoneAward: 1000e18, - unlockOnCompletion: true, - complete: false, - conditionContracts: new address[](0) - }); - - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setTokenOption( - alice, - address(paymentToken), - 5e17, // exercisePrice - 1 days, // shortStopDuration - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000e18, - vestingCliffCredit: 100e18, - unlockingCliffCredit: 100e18, - vestingRate: 10e18, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10e18, - unlockStartTime: uint48(block.timestamp) - }), - milestones - ), - "Alice", - metavestExpiry, - "" - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - ); - } - - function createDummyRestrictedTokenAward() internal returns (address) { - return createDummyRestrictedTokenAward(alice, ""); - } - - function createDummyRestrictedTokenAward(address recipient) internal returns (address) { - return createDummyRestrictedTokenAward(recipient, ""); - } - - function createDummyRestrictedTokenAward(address recipient, bytes memory expectRevertData) internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); - milestones[0] = BaseAllocation.Milestone({ - milestoneAward: 1000e18, - unlockOnCompletion: true, - complete: false, - conditionContracts: new address[](0) - }); - - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setRestrictedToken( - alice, - address(paymentToken), - 1e18, // exercisePrice - 1 days, // shortStopDuration - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000e18, - vestingCliffCredit: 100e18, - unlockingCliffCredit: 100e18, - vestingRate: 10e18, - vestingStartTime: uint48(block.timestamp), - unlockRate: 10e18, - unlockStartTime: uint48(block.timestamp) - }), - milestones - ), - "Alice", - metavestExpiry, - "" - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - recipient, - alicePrivateKey, - "Alice" - ); - } - - function createDummyRestrictedTokenAwardFuture() internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); - milestones[0] = BaseAllocation.Milestone({ - milestoneAward: 1000e18, - unlockOnCompletion: true, - complete: false, - conditionContracts: new address[](0) - }); - - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setRestrictedToken( - alice, - address(paymentToken), - 1e18, // exercisePrice - 1 days, // shortStopDuration - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 1000e18, - vestingCliffCredit: 100e18, - unlockingCliffCredit: 100e18, - vestingRate: 10e18, - vestingStartTime: uint48(block.timestamp + 1000), - unlockRate: 10e18, - unlockStartTime: uint48(block.timestamp + 1000) - }), - milestones - ), - "Alice", - metavestExpiry, - "" - ); - - return _granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - ); - } - function testWithdrawFromController() public { uint256 amount = 100e18; vm.startPrank(authority); @@ -1265,7 +863,7 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.stopPrank(); vm.prank(grantee); RestrictedTokenAward(restrictedTokenAward).claimRepurchasedTokens(); - console.log(vestingToken.balanceOf(restrictedTokenAward)); + console2.log(vestingToken.balanceOf(restrictedTokenAward)); } function testZeroReclaimVesting() public { @@ -1584,276 +1182,4 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVestController_DuplicateCondition.selector)); controller.updateFunctionCondition(address(condition), functionSig); } - - function test_RevertIf_GranteeNotDirectParty() public { - // Proposal should fail if the grantee is not listed as a direct party (non-delegate). - // This is to prevent accidentally signing an agreement for other's grant - address[] memory parties = new address[](2); - parties[0] = authority; - parties[1] = bob; // not Alice the grantee - - _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - parties, - MetaVestDealLib.draft().setVesting( - grantee, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0) - ), - "Alice", - block.timestamp + 7 days, - abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_GranteeNotDirectParty.selector) // Expected revert - ); - } - - function test_RevertIf_IncorrectGrantorSignature() public { - // Should not be able to propose a deal without grantor's authorization - _proposeAndSignDeal( - templateId, - block.timestamp, // salt - alicePrivateKey, // Should fail because Alice is not delegated by the grantor - MetaVestDealLib.draft().setVesting( - alice, // grantee - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0) - ), - "Alice", - block.timestamp + 7 days, - abi.encodeWithSelector(CyberAgreementRegistry.SignatureVerificationFailed.selector) // Expected revert - ); - } - - function test_RevertIf_IncorrectGranteeSignature() public { - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0) - ), - "Alice", - block.timestamp + 7 days - ); - - // Should not be able to sign Alice's agreement with other's signature - _granteeSignDeal( - contractIdAlice, - alice, - alice, - bobPrivateKey, // Wrong signer - "Alice", - abi.encodeWithSelector(CyberAgreementRegistry.SignatureVerificationFailed.selector) // Expected revert - ); - } - - function test_GranteeDelegateSignature() public { - // Alice to delegate to Bob - vm.prank(alice); - registry.setDelegation(bob, block.timestamp + 60); - assertTrue(registry.isValidDelegate(alice, bob), "Bob should be Alice's delegate"); - - // Bob should be able to sign for Alice now - bytes32 contractId = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0) - ), - "Alice", - metavestExpiry - ); - VestingAllocation vestingAllocation = VestingAllocation(_granteeSignDeal( - contractId, - alice, - alice, - bobPrivateKey, // Use Bob to sign - "Alice" - )); - assertEq(vestingAllocation.grantee(), alice, "Alice should be the grantee"); - - // Wait until expiry - skip(61); - - // Bob should no longer be able to sign for Alice - assertFalse(registry.isValidDelegate(alice, bob), "Bob should no longer be Alice's delegate"); - } - - function test_GranteeSignedExternally() public { - // It should still be able to create metavest if the grantee has signed externally by interacting directly with - // CyberAgreementRegistry - - bytes32 contractId = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - tokenStreamTotal: 100 ether, - vestingCliffCredit: 10 ether, - unlockingCliffCredit: 10 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0) - ), - "Alice", - metavestExpiry - ); - - // Alice to sign the agreement externally - - MetaVestDeal memory deal = controller.getDeal(contractId); - - string[] memory globalValues = new string[](11); - globalValues[0] = vm.toString(uint256(MetaVestType.Vesting)); - globalValues[1] = vm.toString(address(guardianSafe)); // grantor - globalValues[2] = vm.toString(grantee); // grantee - globalValues[3] = vm.toString(deal.allocation.tokenContract); // tokenContract - globalValues[4] = vm.toString(deal.allocation.tokenStreamTotal / 1 ether); //tokenStreamTotal (human-readable) - globalValues[5] = vm.toString(deal.allocation.vestingCliffCredit / 1 ether); // vestingCliffCredit (human-readable) - globalValues[6] = vm.toString(deal.allocation.unlockingCliffCredit / 1 ether); // unlockingCliffCredit (human-readable) - globalValues[7] = vm.toString(deal.allocation.vestingRate * 365 days / 1 ether); // vestingRate (annually) (human-readable) - globalValues[8] = vm.toString(deal.allocation.vestingStartTime); // vestingStartTime - globalValues[9] = vm.toString(deal.allocation.unlockRate * 365 days / 1 ether); // unlockRate (annually) (human-readable) - globalValues[10] = vm.toString(deal.allocation.unlockStartTime); // unlockStartTime - - string[] memory partyValues = new string[](4); - partyValues[0] = "Alice"; - partyValues[1] = vm.toString(grantee); // evmAddress - partyValues[2] = "email@company.com"; // Make sure it matches the proposed deal - partyValues[3] = "individual"; // Make sure it matches the proposed deal - - registry.signContractFor( - alice, - contractId, - partyValues, - CyberAgreementUtils.signAgreementTypedData( - vm, - registry.DOMAIN_SEPARATOR(), - registry.SIGNATUREDATA_TYPEHASH(), - contractId, - agreementUri, - globalFields, - partyFields, - globalValues, - partyValues, - alicePrivateKey - ), - false, // fillUnallocated - "" // secret - ); - assertTrue(registry.hasSigned(contractId, alice), "Alice should've signed"); - - // Should still be able to create metavest for Alice - - VestingAllocation metavest = VestingAllocation(controller.signDealAndCreateMetavest( - alice, - alice, - contractId, - partyValues, - "", // signature no longer needed since Alice has signed externally - "" // no secrets - )); - assertEq(metavest.grantee(), alice, "Alice should be the grantee"); - } - - function test_UpgradeMetaVesTController() public { - // MetaLeX to release new implementation - vm.startPrank(deployer); - address newImplementation = address(new metavestController()); - metavestControllerFactory.setRefImplementation(newImplementation); - vm.stopPrank(); - - // Upgrade to new implementation without initialization data - - // Non-owner should not be able to upgrade it - vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); - controller.upgradeToAndCall(newImplementation, ""); - - // Owner should be able to upgrade it - vm.prank(guardianSafe); - controller.upgradeToAndCall(newImplementation, ""); - assertEq(address(controller).getErc1967Implementation(), newImplementation); - - // Verify the controller still works - - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - MetaVestDealLib.draft().setVesting( - alice, - BaseAllocation.Allocation({ - tokenContract: address(vestingToken), - // 100k ZK total, the first half unlocks with a cliff and the second half unlocks over an year - tokenStreamTotal: 60 ether, - vestingCliffCredit: 30 ether, - unlockingCliffCredit: 30 ether, - vestingRate: 1 ether, - vestingStartTime: uint48(block.timestamp), - unlockRate: 1 ether, - unlockStartTime: uint48(block.timestamp) - }), - new BaseAllocation.Milestone[](0) - ), - "Alice", - metavestExpiry - ); - - VestingAllocation vestingAllocationAlice = VestingAllocation(_granteeSignDeal( - contractIdAlice, - alice, // grantee - alice, // recipient - alicePrivateKey, - "Alice" - )); - assertEq(vestingAllocationAlice.grantee(), alice); - } } diff --git a/test/controller2.t.sol b/test/controller2.t.sol new file mode 100644 index 0000000..d76d825 --- /dev/null +++ b/test/controller2.t.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.20; + +import {console2} from "forge-std/console2.sol"; +import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import "../src/RestrictedTokenAllocation.sol"; +import "../src/TokenOptionAllocation.sol"; +import "../src/VestingAllocation.sol"; +import "../src/interfaces/IAllocationFactory.sol"; +import "./lib/MetaVesTControllerTestBaseExtended.sol"; +import "./mocks/MockCondition.sol"; +import {ERC1967ProxyLib} from "./lib/ERC1967ProxyLib.sol"; + +contract MetaVestControllerTest2 is MetaVesTControllerTestBaseExtended { + using ERC1967ProxyLib for address; + using MetaVestDealLib for MetaVestDeal; + + function test_RevertIf_GranteeNotDirectParty() public { + // Proposal should fail if the grantee is not listed as a direct party (non-delegate). + // This is to prevent accidentally signing an agreement for other's grant + address[] memory parties = new address[](2); + parties[0] = authority; + parties[1] = bob; // not Alice the grantee + + _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + parties, + MetaVestDealLib.draft().setVesting( + grantee, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + block.timestamp + 7 days, + abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_GranteeNotDirectParty.selector) // Expected revert + ); + } + + function test_RevertIf_IncorrectGrantorSignature() public { + // Should not be able to propose a deal without grantor's authorization + _proposeAndSignDeal( + templateId, + block.timestamp, // salt + alicePrivateKey, // Should fail because Alice is not delegated by the grantor + MetaVestDealLib.draft().setVesting( + alice, // grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + block.timestamp + 7 days, + abi.encodeWithSelector(CyberAgreementRegistry.SignatureVerificationFailed.selector) // Expected revert + ); + } + + function test_RevertIf_IncorrectGranteeSignature() public { + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + block.timestamp + 7 days + ); + + // Should not be able to sign Alice's agreement with other's signature + _granteeSignDeal( + contractIdAlice, + alice, + alice, + bobPrivateKey, // Wrong signer + "Alice", + abi.encodeWithSelector(CyberAgreementRegistry.SignatureVerificationFailed.selector) // Expected revert + ); + } + + function test_GranteeDelegateSignature() public { + // Alice to delegate to Bob + vm.prank(alice); + registry.setDelegation(bob, block.timestamp + 60); + assertTrue(registry.isValidDelegate(alice, bob), "Bob should be Alice's delegate"); + + // Bob should be able to sign for Alice now + bytes32 contractId = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + metavestExpiry + ); + VestingAllocation vestingAllocation = VestingAllocation(_granteeSignDeal( + contractId, + alice, + alice, + bobPrivateKey, // Use Bob to sign + "Alice" + )); + assertEq(vestingAllocation.grantee(), alice, "Alice should be the grantee"); + + // Wait until expiry + skip(61); + + // Bob should no longer be able to sign for Alice + assertFalse(registry.isValidDelegate(alice, bob), "Bob should no longer be Alice's delegate"); + } + + function test_GranteeSignedExternally() public { + // It should still be able to create metavest if the grantee has signed externally by interacting directly with + // CyberAgreementRegistry + + bytes32 contractId = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 100 ether, + vestingCliffCredit: 10 ether, + unlockingCliffCredit: 10 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + metavestExpiry + ); + + // Alice to sign the agreement externally + + MetaVestDeal memory deal = controller.getDeal(contractId); + + string[] memory globalValues = new string[](11); + globalValues[0] = vm.toString(uint256(MetaVestType.Vesting)); + globalValues[1] = vm.toString(address(guardianSafe)); // grantor + globalValues[2] = vm.toString(grantee); // grantee + globalValues[3] = vm.toString(deal.allocation.tokenContract); // tokenContract + globalValues[4] = vm.toString(deal.allocation.tokenStreamTotal / 1 ether); //tokenStreamTotal (human-readable) + globalValues[5] = vm.toString(deal.allocation.vestingCliffCredit / 1 ether); // vestingCliffCredit (human-readable) + globalValues[6] = vm.toString(deal.allocation.unlockingCliffCredit / 1 ether); // unlockingCliffCredit (human-readable) + globalValues[7] = vm.toString(deal.allocation.vestingRate * 365 days / 1 ether); // vestingRate (annually) (human-readable) + globalValues[8] = vm.toString(deal.allocation.vestingStartTime); // vestingStartTime + globalValues[9] = vm.toString(deal.allocation.unlockRate * 365 days / 1 ether); // unlockRate (annually) (human-readable) + globalValues[10] = vm.toString(deal.allocation.unlockStartTime); // unlockStartTime + + string[] memory partyValues = new string[](4); + partyValues[0] = "Alice"; + partyValues[1] = vm.toString(grantee); // evmAddress + partyValues[2] = "email@company.com"; // Make sure it matches the proposed deal + partyValues[3] = "individual"; // Make sure it matches the proposed deal + + registry.signContractFor( + alice, + contractId, + partyValues, + CyberAgreementUtils.signAgreementTypedData( + vm, + registry.DOMAIN_SEPARATOR(), + registry.SIGNATUREDATA_TYPEHASH(), + contractId, + agreementUri, + globalFields, + partyFields, + globalValues, + partyValues, + alicePrivateKey + ), + false, // fillUnallocated + "" // secret + ); + assertTrue(registry.hasSigned(contractId, alice), "Alice should've signed"); + + // Should still be able to create metavest for Alice + + VestingAllocation metavest = VestingAllocation(controller.signDealAndCreateMetavest( + alice, + alice, + contractId, + partyValues, + "", // signature no longer needed since Alice has signed externally + "" // no secrets + )); + assertEq(metavest.grantee(), alice, "Alice should be the grantee"); + } + + function test_UpgradeMetaVesTController() public { + // MetaLeX to release new implementation + vm.startPrank(deployer); + address newImplementation = address(new metavestController()); + metavestControllerFactory.setRefImplementation(newImplementation); + vm.stopPrank(); + + // Upgrade to new implementation without initialization data + + // Non-owner should not be able to upgrade it + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); + controller.upgradeToAndCall(newImplementation, ""); + + // Owner should be able to upgrade it + vm.prank(guardianSafe); + controller.upgradeToAndCall(newImplementation, ""); + assertEq(address(controller).getErc1967Implementation(), newImplementation); + + // Verify the controller still works + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + // 100k ZK total, the first half unlocks with a cliff and the second half unlocks over an year + tokenStreamTotal: 60 ether, + vestingCliffCredit: 30 ether, + unlockingCliffCredit: 30 ether, + vestingRate: 1 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 1 ether, + unlockStartTime: uint48(block.timestamp) + }), + new BaseAllocation.Milestone[](0) + ), + "Alice", + metavestExpiry + ); + + VestingAllocation vestingAllocationAlice = VestingAllocation(_granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + )); + assertEq(vestingAllocationAlice.grantee(), alice); + } +} diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index e24252d..08f3cb5 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.24; -import "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import "../../src/MetaVesTController.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {BorgAuth} from "cybercorps-contracts/src/libs/auth.sol"; @@ -14,8 +14,8 @@ import {MetaVestDealLib, MetaVestDeal} from "../../src/lib/MetaVestDealLib.sol"; contract MetaVesTControllerTestBase is Test { using MetaVestDealLib for MetaVestDeal; - MockERC20 vestingToken = new MockERC20("Vesting Token", "VEST", 18); - MockERC20 paymentToken = new MockERC20("Payment Token", "PAY", 18); + MockERC20 vestingToken; + MockERC20 paymentToken; address deployer = address(0x2); address guardianSafe = address(0x3); @@ -44,6 +44,11 @@ contract MetaVesTControllerTestBase is Test { metavestController controller; function setUp() public virtual { + vestingToken = new MockERC20("Vesting Token", "VEST", 18); + paymentToken = new MockERC20("Payment Token", "PAY", 18); + vm.label(address(vestingToken), "VEST"); + vm.label(address(paymentToken), "PAY"); + vm.startPrank(deployer); // Deploy CyberAgreementRegistry and prepare templates diff --git a/test/lib/MetaVesTControllerTestBaseExtended.sol b/test/lib/MetaVesTControllerTestBaseExtended.sol new file mode 100644 index 0000000..d83dcfe --- /dev/null +++ b/test/lib/MetaVesTControllerTestBaseExtended.sol @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.20; + +import {console2} from "forge-std/console2.sol"; +import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import "../../src/RestrictedTokenAllocation.sol"; +import "../../src/TokenOptionAllocation.sol"; +import "../../src/VestingAllocation.sol"; +import "../../src/interfaces/IAllocationFactory.sol"; +import "./MetaVesTControllerTestBase.sol"; +import "../mocks/MockCondition.sol"; +import {ERC1967ProxyLib} from "./ERC1967ProxyLib.sol"; + +contract MetaVesTControllerTestBaseExtended is MetaVesTControllerTestBase { + using ERC1967ProxyLib for address; + using MetaVestDealLib for MetaVestDeal; + + address authority = guardianSafe; + address dao = guardianSafe; + address grantee = alice; + address transferee = address(0x101); + + // Parameters + uint48 metavestExpiry = uint48(block.timestamp + 1600); // MetaVest expires 1600 seconds later + + function setUp() public override { + MetaVesTControllerTestBase.setUp(); + + vm.startPrank(deployer); + + // Deploy MetaVesT controller + + controller = metavestController(address(new ERC1967Proxy{salt: salt}( + address(new metavestController{salt: salt}()), + abi.encodeWithSelector( + metavestController.initialize.selector, + guardianSafe, + guardianSafe, + address(registry), + address(metavestControllerFactory) + ) + ))); + + vm.stopPrank(); + + // Prepare funds (vesting token) + vestingToken.mint( + address(guardianSafe), + 9999 ether + ); + vm.prank(address(guardianSafe)); + vestingToken.approve(address(controller), 9999 ether); + + // Prepare funds (payment token) + paymentToken.mint( + address(guardianSafe), + 9999 ether + ); + vm.prank(address(guardianSafe)); + paymentToken.approve(address(controller), 9999 ether); + + vm.startPrank(guardianSafe); + controller.createSet("testSet"); + vm.stopPrank(); + + // Guardian SAFE to delegate signing to an EOA + vm.prank(guardianSafe); + registry.setDelegation(delegate, block.timestamp + 365 days * 3); // This is a hack. One should not delegate signing for this long + assertTrue(registry.isValidDelegate(guardianSafe, delegate), "delegate should be Guardian SAFE's delegate"); + } + + // Helper functions to create dummy allocations for testing + function createDummyVestingAllocation() internal returns (address) { + return createDummyVestingAllocation(""); // Expect no reverts + } + + // Helper functions to create dummy allocations for testing + function createDummyVestingAllocation(bytes memory expectRevertData) internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000 ether, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + // Guardians to sign agreements and register on MetaVesTController + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice", + expectRevertData + ); + } + + // Helper functions to create dummy allocations for testing + function createDummyVestingAllocationNoUnlock() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000 ether, + unlockOnCompletion: false, + complete: false, + conditionContracts: new address[](0) + }); + + // Guardians to sign agreements and register on MetaVesTController + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + // Helper functions to create dummy allocations for testing + function createDummyVestingAllocationSlowUnlock() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000 ether, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + // Guardians to sign agreements and register on MetaVesTController + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 100 ether, + unlockingCliffCredit: 100 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 5 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + // Helper functions to create dummy allocations for testing + function createDummyVestingAllocationLarge() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); + + // Guardians to sign agreements and register on MetaVesTController + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 0 ether, + unlockingCliffCredit: 0 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + // Helper functions to create dummy allocations for testing + function createDummyVestingAllocationLargeFuture() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); + + // Guardians to sign agreements and register on MetaVesTController + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setVesting( + alice, // = grantee + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000 ether, + vestingCliffCredit: 0 ether, + unlockingCliffCredit: 0 ether, + vestingRate: 10 ether, + vestingStartTime: uint48(block.timestamp + 2000), + unlockRate: 10 ether, + unlockStartTime: uint48(block.timestamp + 2000) + }), + milestones + ), + "Alice", + metavestExpiry + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + function createDummyTokenOptionAllocation() internal returns (address) { + return createDummyTokenOptionAllocation(""); // Expect no reverts + } + + function createDummyTokenOptionAllocation(bytes memory expectRevertData) internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setTokenOption( + alice, + address(paymentToken), + 5e17, // exercisePrice + 1 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry, + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } + + function createDummyRestrictedTokenAward() internal returns (address) { + return createDummyRestrictedTokenAward(alice, ""); + } + + function createDummyRestrictedTokenAward(address recipient) internal returns (address) { + return createDummyRestrictedTokenAward(recipient, ""); + } + + function createDummyRestrictedTokenAward(address recipient, bytes memory expectRevertData) internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setRestrictedToken( + alice, + address(paymentToken), + 1e18, // exercisePrice + 1 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp) + }), + milestones + ), + "Alice", + metavestExpiry, + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + recipient, + alicePrivateKey, + "Alice" + ); + } + + function createDummyRestrictedTokenAwardFuture() internal returns (address) { + BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1); + milestones[0] = BaseAllocation.Milestone({ + milestoneAward: 1000e18, + unlockOnCompletion: true, + complete: false, + conditionContracts: new address[](0) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // salt + delegatePrivateKey, + MetaVestDealLib.draft().setRestrictedToken( + alice, + address(paymentToken), + 1e18, // exercisePrice + 1 days, // shortStopDuration + BaseAllocation.Allocation({ + tokenContract: address(vestingToken), + tokenStreamTotal: 1000e18, + vestingCliffCredit: 100e18, + unlockingCliffCredit: 100e18, + vestingRate: 10e18, + vestingStartTime: uint48(block.timestamp + 1000), + unlockRate: 10e18, + unlockStartTime: uint48(block.timestamp + 1000) + }), + milestones + ), + "Alice", + metavestExpiry, + "" + ); + + return _granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + ); + } +} From df2fcde5d0e77bfe9a2da6cbb294ee5bc371c1ae Mon Sep 17 00:00:00 2001 From: detoo Date: Fri, 12 Dec 2025 14:41:31 -0800 Subject: [PATCH 25/25] feat: MetavestControllerFactory to provide address pre-compute. More tests --- scripts/deployYearnBorgCompensation.s.sol | 2 +- src/MetaVesTControllerFactory.sol | 23 +++ test/MetaVesTControllerFactory.t.sol | 143 +++++++++++++++++- test/amendement.t.sol | 16 +- test/lib/MetaVesTControllerTestBase.sol | 3 +- .../MetaVesTControllerTestBaseExtended.sol | 16 +- 6 files changed, 178 insertions(+), 25 deletions(-) diff --git a/scripts/deployYearnBorgCompensation.s.sol b/scripts/deployYearnBorgCompensation.s.sol index 92586ea..da71571 100644 --- a/scripts/deployYearnBorgCompensation.s.sol +++ b/scripts/deployYearnBorgCompensation.s.sol @@ -54,7 +54,7 @@ contract DeployYearnBorgCompensationScript is SafeTxHelper, Script { vm.startBroadcast(deployerPrivateKey); // Deploy MetaVesT Controller - + // TODO fixme: next time use MetavestControllerFactory to deploy the controller metavestController controller = metavestController(address(new ERC1967Proxy{salt: salt}( address(new metavestController{salt: salt}()), abi.encodeWithSelector( diff --git a/src/MetaVesTControllerFactory.sol b/src/MetaVesTControllerFactory.sol index 2335c6d..8b33134 100644 --- a/src/MetaVesTControllerFactory.sol +++ b/src/MetaVesTControllerFactory.sol @@ -13,6 +13,7 @@ import {MetaVesTControllerFactoryStorage} from "./storage/MetaVesTControllerFact import {BorgAuthACL} from "cybercorps-contracts/src/libs/auth.sol"; import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Create2} from "openzeppelin-contracts/utils/Create2.sol"; /** * @title MetaVesT Controller Factory @@ -100,6 +101,28 @@ contract MetaVesTControllerFactory is BorgAuthACL, UUPSUpgradeable { emit RefImplementationSet(newImplementation, metavestController(newImplementation).DEPLOY_VERSION()); } + /// @notice Computes the deterministic address for an MetavestController + /// @param salt Salt used for CREATE2 + /// @return computedAddress The precomputed address of the proxy + function computeMetavestControllerAddress(bytes32 salt, address authority, address dao) external view returns (address) { + return Create2.computeAddress( + salt, + keccak256(abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode( + MetaVesTControllerFactoryStorage.getStorageData().refImplementation, + abi.encodeWithSelector( + metavestController.initialize.selector, + authority, + dao, + MetaVesTControllerFactoryStorage.getStorageData().registry, + address(this) + ) + ) + )) + ); + } + // ======================== // UUPSUpgradeable // ======================== diff --git a/test/MetaVesTControllerFactory.t.sol b/test/MetaVesTControllerFactory.t.sol index 35e45e6..2f90538 100644 --- a/test/MetaVesTControllerFactory.t.sol +++ b/test/MetaVesTControllerFactory.t.sol @@ -1,11 +1,76 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.24; -import {MetaVesTControllerFactory} from "../src/MetaVesTControllerFactory.sol"; import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {Initializable} from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import {BorgAuth} from "cybercorps-contracts/src/libs/auth.sol"; +import {CyberAgreementRegistry} from "cybercorps-contracts/src/CyberAgreementRegistry.sol"; +import {MetaVesTControllerFactory} from "../src/MetaVesTControllerFactory.sol"; +import {metavestController} from "../src/MetaVesTController.sol"; +import {ERC1967ProxyLib} from "./lib/ERC1967ProxyLib.sol"; + +contract MockRefImplementation is UUPSUpgradeable { + string public constant DEPLOY_VERSION = "test"; + + // UUPS upgrade authorization + function _authorizeUpgrade( + address newImplementation + ) internal override {} +} contract MetaVesTControllerFactoryTest is Test { + using ERC1967ProxyLib for address; + + bytes32 salt = keccak256("MetaVesTControllerFactoryTest"); + + BorgAuth auth; + CyberAgreementRegistry registry; + MetaVesTControllerFactory metavestControllerFactory; + address refImplAddr; + + uint256 deployerPrivateKey; + address deployer; + uint256 alicePrivateKey; + address alice; + uint256 bobPrivateKey; + address bob; + + function setUp() public { + (deployer, deployerPrivateKey) = makeAddrAndKey("deployer"); + (alice, alicePrivateKey) = makeAddrAndKey("alice"); + (bob, bobPrivateKey) = makeAddrAndKey("bob"); + + auth = new BorgAuth{salt: salt}(deployer); + registry = CyberAgreementRegistry(address(new ERC1967Proxy{salt: salt}( + address(new CyberAgreementRegistry{salt: salt}()), + abi.encodeWithSelector( + CyberAgreementRegistry.initialize.selector, + address(auth) + ) + ))); + + // create2 all the way down so the outcome is consistent + refImplAddr = address(new metavestController{salt: salt}()); + metavestControllerFactory = MetaVesTControllerFactory(address(new ERC1967Proxy{salt: salt}( + address(new MetaVesTControllerFactory{salt: salt}()), + abi.encodeWithSelector( + MetaVesTControllerFactory.initialize.selector, + address(auth), + address(registry), + refImplAddr + ) + ))); + } + + function test_sanityCheck() public { + assertEq(address(metavestControllerFactory.AUTH()), address(auth), "unexpected auth"); + assertEq(metavestControllerFactory.getRegistry(), address(registry), "unexpected registry"); + // Make sure reference implementation address is also create2() + assertEq(metavestControllerFactory.getRefImplementation(), refImplAddr, "unexpected reference implementation"); + } + function test_RevertIf_InitializeImplementation() public { MetaVesTControllerFactory impl = new MetaVesTControllerFactory(); vm.expectRevert(Initializable.InvalidInitialization.selector); @@ -15,4 +80,80 @@ contract MetaVesTControllerFactoryTest is Test { address(0) // no-op ); } + + /// @notice Should be able to deploy a MetavestController with correct parameters + function test_deployMetavestController() public { + vm.expectEmit(true, true, true, true, address(metavestControllerFactory)); + emit MetaVesTControllerFactory.MetaVesTControllerDeployed( + metavestControllerFactory.computeMetavestControllerAddress(salt, alice, bob), + alice, + bob + ); + metavestController controller = metavestController(metavestControllerFactory.deployMetavestController( + salt, + alice, // authority + bob // dao + )); + assertEq(controller.authority(), alice, "unexpected authority"); + assertEq(controller.dao(), bob, "unexpected dao"); + assertEq(controller.registry(), address(registry), "unexpected registry"); + assertEq(controller.upgradeFactory(), address(metavestControllerFactory), "unexpected factory"); + } + + /// @notice Should be able to deterministically compute the MetevestController address + function test_computeMetavestControllerAddress() public { + assertEq( + metavestControllerFactory.computeMetavestControllerAddress(salt, alice, bob), + metavestControllerFactory.deployMetavestController(salt, alice, bob), + "unexpected deployed address" + ); + } + + function test_setRegistry() public { + address newRegistry = address(123); // no-op + vm.expectEmit(true, true, true, true, address(metavestControllerFactory)); + emit MetaVesTControllerFactory.RegistrySet(newRegistry); + vm.prank(deployer); + metavestControllerFactory.setRegistry(newRegistry); + assertEq(metavestControllerFactory.getRegistry(), newRegistry, "unexpected registry"); + } + + function test_RevertIf_setRegistryNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, auth.OWNER_ROLE(), alice)); + vm.prank(alice); + metavestControllerFactory.setRegistry(address(123)); + } + + function test_setRefImplementation() public { + address newRefImplementation = address(new MockRefImplementation()); + vm.expectEmit(true, true, true, true, address(metavestControllerFactory)); + emit MetaVesTControllerFactory.RefImplementationSet(newRefImplementation, "test"); + vm.prank(deployer); + metavestControllerFactory.setRefImplementation(newRefImplementation); + assertEq(metavestControllerFactory.getRefImplementation(), newRefImplementation, "unexpected reference implementation"); + } + + function test_RevertIf_setRefImplementationNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, auth.OWNER_ROLE(), alice)); + vm.prank(alice); + metavestControllerFactory.setRefImplementation(address(123)); + } + + function test_Upgrade() public { + address newImpl = address(new MetaVesTControllerFactory()); + vm.startPrank(deployer); + metavestControllerFactory.upgradeToAndCall(newImpl, ""); + vm.stopPrank(); + assertEq( + address(metavestControllerFactory).getErc1967Implementation(), + newImpl, + "unexpected implementation" + ); + } + + function test_RevertIf_UpgradeFactoryNonOwner() public { + vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, auth.OWNER_ROLE(), alice)); + vm.prank(alice); + metavestControllerFactory.upgradeToAndCall(address(123), ""); + } } diff --git a/test/amendement.t.sol b/test/amendement.t.sol index c8d1f24..e9d38b2 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -28,17 +28,11 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.startPrank(deployer); // Deploy MetaVesT controller - - controller = metavestController(address(new ERC1967Proxy{salt: salt}( - address(new metavestController{salt: salt}()), - abi.encodeWithSelector( - metavestController.initialize.selector, - guardianSafe, - guardianSafe, - address(registry), - address(metavestControllerFactory) - ) - ))); + controller = metavestController(metavestControllerFactory.deployMetavestController( + salt, + guardianSafe, + guardianSafe + )); vm.stopPrank(); diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 08f3cb5..a19d852 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -89,13 +89,14 @@ contract MetaVesTControllerTestBase is Test { partyFields ); + // create2 all the way down so the outcome is consistent metavestControllerFactory = MetaVesTControllerFactory(address(new ERC1967Proxy{salt: salt}( address(new MetaVesTControllerFactory{salt: salt}()), abi.encodeWithSelector( MetaVesTControllerFactory.initialize.selector, address(auth), address(registry), - new metavestController() + new metavestController{salt: salt}() ) ))); diff --git a/test/lib/MetaVesTControllerTestBaseExtended.sol b/test/lib/MetaVesTControllerTestBaseExtended.sol index d83dcfe..db5b086 100644 --- a/test/lib/MetaVesTControllerTestBaseExtended.sol +++ b/test/lib/MetaVesTControllerTestBaseExtended.sol @@ -29,17 +29,11 @@ contract MetaVesTControllerTestBaseExtended is MetaVesTControllerTestBase { vm.startPrank(deployer); // Deploy MetaVesT controller - - controller = metavestController(address(new ERC1967Proxy{salt: salt}( - address(new metavestController{salt: salt}()), - abi.encodeWithSelector( - metavestController.initialize.selector, - guardianSafe, - guardianSafe, - address(registry), - address(metavestControllerFactory) - ) - ))); + controller = metavestController(metavestControllerFactory.deployMetavestController( + salt, + guardianSafe, + guardianSafe + )); vm.stopPrank();