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/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 ``` 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/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 7935855..da71571 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)); @@ -55,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( @@ -63,7 +62,7 @@ contract DeployYearnBorgCompensationScript is SafeTxHelper, Script { address(config.borgSafe), address(config.borgSafe), address(config.registry), - address(config.vestingAllocationFactory) + 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 eaf4327..299b4df 100644 --- a/scripts/lib/YearnBorgCompensation2025_2026.sol +++ b/scripts/lib/YearnBorgCompensation2025_2026.sol @@ -6,8 +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 {metavestController} from "../../src/MetaVesTController.sol"; +import {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; library YearnBorgCompensation2025_2026 { @@ -27,7 +27,7 @@ library YearnBorgCompensation2025_2026 { IGnosisSafe metalexSafe; CyberAgreementRegistry registry; - VestingAllocationFactory vestingAllocationFactory; + MetaVesTControllerFactory metavestControllerFactory; metavestController controller; // Yearn BORG Director Compensation Agreement (one template per director for now) @@ -81,7 +81,7 @@ library YearnBorgCompensation2025_2026 { metalexSafe: metalexSafe, registry: CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134), - vestingAllocationFactory: VestingAllocationFactory(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 b8de1d6..0508a5a 100644 --- a/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol +++ b/scripts/lib/YearnBorgCompensationSepolia2025_2026.sol @@ -6,7 +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 {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; import {metavestController} from "../../src/MetaVesTController.sol"; import {YearnBorgCompensation2025_2026} from "./YearnBorgCompensation2025_2026.sol"; @@ -35,7 +35,7 @@ library YearnBorgCompensationSepolia2025_2026 { metalexSafe: metalexSafe, registry: CyberAgreementRegistry(0xa9E808B8eCBB60Bb19abF026B5b863215BC4c134), - vestingAllocationFactory: VestingAllocationFactory(0x87dC5e3FBFE8B5F2B74C64eE34da8bdc9fedCb0f), + 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 7ab3e0d..6ab5b84 100644 --- a/scripts/proposeMetavestDeal.s.sol +++ b/scripts/proposeMetavestDeal.s.sol @@ -10,11 +10,12 @@ 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"; 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. @@ -67,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(""); @@ -104,10 +105,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/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/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 6c83a09..85af111 100644 --- a/src/MetaVesTController.sol +++ b/src/MetaVesTController.sol @@ -10,8 +10,10 @@ 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 {IMetaVesTControllerFactory} from "./interfaces/IMetaVesTControllerFactory.sol"; import "./BaseAllocation.sol"; -//import "./RestrictedTokenAllocation.sol"; import "./interfaces/IAllocationFactory.sol"; import "./interfaces/IPriceAllocation.sol"; import "./lib/EnumberableSet.sol"; @@ -26,195 +28,41 @@ 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; - /// @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; - } - - struct DealData { - bytes32 agreementId; - metavestType _metavestType; - address grantee; - 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; - - /// @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 => DealData) 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 - /// - - 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 - /// - - 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() { - address[] memory conditions = functionToConditions[msg.sig]; - for (uint256 i; i < conditions.length; ++i) { - if (!IConditionM(conditions[i]).checkCondition(address(this), msg.sig, "")) { - revert MetaVesTController_ConditionNotSatisfied(conditions[i]); - } - } + MetaVesTControllerStorage.conditionCheck(msg.sig); _; } modifier consentCheck(address _grant, bytes calldata _data) { - if (isMetavestInSet(_grant)) { - bytes32 set = getSetOfMetavest(_grant); - MajorityAmendmentProposal storage proposal = 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 { - AmendmentProposal storage proposal = functionToGranteeToAmendmentPending[msg.sig][_grant]; - if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { - revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); - } - } + MetaVesTControllerStorage.consentCheck(msg.sig, _grant, _data); _; } modifier onlyAuthority() { - if (msg.sender != authority) revert MetaVesTController_OnlyAuthority(); + if (msg.sender != MetaVesTControllerStorage.getStorage().authority) revert MetaVesTControllerStorage.MetaVesTController_OnlyAuthority(); _; } modifier onlyDao() { - if (msg.sender != dao) revert MetaVesTController_OnlyDAO(); + if (msg.sender != MetaVesTControllerStorage.getStorage().dao) revert MetaVesTControllerStorage.MetaVesTController_OnlyDAO(); _; } + /// @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'. @@ -222,32 +70,31 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { address _authority, address _dao, address _registry, - address _vestingFactory -// address _tokenOptionFactory, -// address _restrictedTokenFactory + address upgradeFactory ) public initializer { __UUPSUpgradeable_init(); - if (_authority == address(0)) revert MetaVesTController_ZeroAddress(); - authority = _authority; - registry = _registry; - vestingFactory = _vestingFactory; -// tokenOptionFactory = _tokenOptionFactory; -// restrictedTokenFactory = _restrictedTokenFactory; - dao = _dao; + if (_authority == address(0)) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); + + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + st.authority = _authority; + st.registry = _registry; + 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 /// @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) - revert MetaVesTController_NoPendingAmendment(_msgSig, _grant); - address grantee =BaseAllocation(_grant).grantee(); - if(msg.sender!= grantee) revert MetaVesTController_OnlyGranteeMayCall(); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (!st.functionToGranteeToAmendmentPending[_msgSig][_grant].isPending) + revert MetaVesTControllerStorage.MetaVesTController_NoPendingAmendment(_msgSig, _grant); + address grantee = BaseAllocation(_grant).grantee(); + if (msg.sender != grantee) revert MetaVesTControllerStorage.MetaVesTController_OnlyGranteeMayCall(); - functionToGranteeToAmendmentPending[_msgSig][_grant].inFavor = _inFavor; - emit MetaVesTController_AmendmentConsentUpdated(_msgSig, msg.sender, _inFavor); + st.functionToGranteeToAmendmentPending[_msgSig][_grant].inFavor = _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.) @@ -255,18 +102,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 MetaVesTControllerStorage.MetaVestController_DuplicateCondition(); } - functionToConditions[_functionSig].push(_condition); - emit MetaVesTController_ConditionUpdated(_condition, _functionSig); + st.functionToConditions[_functionSig].push(_condition); + emit MetaVesTControllerStorage.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]; @@ -274,17 +123,14 @@ 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 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, @@ -292,68 +138,19 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes32 secretHash, uint256 expiry ) external returns (bytes32) { - - // Call internal function to avoid stack-too-deep errors - bytes32 agreementId = _createAgreement( + MetaVestDeal memory dealProposed = MetaVesTControllerStorage.proposeAndSignDeal( templateId, salt, + dealDraft, globalValues, parties, partyValues, - secretHash, - expiry - ); - - if (partyValues.length < 2) revert MetaVesTController_CounterPartyNotFound(); - if (partyValues[1].length != partyValues[0].length) revert MetaVesTController_PartyValuesLengthMismatch(); - counterPartyValues[agreementId] = partyValues[1]; - - ICyberAgreementRegistry(registry).signContractFor( - authority, // First party (grantor) should always be the authority - 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, - allocation: allocation, - milestones: milestones, - metavest: address(0) // Not deployed yet - }); - dealIds.push(agreementId); - - emit MetaVesTController_DealProposed( - agreementId, grantee, _metavestType, allocation, 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 dealProposed.agreementId; } function signDealAndCreateMetavest( @@ -363,217 +160,38 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { string[] memory partyValues, bytes memory signature, string memory secret - ) external conditionCheck returns (address) { - // Finalize agreement - - if(ICyberAgreementRegistry(registry).isVoided(agreementId)) revert MetaVesTController_DealVoided(); - if(ICyberAgreementRegistry(registry).isFinalized(agreementId)) revert MetaVesTController_DealAlreadyFinalized(); - - string[] storage counterPartyCheck = counterPartyValues[agreementId]; - if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) revert MetaVesTController_CounterPartyValueMismatch(); - - ICyberAgreementRegistry(registry).signContractFor(grantee, agreementId, partyValues, signature, false, secret); - - ICyberAgreementRegistry(registry).finalizeContract(agreementId); - - // Create and provision MetaVesT + ) external conditionCheck() returns (address) { + // Interaction: finalize the deal and create metavest contract + (address newMetavest, uint256 total) = MetaVesTControllerStorage.signDealAndCreateMetavest( + grantee, + recipient, + agreementId, + partyValues, + signature, + secret + ); - address newMetavest = _createMetavest(agreementId, recipient); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); - emit MetaVesTController_DealFinalizedAndMetaVestCreated(agreementId, recipient, newMetavest); + // Interaction: transfer tokens to escrow + safeTransferFrom(st.deals[agreementId].allocation.tokenContract, st.authority, newMetavest, total); + emit MetaVesTControllerStorage.MetaVesTController_DealFinalizedAndMetaVestCreated(agreementId, recipient, newMetavest); return newMetavest; } - function _createMetavest(bytes32 agreementId, address recipient) internal returns (address) { - DealData storage deal = deals[agreementId]; - - if(deal._metavestType == metavestType.Vesting) - { - deal.metavest = createVestingAllocation(deal.grantee, recipient, deal.allocation, deal.milestones); - } - else if(deal._metavestType == metavestType.TokenOption) - { - // TODO will be supported in the next stage - revert MetaVesTController_TypeNotSupported(deal._metavestType); - } - else if(deal._metavestType == metavestType.RestrictedTokenAward) - { - // TODO will be supported in the next stage - revert MetaVesTController_TypeNotSupported(deal._metavestType); - } - else - { - revert MetaVesTController_IncorrectMetaVesTType(); - } - metavestAgreementIds[deal.metavest] = agreementId; - - return deal.metavest; - } - - - 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) - 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 { - if ( - IERC20M(tokenContract).allowance(authority, address(this)) < total || - IERC20M(tokenContract).balanceOf(authority) < total - ) 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; - if (_total == 0) revert MetaVesTController_ZeroAmount(); - validateTokenApprovalAndBalance(_allocation.tokenContract, _total); - - address vestingAllocation = IAllocationFactory(vestingFactory).createAllocation( - IAllocationFactory.AllocationType.Vesting, - _grantee, - _recipient, - address(this), - _allocation, - _milestones, - address(0), - 0, - 0 - ); - safeTransferFrom(_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 getMetaVestType(address _grant) public view returns (uint256) { + function getMetaVestType(address _grant) public view returns (MetaVestType) { return BaseAllocation(_grant).getVestingType(); } /// @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(); + if (_balance == 0) revert MetaVesTControllerStorage.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 @@ -587,19 +205,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); -// } + /// @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 MetaVesTControllerStorage.MetaVesTController_ZeroPrice(); + IPriceAllocation grant = IPriceAllocation(_grant); + // 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); + } /// @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 @@ -610,7 +229,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); } @@ -620,13 +239,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 > 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 || 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); @@ -654,7 +273,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); } @@ -699,35 +318,37 @@ 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(); - _pendingAuthority = _newAuthority; + if (_newAuthority == address(0)) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); + 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; - emit MetaVesTController_AuthorityUpdated(msg.sender); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (msg.sender != st._pendingAuthority) revert MetaVesTControllerStorage.MetaVesTController_OnlyPendingAuthority(); + delete st._pendingAuthority; + st.authority = 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()' /// @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(); - _pendingDao = _newDao; + if (_newDao == address(0)) revert MetaVesTControllerStorage.MetaVesTController_ZeroAddress(); + 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; - emit MetaVesTController_DaoUpdated(msg.sender); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if (msg.sender != st._pendingDao) revert MetaVesTControllerStorage.MetaVesTController_OnlyPendingDao(); + delete st._pendingDao; + st.dao = msg.sender; + emit MetaVesTControllerStorage.MetaVesTController_DaoUpdated(msg.sender); } /// @notice for 'authority' to propose a metavest detail amendment @@ -738,13 +359,14 @@ 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 - ); - emit MetaVesTController_AmendmentProposed(_grant, _msgSig); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + //override existing amendment if it exists + st.functionToGranteeToAmendmentPending[_msgSig][_grant] = MetaVesTControllerStorage.AmendmentProposal( + true, + keccak256(_callData), + false + ); + emit MetaVesTControllerStorage.MetaVesTController_AmendmentProposed(_grant, _msgSig); } /// @notice for 'authority' to propose a metavest detail amendment @@ -756,124 +378,103 @@ contract metavestController is UUPSUpgradeable, SafeTransferLib { bytes4 _msgSig, bytes calldata _callData ) external onlyAuthority { - 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]) - revert MetaVesTController_AmendmentAlreadyPending(); - - MajorityAmendmentProposal storage proposal = 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; - for (uint256 i; i < sets[nameHash].length(); ++i) { - uint256 _votingPower = BaseAllocation(sets[nameHash].at(i)).getMajorityVotingPower(); - totalVotingPower += _votingPower; - proposal.voterPower[sets[nameHash].at(i)] = _votingPower; - } - proposal.totalVotingPower = totalVotingPower; - - setMajorityVoteActive[nameHash] = true; - emit MetaVesTController_MajorityAmendmentProposed(setName, _msgSig, _callData, totalVotingPower); + MetaVesTControllerStorage.proposeMajorityMetavestAmendment(setName, _msgSig, _callData); } /// @notice for 'authority' to cancel a metavest majority amendment /// @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 { - if(!doesSetExist(_setName)) revert MetaVesTController_SetDoesNotExist(); + MetaVesTControllerStorage.MetaVesTControllerData storage st = MetaVesTControllerStorage.getStorage(); + if(!MetaVesTControllerStorage.doesSetExist(_setName)) revert MetaVesTControllerStorage.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 + MetaVesTControllerStorage.AMENDMENT_TIME_LIMIT) revert MetaVesTControllerStorage.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(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(); - 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. 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) { 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 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 + MetaVesTControllerStorage.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 (bytes(_name).length > 512) revert MetaVesTController_StringTooLong(); - - setNames.add(nameHash); - emit MetaVesTController_SetCreated(_name); + 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 MetaVesTControllerStorage.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 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 = 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); - emit MetaVesTController_SetRemoved(_name); - } - - function doesSetExist(string memory _name) internal view returns (bool) { - return setNames.contains(keccak256(bytes(_name))); + st.setNames.remove(nameHash); + emit MetaVesTControllerStorage.MetaVesTController_SetRemoved(_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; } } @@ -882,14 +483,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; } } @@ -897,43 +499,94 @@ 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 (isMetavestInSet(_metaVest)) revert MetaVesTController_MetaVesTAlreadyExists(); - if (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(); - sets[nameHash].add(_metaVest); - emit MetaVesTController_AddressAddedToSet(_name, _metaVest); + st.sets[nameHash].add(_metaVest); + emit MetaVesTControllerStorage.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 MetaVesTControllerStorage.MetaVesTController_SetDoesNotExist(); + if (st.setMajorityVoteActive[nameHash]) revert MetaVesTControllerStorage.MetaVesTController_AmendmentAlreadyPending(); + if (!st.sets[nameHash].contains(_metaVest)) revert MetaVesTControllerStorage.MetaVestController_MetaVestNotInSet(); - sets[nameHash].remove(_metaVest); - emit MetaVesTController_AddressRemovedFromSet(_name, _metaVest); + st.sets[nameHash].remove(_metaVest); + emit MetaVesTControllerStorage.MetaVesTController_AddressRemovedFromSet(_name, _metaVest); } - function getDeal(bytes32 agreementId) public view returns (DealData memory) { - return deals[agreementId]; + function getDeal(bytes32 agreementId) public view returns (MetaVestDeal memory) { + 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 upgradeFactory() external view returns (address) { + return MetaVesTControllerStorage.getStorage().upgradeFactory; + } + + 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 + ); + } + + // ======================== + // 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 {} - - // Avoid "Address: low-level delegate call failed" due to `UUPSUpgradeable.upgradeToAndCall()` runs with `forceCall=true` - fallback() external {} + ) internal override onlyAuthority { + if (IMetaVesTControllerFactory(MetaVesTControllerStorage.getStorage().upgradeFactory).getRefImplementation() != newImplementation) { + revert MetaVesTControllerStorage.NotRefImplementation(); + } + } } diff --git a/src/MetaVesTControllerFactory.sol b/src/MetaVesTControllerFactory.sol new file mode 100644 index 0000000..8b33134 --- /dev/null +++ b/src/MetaVesTControllerFactory.sol @@ -0,0 +1,131 @@ +//SPDX-License-Identifier: AGPL-3.0-only + +/* +************************************ + MetaVesTFactory + ************************************ + */ + +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"; +import {Create2} from "openzeppelin-contracts/utils/Create2.sol"; + +/** + * @title MetaVesT Controller Factory + * + * @notice Deploy a new instance of MetaVesTController, which in turn deploys a new MetaVesT it controls + * + * + */ +contract MetaVesTControllerFactory is BorgAuthACL, UUPSUpgradeable { + event RegistrySet(address registry); + event RefImplementationSet(address refImplementation, string version); + event MetaVesTControllerDeployed( + address controller, + address authority, + address dao + ); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + 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()); + } + + /// @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 + // ======================== + + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {} +} diff --git a/src/RestrictedTokenAllocation.sol b/src/RestrictedTokenAllocation.sol new file mode 100644 index 0000000..e866ef0 --- /dev/null +++ b/src/RestrictedTokenAllocation.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: AGPL-3.0-only +import "./BaseAllocation.sol"; +import {MetaVestType} from "./lib/MetaVestDealLib.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 _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 + /// @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, + 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(); + if (_allocation.vestingRate < 100 || _allocation.unlockRate < 100) revert MetaVesT_RateTooLow(); + + // set vesting allocation variables + allocation = _allocation; + + // 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 + /// @return MetaVestType + function getVestingType() external pure override returns (MetaVestType) { + return MetaVestType.RestrictedTokenAward; + } + + /// @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/TokenOptionAllocation.sol b/src/TokenOptionAllocation.sol new file mode 100644 index 0000000..b0495ed --- /dev/null +++ b/src/TokenOptionAllocation.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: AGPL-3.0-only +import "./BaseAllocation.sol"; +import {MetaVestType} from "./lib/MetaVestDealLib.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 _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 + /// @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, + 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(); + if (_allocation.vestingRate < 100 || _allocation.unlockRate < 100) revert MetaVesT_RateTooLow(); + + // set vesting allocation variables + allocation = _allocation; + + // 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 vesting type + /// @return MetaVestType + function getVestingType() external pure override returns (MetaVestType) { + return MetaVestType.TokenOption; + } + + /// @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, grantee, getAuthority(), paymentAmount); + tokensExercised += _tokensToExercise; + emit MetaVesT_TokenOptionExercised(grantee, _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/VestingAllocation.sol b/src/VestingAllocation.sol index 9d8726e..302dcd1 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; @@ -22,33 +23,25 @@ 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]); } } - /// @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/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/lib/MetaVestDealLib.sol b/src/lib/MetaVestDealLib.sol new file mode 100644 index 0000000..d484df2 --- /dev/null +++ b/src/lib/MetaVestDealLib.sol @@ -0,0 +1,122 @@ +/* .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; + 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; + } + + /// @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.RestrictedTokenAward; + deal.grantee = grantee; + deal.paymentToken = paymentToken; + deal.exercisePrice = exercisePrice; + deal.shortStopDuration = shortStopDuration; + deal.allocation = allocation; + deal.milestones = milestones; + return deal; + } +} 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/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 new file mode 100644 index 0000000..e5277f3 --- /dev/null +++ b/src/storage/MetaVesTControllerStorage.sol @@ -0,0 +1,447 @@ + +/* .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, IConditionM, IERC20M} from "../BaseAllocation.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 { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; + + // 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 { + 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 upgradeFactory; + 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; + } + + /// + /// 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 + /// + + 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(); // TODO review needed: should we move it elsewhere? + 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_GranteeNotDirectParty(); + 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; + assembly { + st.slot := position + } + } + + 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) { + 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) + .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); + + emit MetaVesTController_DealProposed( + dealDraft.agreementId, dealDraft.grantee, dealDraft.metavestType, dealDraft.allocation, dealDraft.milestones, + secretHash > 0, + st.registry + ); + + return dealDraft; + } + + function signDealAndCreateMetavest( + address grantee, + address recipient, + bytes32 agreementId, + string[] memory partyValues, + bytes memory signature, + string memory secret + ) 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)) { + revert MetaVesTController_DealVoided(); + } + if (ICyberAgreementRegistry(st.registry).isFinalized(agreementId)) { + revert MetaVesTController_DealAlreadyFinalized(); + } + + string[] storage counterPartyCheck = st.counterPartyValues[agreementId]; + if (keccak256(abi.encode(counterPartyCheck)) != keccak256(abi.encode(partyValues))) { + revert MetaVesTController_CounterPartyValueMismatch(); + } + + 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) + ) { + revert MetaVesTController_ZeroAddress(); + } + if ( + deal.allocation.vestingCliffCredit > deal.allocation.tokenStreamTotal + || deal.allocation.unlockingCliffCredit > deal.allocation.tokenStreamTotal + ) { + revert MetaVesTController_CliffGreaterThanTotal(); + } + uint256 milestoneTotal = 0; + if (deal.milestones.length != 0) { + if (deal.milestones.length > ARRAY_LENGTH_LIMIT) { + revert MetaVesTController_LengthMismatch(); + } + for (uint256 i; i < deal.milestones.length; ++i) { + if (deal.milestones[i].conditionContracts.length > ARRAY_LENGTH_LIMIT) { + revert MetaVesTController_LengthMismatch(); + } + if (deal.milestones[i].milestoneAward == 0) { + revert MetaVesTController_ZeroAmount(); + } + milestoneTotal += deal.milestones[i].milestoneAward; + } + } + total = deal.allocation.tokenStreamTotal + milestoneTotal; + if (total == 0) { + revert MetaVesTController_ZeroAmount(); + } + if ( + IERC20M(deal.allocation.tokenContract).allowance(st.authority, address(this)) < total + || IERC20M(deal.allocation.tokenContract).balanceOf(st.authority) < total + ) { + revert MetaVesTController_AmountNotApprovedForTransferFrom(); + } + + // Interaction: Finalize agreement + + // 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))) { + revert MetaVesTController_CounterPartyValueMismatch(); + } + } + ICyberAgreementRegistry(st.registry).finalizeContract(agreementId); + + // Interaction: Create and provision MetaVesT + if (deal.metavestType == MetaVestType.Vesting) { + deal.metavest = VestingAllocationFactory.createVestingAllocation(deal, recipient); + } else if (deal.metavestType == MetaVestType.TokenOption) { + deal.metavest = TokenOptionFactory.createTokenOptionAllocation(deal, recipient); + } else if (deal.metavestType == MetaVestType.RestrictedTokenAward) { + deal.metavest = RestrictedTokenFactory.createRestrictedTokenAward(deal, recipient); + } else { + revert MetaVesTController_IncorrectMetaVesTType(); + } + st.metavestAgreementIds[deal.metavest] = agreementId; + + return (deal.metavest, total); + } + + 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)); + //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:]); + proposal.time = block.timestamp; + 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; + proposal.voterPower[st.sets[nameHash].at(i)] = _votingPower; + } + proposal.totalVotingPower = totalVotingPower; + + st.setMajorityVoteActive[nameHash] = true; + + emit MetaVesTController_MajorityAmendmentProposed(setName, _msgSig, _callData, totalVotingPower); + } + + 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, "")) { + revert MetaVesTController_ConditionNotSatisfied(conditions[i]); + } + } + } + + function consentCheck(bytes4 msgSig, address _grant, bytes calldata _data) external { + MetaVesTControllerData storage st = getStorage(); + if (isMetavestInSet(_grant)) { + bytes32 set = getSetOfMetavest(_grant); + MajorityAmendmentProposal storage proposal = + st.functionToSetMajorityProposal[msgSig][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 { + AmendmentProposal storage proposal = + st.functionToGranteeToAmendmentPending[msgSig][_grant]; + if (!proposal.inFavor || proposal.dataHash != keccak256(_data)) { + revert MetaVesTController_AmendmentNeitherMutualNorMajorityConsented(); + } + } + } + + function isMetavestInSet(address _metavest) internal view returns (bool) { + MetaVesTControllerData storage st = 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) { + MetaVesTControllerData storage st = 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 ""; + } + + function doesSetExist(string memory _name) internal view returns (bool) { + return getStorage().setNames.contains(keccak256(bytes(_name))); + } +} diff --git a/test/AuditBaseA.t.sol b/test/AuditBaseA.t.sol index 1846b52..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 { @@ -30,7 +28,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 +49,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 427dd78..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 { @@ -14,7 +12,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 @@ -31,7 +28,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 +49,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); } @@ -82,51 +79,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 @@ -144,9 +145,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); } @@ -168,29 +169,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/AuditBaseC.t.sol b/test/AuditBaseC.t.sol index 8bb8b9b..35df7a5 100644 --- a/test/AuditBaseC.t.sol +++ b/test/AuditBaseC.t.sol @@ -2,15 +2,14 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "../test/controller.t.sol"; +import "./lib/MetaVesTControllerTestBaseExtended.sol"; -// TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry -contract Audit is MetaVestControllerTest { +contract Audit is MetaVesTControllerTestBaseExtended { 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); @@ -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 418fe93..d53ac3b 100644 --- a/test/AuditBaseC3.t.sol +++ b/test/AuditBaseC3.t.sol @@ -2,15 +2,14 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "../test/controller.t.sol"; +import "./lib/MetaVesTControllerTestBaseExtended.sol"; -// TODO WIP: non-VestingAllocation tests are disabled until reviewed with new design with CyberAgreementRegistry -contract Audit is MetaVestControllerTest { +contract Audit is MetaVesTControllerTestBaseExtended { 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); @@ -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/MetaVesTControllerFactory.t.sol b/test/MetaVesTControllerFactory.t.sol new file mode 100644 index 0000000..2f90538 --- /dev/null +++ b/test/MetaVesTControllerFactory.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +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); + impl.initialize( + address(0), // no-op + address(0), // no-op + 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/YearnBorgCompensation.t.sol b/test/YearnBorgCompensation.t.sol index 3893825..74e09d2 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 48e7d19..e9d38b2 100644 --- a/test/amendement.t.sol +++ b/test/amendement.t.sol @@ -1,24 +1,22 @@ // 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/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 "./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; 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; @@ -30,29 +28,21 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { vm.startPrank(deployer); // Deploy MetaVesT controller - - vestingAllocationFactory = new VestingAllocationFactory(); - - controller = metavestController(address(new ERC1967Proxy{salt: salt}( - address(new metavestController{salt: salt}()), - abi.encodeWithSelector( - metavestController.initialize.selector, - guardianSafe, - guardianSafe, - address(registry), - address(vestingAllocationFactory) - ) - ))); + controller = metavestController(metavestControllerFactory.deployMetavestController( + salt, + guardianSafe, + guardianSafe + )); 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); @@ -75,11 +65,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 { @@ -105,7 +95,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); } @@ -138,64 +128,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); + + 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); -// 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); -// } + 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(); @@ -277,11 +280,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 +317,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 +346,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"); } @@ -374,20 +377,22 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, agreementSaltCounter++, delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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 + metavestExpiry // Same expiry as the minter so grantee can defer vesting contract creation as much as possible ); return _granteeSignDeal( @@ -400,74 +405,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( -// metavestController.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( -// metavestController.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 { @@ -494,11 +522,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 +535,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 +548,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 +561,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,101 +574,92 @@ 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); } -// 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(); @@ -648,159 +667,159 @@ 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); } -// 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(); @@ -922,7 +941,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 a63f233..4de04e4 100644 --- a/test/controller.t.sol +++ b/test/controller.t.sol @@ -1,68 +1,28 @@ // 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 {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/VestingAllocationFactory.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; - - address authority = guardianSafe; - address dao = guardianSafe; - address grantee = alice; - 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 - - function setUp() public override { - MetaVesTControllerTestBase.setUp(); - - vm.startPrank(deployer); - - // Deploy MetaVesT controller - - vestingAllocationFactory = new VestingAllocationFactory(); - - controller = metavestController(address(new ERC1967Proxy{salt: salt}( - address(new metavestController{salt: salt}()), - abi.encodeWithSelector( - metavestController.initialize.selector, - guardianSafe, - guardianSafe, - address(registry), - address(vestingAllocationFactory) - ) - ))); - - vm.stopPrank(); - - // Prepare funds - paymentToken.mint( - address(guardianSafe), - 9999 ether +contract MetaVestControllerTest is MetaVesTControllerTestBaseExtended { + using MetaVestDealLib for MetaVestDeal; + + 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 ); - 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 testCreateVestingAllocation() public { @@ -70,20 +30,22 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { templateId, block.timestamp, // salt delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); VestingAllocation vestingAllocationAlice = VestingAllocation(_granteeSignDeal( @@ -101,78 +63,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( -// metavestController.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 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( -// metavestController.metavestType.RestrictedTokenAward, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 365 days, -// 0 -// -// ); -// -// assertEq(token.balanceOf(restrictedTokenAward), 1100e18); -// //assertEq(controller.restrictedTokenAllocations(grantee, 0), restrictedTokenAward); -// } + 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) + }); + + bytes32 contractIdAlice = _proposeAndSignDeal( + templateId, + block.timestamp, // 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, + "" + ); + + TokenOptionAllocation metavestAlice = TokenOptionAllocation(_granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + )); + + assertEq(vestingToken.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), + 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, + "" + ); + + RestrictedTokenAward metavestAlice = RestrictedTokenAward(_granteeSignDeal( + contractIdAlice, + alice, // grantee + alice, // recipient + alicePrivateKey, + "Alice" + )); + + assertEq(vestingToken.balanceOf(address(metavestAlice)), 1100e18); + } function testUpdateTransferability() public { uint256 startTimestamp = block.timestamp; @@ -248,7 +229,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,27 +258,29 @@ 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); } -// 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(); @@ -328,11 +311,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" ); @@ -394,7 +377,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 +402,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(); @@ -443,20 +426,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(); @@ -466,47 +453,84 @@ 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 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(); + + 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(); + console2.log(vestingToken.balanceOf(restrictedTokenAward)); + assertEq(paymentToken.balanceOf(grantee), startingPaymentTokenBalance + payment); + + } function testTerminateTokensFuture() public { address vestingAllocation = createDummyVestingAllocationLargeFuture(); @@ -537,328 +561,22 @@ 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, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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 - ); - - 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, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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 - ); - - 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, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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", - 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); - // Helper functions to create dummy allocations for testing - function createDummyVestingAllocationLarge() internal returns (address) { - BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); + paymentToken.transfer(address(controller), amount); - // Guardians to sign agreements and register on MetaVesTController - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible - ); + uint256 initialBalance = paymentToken.balanceOf(authority); + controller.withdrawFromController(address(paymentToken)); + uint256 finalBalance = paymentToken.balanceOf(authority); - 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, - alice, // = grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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", - 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 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( -// metavestController.metavestType.TokenOption, -// grantee, -// allocation, -// milestones, -// 5e17, -// address(paymentToken), -// 1 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: 1000e18, -// unlockOnCompletion: true, -// complete: false, -// conditionContracts: new address[](0) -// }); -// -// token.approve(address(controller), 2100e18); -// -// return controller.createMetavest( -// metavestController.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), -// 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( -// metavestController.metavestType.RestrictedTokenAward, -// grantee, -// allocation, -// milestones, -// 1e18, -// address(paymentToken), -// 1 days, -// 0 -// -// ); -// } - - - function testGetMetaVestType() public { - address vestingAllocation = createDummyVestingAllocation(); -// address tokenOptionAllocation = createDummyTokenOptionAllocation(); -// address restrictedTokenAward = createDummyRestrictedTokenAward(); + vm.stopPrank(); - assertEq(controller.getMetaVestType(vestingAllocation), 1); -// assertEq(controller.getMetaVestType(tokenOptionAllocation), 2); -// assertEq(controller.getMetaVestType(restrictedTokenAward), 3); + assertEq(finalBalance - initialBalance, amount); + assertEq(paymentToken.balanceOf(address(controller)), 0); } -// 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 test_RevertIf_CreateMetavestWithZeroAddress() public { BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](0); @@ -867,20 +585,22 @@ 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", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible + metavestExpiry ); _granteeSignDeal( @@ -889,20 +609,20 @@ contract MetaVestControllerTest is MetaVesTControllerTestBase { alice, // recipient alicePrivateKey, "Alice", - abi.encodeWithSelector(metavestController.MetaVesTController_ZeroAddress.selector) + abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_ZeroAddress.selector) ); } 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" @@ -911,12 +631,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); @@ -926,18 +646,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" @@ -946,22 +666,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" @@ -970,165 +690,181 @@ 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 { + 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 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 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 = paymentToken.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 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 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 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(); + console2.log(vestingToken.balanceOf(restrictedTokenAward)); + } function testZeroReclaimVesting() public { address vestingAllocation = createDummyVestingAllocation(); @@ -1210,88 +946,130 @@ 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)); - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); + vm.expectRevert(abi.encodeWithSelector(MetaVesTControllerStorage.MetaVesTController_OnlyAuthority.selector)); controller.initiateAuthorityUpdate(address(0x5678)); } @@ -1300,13 +1078,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 +1093,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 +1118,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 +1159,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,253 +1179,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); } - - // 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( - templateId, - block.timestamp, // salt - alicePrivateKey, // Should fail because Alice is not delegated by the grantor - alice, // grantee - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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, - alice, - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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, - alice, - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - 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", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible - ); - 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"); - } - - // 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(metavestController.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(metavestController.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(metavestController.MetaVesTController_UnauthorizedToMint.selector)); -// controller.mint(alice, 1 ether); -// } - - function test_UpgradeMetaVesTController() public { - // Deploy new implementation - address newImplementation = address(new metavestController()); - - // Upgrade to new implementation without initialization data - - // Non-owner should not be able to upgrade it - vm.expectRevert(abi.encodeWithSelector(metavestController.MetaVesTController_OnlyAuthority.selector)); - controller.upgradeToAndCall(newImplementation, ""); - - // Owner should be able to upgrade it - vm.prank(guardianSafe); - controller.upgradeToAndCall(newImplementation, ""); - assertEq(address(controller).getErc1967Implementation(vm), newImplementation); - - // Verify the controller still works - - bytes32 contractIdAlice = _proposeAndSignDeal( - templateId, - block.timestamp, // salt - delegatePrivateKey, - alice, - BaseAllocation.Allocation({ - tokenContract: address(paymentToken), - // 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", - cappedMinterExpirationTime // Same expiry as the minter so grantee can defer vesting contract creation as much as possible - ); - - 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/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)))); } } diff --git a/test/lib/MetaVesTControllerTestBase.sol b/test/lib/MetaVesTControllerTestBase.sol index 8b07847..a19d852 100644 --- a/test/lib/MetaVesTControllerTestBase.sol +++ b/test/lib/MetaVesTControllerTestBase.sol @@ -1,17 +1,21 @@ // 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 "../../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 {MetaVesTControllerFactory} from "../../src/MetaVesTControllerFactory.sol"; +import {MetaVestDealLib, MetaVestDeal} from "../../src/lib/MetaVestDealLib.sol"; contract MetaVesTControllerTestBase is Test { - MockERC20 paymentToken = new MockERC20("Payment Token", "PAY", 18); + using MetaVestDealLib for MetaVestDeal; + + MockERC20 vestingToken; + MockERC20 paymentToken; address deployer = address(0x2); address guardianSafe = address(0x3); @@ -35,16 +39,20 @@ contract MetaVesTControllerTestBase is Test { BorgAuth auth; CyberAgreementRegistry registry; - VestingAllocationFactory vestingAllocationFactory; + MetaVesTControllerFactory metavestControllerFactory; 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 - // 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}()), @@ -81,59 +89,102 @@ 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{salt: salt}() + ) + ))); + vm.stopPrank(); } 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)"))); } + /// @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 + templateId, + agreementSalt, + grantorOrDelegatePrivateKey, + dealDraft, + partyName, + expiry, + "" ); } + /// @notice Shortcut for: + /// - 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, + 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 + globalValues[0] = vm.toString(uint256(dealDraft.metavestType)); // metavestType 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 @@ -145,13 +196,10 @@ 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; bytes32 expectedContractId = keccak256( abi.encode( templateId, @@ -180,10 +228,7 @@ contract MetaVesTControllerTestBase is Test { bytes32 contractId = controller.proposeAndSignDeal( templateId, agreementSalt, - metavestController.metavestType.Vesting, - grantee, - allocation, - milestones, + dealDraft, globalValues, parties, partyValues, @@ -221,10 +266,10 @@ 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 + 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 diff --git a/test/lib/MetaVesTControllerTestBaseExtended.sol b/test/lib/MetaVesTControllerTestBaseExtended.sol new file mode 100644 index 0000000..db5b086 --- /dev/null +++ b/test/lib/MetaVesTControllerTestBaseExtended.sol @@ -0,0 +1,413 @@ +// 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(metavestControllerFactory.deployMetavestController( + salt, + guardianSafe, + guardianSafe + )); + + 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" + ); + } +}