From 80126061bb6f4a0ba57d4cb93d004b93742ef558 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:34:24 +0200 Subject: [PATCH] [AUTOMATIONS] Vesting Scheduler - add claimable schedule feature (#1944) * added claimable vesting feature * add check on `_executeCliffAndFlow` for claimable schedules * updated time window condition on schedule claim * fix typo * add some unit tests for claiming schedules * increased test coverage * added claimValidityDate feature * updated tests * add claimValidityDate param to createSchedules function * updated unit tests * refactor internal function params (stack too deep) + add claimValidityDate to schedule creation event * removed `isClaimable` boolean from VestingSchedule data structure * remove internal function creating dupplication * updated claim validity date check logic * refactor: re-order the claimValidityDate in the event - keep it as one of the last for backwards compatibility * refactor: rename error CannotClaimFlowOnBehalf to CannotClaimScheduleOnBehalf * fix: remove merge issues from hardhat configs * fix: remove duplication from hardhat config * fix: moved & rename params struct into VestingSchedulerV2 contract --------- Co-authored-by: Kaspar Kallas Co-authored-by: Kaspar Kallas --- .../autowrap/hardhat.config.js | 2 +- .../scheduler/contracts/VestingScheduler.sol | 4 +- .../contracts/VestingSchedulerV2.sol | 513 ++++++++--- .../interface/IVestingSchedulerV2.sol | 108 +++ .../scheduler/hardhat.config.js | 2 +- .../scheduler/test/VestingSchedulerV2.t.sol | 804 ++++++++++++++++-- yarn.lock | 29 +- 7 files changed, 1235 insertions(+), 227 deletions(-) diff --git a/packages/automation-contracts/autowrap/hardhat.config.js b/packages/automation-contracts/autowrap/hardhat.config.js index 6d6e85f978..923ae77b0e 100644 --- a/packages/automation-contracts/autowrap/hardhat.config.js +++ b/packages/automation-contracts/autowrap/hardhat.config.js @@ -53,7 +53,7 @@ module.exports = { process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], - gasPrice: 1000000000 + gasPrice: 1000000000, }, }, namedAccounts: { diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 031e0d4ab2..9f6fd0c6c9 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -18,7 +18,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 public constant START_DATE_VALID_AFTER = 3 days; uint32 public constant END_DATE_VALID_BEFORE = 1 days; - constructor(ISuperfluid host, string memory registrationKey) { + constructor(ISuperfluid host) { cfaV1 = CFAv1Library.InitData( host, IConstantFlowAgreementV1( @@ -37,7 +37,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; - host.registerAppWithKey(configWord, registrationKey); + host.registerApp(configWord); } /// @dev IVestingScheduler.createVestingSchedule implementation. diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol index 072cbe15be..1cfa1a3aa0 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -20,6 +20,54 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { uint32 public constant START_DATE_VALID_AFTER = 3 days; uint32 public constant END_DATE_VALID_BEFORE = 1 days; + /** + * @dev Parameters used to create claimable vesting schedules + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param totalAmount The total amount to be vested + * @param totalDuration The total duration of the vestingß + * @param cliffPeriod The cliff period of the vesting + * @param startDate Timestamp when the vesting should start + * @param claimValidityDate Date before which the claimable schedule must be claimed + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + struct ScheduleCreationFromAmountAndDurationParams { + ISuperToken superToken; + address receiver; + uint256 totalAmount; + uint32 totalDuration; + uint32 cliffPeriod; + uint32 startDate; + uint32 claimValidityDate; + bytes ctx; + } + + /** + * @dev Parameters used to create vesting schedules + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting should start + * @param claimValidityDate Date before which the claimable schedule must be claimed + * @param cliffDate Timestamp of cliff exectution - if 0, startDate acts as cliff + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount to be transferred at the cliff + * @param endDate The timestamp when the stream should stop. + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + struct ScheduleCreationParams { + ISuperToken superToken; + address receiver; + uint32 startDate; + uint32 claimValidityDate; + uint32 cliffDate; + int96 flowRate; + uint256 cliffAmount; + uint32 endDate; + uint256 remainderAmount; + bytes ctx; + } + constructor(ISuperfluid host) { cfaV1 = CFAv1Library.InitData( host, @@ -54,15 +102,18 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { bytes memory ctx ) external returns (bytes memory newCtx) { newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - 0, // remainderAmount - ctx + ScheduleCreationParams( + superToken, + receiver, + startDate, + 0, // claimValidityDate + cliffDate, + flowRate, + cliffAmount, + endDate, + 0, // remainderAmount + ctx + ) ); } @@ -77,79 +128,85 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { uint32 endDate ) external { _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - 0, // remainderAmount - bytes("") + ScheduleCreationParams( + superToken, + receiver, + startDate, + 0, // claimValidityDate + cliffDate, + flowRate, + cliffAmount, + endDate, + 0, // remainderAmount + bytes("") + ) ); } function _createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate, - uint256 remainderAmount, - bytes memory ctx + ScheduleCreationParams memory params ) private returns (bytes memory newCtx) { - newCtx = ctx; - address sender = _getSender(ctx); + newCtx = params.ctx; + address sender = _getSender(params.ctx); // Default to current block timestamp if no start date is provided. - if (startDate == 0) { - startDate = uint32(block.timestamp); + if (params.startDate == 0) { + params.startDate = uint32(block.timestamp); } // Note: Vesting Scheduler V2 doesn't allow start date to be in the past. // V1 did but didn't allow cliff and flow to be in the past though. - if (startDate < block.timestamp) revert TimeWindowInvalid(); + if (params.startDate < block.timestamp) revert TimeWindowInvalid(); + if (params.endDate <= END_DATE_VALID_BEFORE) revert TimeWindowInvalid(); - if (receiver == address(0) || receiver == sender) revert AccountInvalid(); - if (address(superToken) == address(0)) revert ZeroAddress(); - if (flowRate <= 0) revert FlowRateInvalid(); - if (cliffDate != 0 && startDate > cliffDate) revert TimeWindowInvalid(); - if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); - uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + if (params.receiver == address(0) || params.receiver == sender) revert AccountInvalid(); + if (address(params.superToken) == address(0)) revert ZeroAddress(); + if (params.flowRate <= 0) revert FlowRateInvalid(); + if (params.cliffDate != 0 && params.startDate > params.cliffDate) revert TimeWindowInvalid(); + if (params.cliffDate == 0 && params.cliffAmount != 0) revert CliffInvalid(); + + uint32 cliffAndFlowDate = params.cliffDate == 0 ? params.startDate : params.cliffDate; // Note: Vesting Scheduler V2 allows cliff and flow to be in the schedule creation block, V1 didn't. if (cliffAndFlowDate < block.timestamp || - cliffAndFlowDate >= endDate || - cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || - endDate - cliffAndFlowDate < MIN_VESTING_DURATION + cliffAndFlowDate >= params.endDate || + cliffAndFlowDate + START_DATE_VALID_AFTER >= params.endDate - END_DATE_VALID_BEFORE || + params.endDate - cliffAndFlowDate < MIN_VESTING_DURATION ) revert TimeWindowInvalid(); - bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver)); + // NOTE : claimable schedule created with a claim validity date equal to 0 is considered regular schedule + if(params.claimValidityDate != 0) { + if (params.claimValidityDate < cliffAndFlowDate || + params.claimValidityDate > params.endDate - END_DATE_VALID_BEFORE + ) revert TimeWindowInvalid(); + } + + bytes32 hashConfig = keccak256(abi.encodePacked(params.superToken, sender, params.receiver)); if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists(); vestingSchedules[hashConfig] = VestingSchedule( cliffAndFlowDate, - endDate, - flowRate, - cliffAmount, - remainderAmount + params.endDate, + params.claimValidityDate, + params.flowRate, + params.cliffAmount, + params.remainderAmount ); emit VestingScheduleCreated( - superToken, + params.superToken, sender, - receiver, - startDate, - cliffDate, - flowRate, - endDate, - cliffAmount, - remainderAmount + params.receiver, + params.startDate, + params.cliffDate, + params.flowRate, + params.endDate, + params.cliffAmount, + params.claimValidityDate, + params.remainderAmount ); } - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, @@ -160,13 +217,16 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { bytes memory ctx ) external returns (bytes memory newCtx) { newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - ctx + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + 0, // claimValidityDate + ctx + ) ); } @@ -180,13 +240,16 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { uint32 startDate ) external { _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - bytes("") + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + 0, // claimValidityDate + bytes("") + ) ); } @@ -199,13 +262,16 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { uint32 cliffPeriod ) external { _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - 0, // startDate - bytes("") + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + 0, // startDate + 0, // claimValidityDate + bytes("") + ) ); } @@ -217,16 +283,66 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { uint32 totalDuration ) external { _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - bytes("") + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + 0, // claimValidityDate + bytes("") + ) ); } + function _createVestingScheduleFromAmountAndDuration( + ScheduleCreationFromAmountAndDurationParams memory params + ) private returns (bytes memory newCtx) { + // Default to current block timestamp if no start date is provided. + if (params.startDate == 0) { + params.startDate = uint32(block.timestamp); + } + + uint32 endDate = params.startDate + params.totalDuration; + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(params.totalAmount / params.totalDuration)); + uint256 remainderAmount = params.totalAmount - (SafeCast.toUint256(flowRate) * params.totalDuration); + + if (params.cliffPeriod == 0) { + newCtx = _createVestingSchedule( + ScheduleCreationParams( + params.superToken, + params.receiver, + params.startDate, + params.claimValidityDate, + 0 /* cliffDate */, + flowRate, + 0 /* cliffAmount */, + endDate, + remainderAmount, + params.ctx + ) + ); + } else { + uint32 cliffDate = params.startDate + params.cliffPeriod; + uint256 cliffAmount = SafeMath.mul(params.cliffPeriod, SafeCast.toUint256(flowRate)); + newCtx = _createVestingSchedule( + ScheduleCreationParams( + params.superToken, + params.receiver, + params.startDate, + params.claimValidityDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + remainderAmount, + params.ctx + ) + ); + } + } + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. function createAndExecuteVestingScheduleFromAmountAndDuration( ISuperToken superToken, @@ -244,53 +360,6 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { ); } - function _createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) private returns (bytes memory newCtx) { - // Default to current block timestamp if no start date is provided. - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - - uint32 endDate = startDate + totalDuration; - int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); - uint256 remainderAmount = totalAmount - (SafeCast.toUint256(flowRate) * totalDuration); - - if (cliffPeriod == 0) { - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - 0 /* cliffDate */, - flowRate, - 0 /* cliffAmount */, - endDate, - remainderAmount, - ctx - ); - } else { - uint32 cliffDate = startDate + cliffPeriod; - uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - remainderAmount, - ctx - ); - } - } - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. function createAndExecuteVestingScheduleFromAmountAndDuration( ISuperToken superToken, @@ -316,19 +385,175 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { bytes memory ctx ) private returns (bytes memory newCtx) { newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - ctx + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + 0, // claimValidityDate + ctx + ) ); address sender = _getSender(ctx); assert(_executeCliffAndFlow(superToken, sender, receiver)); } + /// @dev IVestingScheduler.createClaimableVestingSchedule implementation. + function createClaimableVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 claimValidityDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingSchedule( + ScheduleCreationParams( + superToken, + receiver, + startDate, + claimValidityDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + 0 /* remainderAmount */, + ctx + ) + ); + } + + /// @dev IVestingScheduler.createClaimableVestingSchedule implementation. + function createClaimableVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 claimValidityDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external { + _createVestingSchedule( + ScheduleCreationParams( + superToken, + receiver, + startDate, + claimValidityDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + 0 /* remainderAmount */, + bytes("") + ) + ); + } + + /// @dev IVestingScheduler.createClaimableVestingScheduleFromAmountAndDuration implementation. + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + claimValidityDate, + ctx + ) + ); + } + + /// @dev IVestingScheduler.createClaimableVestingScheduleFromAmountAndDuration implementation. + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate, + uint32 cliffPeriod, + uint32 startDate + ) external { + _createVestingScheduleFromAmountAndDuration( + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + claimValidityDate, + bytes("") + ) + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate, + uint32 cliffPeriod + ) external { + uint32 startDate = uint32(block.timestamp); + + _createVestingScheduleFromAmountAndDuration( + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + claimValidityDate, + bytes("") + ) + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate + ) external { + uint32 startDate = uint32(block.timestamp); + + _createVestingScheduleFromAmountAndDuration( + ScheduleCreationFromAmountAndDurationParams( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + startDate, + claimValidityDate, + bytes("") + ) + ); + } + function updateVestingSchedule( ISuperToken superToken, address receiver, @@ -394,8 +619,20 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); VestingSchedule memory schedule = vestingSchedules[configHash]; + uint32 latestExecutionDate = schedule.claimValidityDate > 0 + ? schedule.claimValidityDate + : schedule.cliffAndFlowDate + START_DATE_VALID_AFTER; + + if (schedule.claimValidityDate > 0) { + // Ensure that the caller is the sender or the receiver if the vesting schedule requires claiming. + if (msg.sender != sender && msg.sender != receiver) { + revert CannotClaimScheduleOnBehalf(); + } + } + + // Ensure that that the claming date is after the cliff/flow date and before the early end of the schedule if (schedule.cliffAndFlowDate > block.timestamp || - schedule.cliffAndFlowDate + START_DATE_VALID_AFTER < block.timestamp + latestExecutionDate < block.timestamp ) revert TimeWindowInvalid(); // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry. diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol index 0840d8f819..64acfdcdc0 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol @@ -15,11 +15,13 @@ interface IVestingSchedulerV2 { error ScheduleAlreadyExists(); error ScheduleDoesNotExist(); error ScheduleNotFlowing(); + error CannotClaimScheduleOnBehalf(); /** * @dev Vesting configuration provided by user. * @param cliffAndFlowDate Date of flow start and cliff execution (if a cliff was specified) * @param endDate End date of the vesting + * @param claimValidityDate Date before which the claimable schedule must be claimed * @param flowRate For the stream * @param cliffAmount Amount to be transferred at the cliff * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" @@ -27,6 +29,7 @@ interface IVestingSchedulerV2 { struct VestingSchedule { uint32 cliffAndFlowDate; uint32 endDate; + uint32 claimValidityDate; int96 flowRate; uint256 cliffAmount; uint256 remainderAmount; // TODO: consider packing @@ -38,6 +41,7 @@ interface IVestingSchedulerV2 { * @param sender Vesting sender * @param receiver Vesting receiver * @param startDate Timestamp when the vesting starts + * @param claimValidityDate Date before which the claimable schedule must be claimed * @param cliffDate Timestamp of the cliff * @param flowRate The flowRate for the stream * @param endDate The timestamp when the stream should stop @@ -53,6 +57,7 @@ interface IVestingSchedulerV2 { int96 flowRate, uint32 endDate, uint256 cliffAmount, + uint32 claimValidityDate, uint256 remainderAmount ); @@ -180,6 +185,109 @@ interface IVestingSchedulerV2 { uint32 totalDuration ) external; + /** + * @dev Creates a new vesting schedule that needs to be claimed by the receiver to start flowing. + * @dev If a non-zero cliffDate is set, the startDate has no effect other than being logged in an event. + * @dev If cliffDate is set to zero, the startDate becomes the cliff (transfer cliffAmount and start stream). + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting should start + * @param claimValidityDate Date before which the claimable schedule must be claimed + * @param cliffDate Timestamp of cliff exectution - if 0, startDate acts as cliff + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount to be transferred at the cliff + * @param endDate The timestamp when the stream should stop. + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createClaimableVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 claimValidityDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createClaimableVestingSchedule overload for more details. + */ + function createClaimableVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 claimValidityDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external; + + /** + * @dev Creates a new vesting schedule that needs to be claimed by the receiver to start flowing. + * @dev The function makes it more intuitive to create a vesting schedule compared to the original function. + * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param totalAmount The total amount to be vested + * @param totalDuration The total duration of the vesting + * @param claimValidityDate Date before which the claimable schedule must be claimed + * @param cliffPeriod The cliff period of the vesting + * @param startDate Timestamp when the vesting should start + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createClaimableVestingScheduleFromAmountAndDuration overload for more details. + */ + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate, + uint32 cliffPeriod, + uint32 startDate + ) external; + + /** + * @dev See IVestingScheduler.createClaimableVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + */ + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate, + uint32 cliffPeriod + ) external; + + /** + * @dev See IVestingScheduler.createClaimableVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + * Cliff period is not applied. + */ + function createClaimableVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 claimValidityDate + ) external; + /** * @dev Event emitted on update of a vesting schedule * @param superToken The superToken to be vested diff --git a/packages/automation-contracts/scheduler/hardhat.config.js b/packages/automation-contracts/scheduler/hardhat.config.js index 441c84d1c5..257a3a6133 100644 --- a/packages/automation-contracts/scheduler/hardhat.config.js +++ b/packages/automation-contracts/scheduler/hardhat.config.js @@ -52,7 +52,7 @@ module.exports = { process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], - gasPrice: 1000000000 + gasPrice: 1000000000, }, }, namedAccounts: { diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol index 695479cd6c..9bbc39f86a 100644 --- a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -25,6 +25,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { int96 flowRate, uint32 endDate, uint256 cliffAmount, + uint32 claimValidityDate, uint256 remainderAmount ); @@ -79,6 +80,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { uint32 immutable CLIFF_DATE = uint32(BLOCK_TIMESTAMP + 10 days); int96 constant FLOW_RATE = 1000000000; uint256 constant CLIFF_TRANSFER_AMOUNT = 1 ether; + uint32 immutable CLAIM_VALIDITY_DATE = uint32(BLOCK_TIMESTAMP + 15 days); uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); bytes constant EMPTY_CTX = ""; uint256 internal _expectedTotalSupply = 0; @@ -137,13 +139,28 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { vm.stopPrank(); } + function _createClaimableVestingScheduleWithDefaultData(address sender, address receiver) private { + vm.startPrank(sender); + vestingScheduler.createClaimableVestingSchedule( + superToken, + receiver, + START_DATE, + CLAIM_VALIDITY_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); + } + /// TESTS function testCreateVestingSchedule() public { vm.expectEmit(true, true, true, true); emit VestingScheduleCreated( - superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 - ); + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0, 0); _createVestingScheduleWithDefaultData(alice, bob); vm.startPrank(alice); //assert storage data @@ -324,8 +341,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); vm.expectEmit(true, true, true, true); emit VestingScheduleCreated( - superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 - ); + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0, 0); _createVestingScheduleWithDefaultData(alice, bob); vm.prank(alice); superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); @@ -342,6 +358,30 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { assertTrue(schedule.endDate == END_DATE + 1000 , "schedule.endDate"); } + function test_updateVestingSchedule_invalidEndDate() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0, 0); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + vm.startPrank(alice); + + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, uint32(initialTimestamp - 1), EMPTY_CTX); + + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, uint32(initialTimestamp), EMPTY_CTX); + + + } + function testCannotUpdateVestingScheduleIfNotRunning() public { _createVestingScheduleWithDefaultData(alice, bob); vm.startPrank(alice); @@ -602,7 +642,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { uint32 startAndCliffDate = uint32(block.timestamp); vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); + emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0, 0); vestingScheduler.createVestingSchedule( superToken, @@ -664,7 +704,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - vm.expectRevert(); + vm.expectRevert(IVestingSchedulerV2.FlowRateInvalid.selector); vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, @@ -688,7 +728,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { ); console.log("Revert with overflow."); - vm.expectRevert(); // todo: the right error + vm.expectRevert("SafeCast: value doesn't fit in 96 bits"); vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, @@ -738,7 +778,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { uint32 expectedEndDate = startDate + vestingDuration; vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); + emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0, 0); vm.startPrank(alice); bool useCtx = randomizer % 2 == 0; @@ -783,7 +823,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { uint32 expectedEndDate = startDate + vestingDuration; vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); + emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0, 0); vm.startPrank(alice); bool useCtx = randomizer % 2 == 0; @@ -868,7 +908,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { _arrangeAllowances(alice, expectedSchedule.flowRate); vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, expectedSchedule.remainderAmount); + emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, 0, expectedSchedule.remainderAmount); // Act vm.startPrank(alice); @@ -949,62 +989,712 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { assertEq(afterSenderBalance, superToken.balanceOf(alice), "After the schedule has ended, the sender's balance should never change."); } - function _getExpectedSchedule( - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate - ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { - if (startDate == 0) { - startDate = uint32(block.timestamp); - } + // Claimable Vesting Schedules tests - uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + function test_createClaimableVestingSchedule() public { - expectedSchedule = IVestingSchedulerV2.VestingSchedule({ - cliffAndFlowDate: cliffAndFlowDate, - flowRate: flowRate, - cliffAmount: cliffAmount, - endDate: endDate, - remainderAmount: 0 - }); + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, CLAIM_VALIDITY_DATE, 0); + + vm.startPrank(alice); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + CLAIM_VALIDITY_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); + + vm.startPrank(alice); + //assert storage data + VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == CLIFF_DATE , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE , "schedule.endDate"); + assertTrue(schedule.flowRate == FLOW_RATE , "schedule.flowRate"); + assertTrue(schedule.claimValidityDate == CLAIM_VALIDITY_DATE, "schedule.claimValidityDate"); + assertTrue(schedule.cliffAmount == CLIFF_TRANSFER_AMOUNT , "schedule.cliffAmount"); } - function _getExpectedScheduleFromAmountAndDuration( - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { - if (startDate == 0) { - startDate = uint32(block.timestamp); - } + function test_createClaimableVestingSchedule_claimValidity() public { + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, CLAIM_VALIDITY_DATE, 0); - int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + vm.startPrank(alice); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + CLAIM_VALIDITY_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); - uint32 cliffDate; - uint32 cliffAndFlowDate; - uint256 cliffAmount; - if (cliffPeriod > 0) { - cliffDate = startDate + cliffPeriod; - cliffAmount = cliffPeriod * SafeCast.toUint256(flowRate); - cliffAndFlowDate = cliffDate; - } else { - cliffDate = 0; - cliffAmount = 0; - cliffAndFlowDate = startDate; - } + vm.startPrank(alice); + //assert storage data + VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == CLIFF_DATE , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE , "schedule.endDate"); + assertTrue(schedule.flowRate == FLOW_RATE , "schedule.flowRate"); + assertTrue(schedule.claimValidityDate == CLAIM_VALIDITY_DATE, "schedule.claimValidityDate"); + assertTrue(schedule.cliffAmount == CLIFF_TRANSFER_AMOUNT , "schedule.cliffAmount"); + } - uint32 endDate = startDate + totalDuration; + function test_createClaimableVestingSchedule_noCtx() public { + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, CLAIM_VALIDITY_DATE, 0); - uint256 remainderAmount = totalAmount - SafeCast.toUint256(flowRate) * totalDuration; + vm.startPrank(alice); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + CLAIM_VALIDITY_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE + ); + vm.stopPrank(); - expectedSchedule = IVestingSchedulerV2.VestingSchedule({ - cliffAndFlowDate: cliffAndFlowDate, - flowRate: flowRate, - cliffAmount: cliffAmount, - endDate: endDate, + vm.startPrank(alice); + //assert storage data + VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == CLIFF_DATE , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE , "schedule.endDate"); + assertTrue(schedule.flowRate == FLOW_RATE , "schedule.flowRate"); + assertTrue(schedule.claimValidityDate == CLAIM_VALIDITY_DATE, "schedule.flowRate"); + assertTrue(schedule.cliffAmount == CLIFF_TRANSFER_AMOUNT , "schedule.cliffAmount"); + } + + function test_createClaimableVestingSchedule_wrongData() public { + vm.startPrank(alice); + // revert with superToken = 0 + vm.expectRevert(IVestingSchedulerV2.ZeroAddress.selector); + vestingScheduler.createClaimableVestingSchedule( + ISuperToken(address(0)), + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = sender + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + alice, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = address(0) + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + address(0), + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with flowRate = 0 + vm.expectRevert(IVestingSchedulerV2.FlowRateInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + 0, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffDate = 0 but cliffAmount != 0 + vm.expectRevert(IVestingSchedulerV2.CliffInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + 0, + 0, // claimValidityDate + 0, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with startDate < block.timestamp && cliffDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + uint32(block.timestamp - 1), + 0, // claimValidityDate + 0, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with endDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + 0, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate < block.timestamp + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + 0, + 0, // claimValidityDate + uint32(block.timestamp) - 1, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate >= endDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate + startDateValidFor >= endDate - endDateValidBefore + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with startDate > cliffDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + CLIFF_DATE + 1, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + + // revert with vesting duration < 7 days + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE + 2 days, + EMPTY_CTX + ); + + // revert with invalid claim validity date (before schedule/cliff start) + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE - 1, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + } + + function test_createClaimableVestingSchedule_dataExists() public { + vm.startPrank(alice); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); + + vm.expectRevert(IVestingSchedulerV2.ScheduleAlreadyExists.selector); + + vm.startPrank(alice); + vestingScheduler.createClaimableVestingSchedule( + superToken, + bob, + START_DATE, + 0, // claimValidityDate + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); + + } + + function test_createClaimableVestingScheduleFromAmountAndDuration_withoutCliff(uint8 randomizer) public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint32 startDate = uint32(block.timestamp); + uint256 totalVestedAmount = 105_840_000; // a value perfectly divisible by a week + uint32 vestingDuration = 1 weeks; + uint32 claimValidityDate = startDate + 1 days; + int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, claimValidityDate, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + claimValidityDate, + 0, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + claimValidityDate, + 0, + startDate + ); + } + vm.stopPrank(); + } + + function test_createClaimableVestingScheduleFromAmountAndDuration_withoutCliff_noStartDate() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint256 totalVestedAmount = 105_840_000; // a value perfectly divisible by a week + uint32 vestingDuration = 1 weeks; + uint32 claimValidityDate = uint32(block.timestamp) + 2 days; + int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration + uint32 expectedEndDate = uint32(block.timestamp) + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, uint32(block.timestamp), 0, expectedFlowRate, expectedEndDate, 0, claimValidityDate, 0); + + vm.startPrank(alice); + + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + claimValidityDate + ); + vm.stopPrank(); + } + + function test_createClaimableVestingScheduleFromAmountAndDuration_withCliff(uint8 randomizer) public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint32 startDate = uint32(block.timestamp); + uint256 totalVestedAmount = 103_680_000; // a value perfectly divisible + uint32 vestingDuration = 1 weeks + 1 days; + uint32 cliffPeriod = 1 days; + uint32 claimValidityDate = startDate + cliffPeriod + 1 days; + + int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) + uint256 expectedCliffAmount = 12960000; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, startDate + cliffPeriod, expectedFlowRate, startDate + vestingDuration, expectedCliffAmount, claimValidityDate, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + claimValidityDate, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + claimValidityDate, + cliffPeriod, + startDate + ); + } + vm.stopPrank(); + } + + function test_createClaimableVestingScheduleFromAmountAndDuration_withCliff_noStartDate() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint256 totalVestedAmount = 103_680_000; // a value perfectly divisible + uint32 vestingDuration = 1 weeks + 1 days; + uint32 cliffPeriod = 1 days; + + int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) + uint256 expectedCliffAmount = 12960000; + uint32 expectedCliffDate = uint32(block.timestamp) + cliffPeriod; + uint32 claimValidityDate = expectedCliffDate + 1 days; + uint32 expectedEndDate = uint32(block.timestamp) + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, uint32(block.timestamp), expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, claimValidityDate, 0); + + vm.startPrank(alice); + + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + claimValidityDate, + cliffPeriod + ); + + vm.stopPrank(); + } + + function test_createClaimableScheduleFromAmountAndDuration_wrongData() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + vm.expectRevert(IVestingSchedulerV2.FlowRateInvalid.selector); + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + 0, + 1209600, + CLAIM_VALIDITY_DATE, + 604800, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with cliff and start in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + CLAIM_VALIDITY_DATE, + 0, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + + console.log("Revert with overflow."); + vm.expectRevert("SafeCast: value doesn't fit in 96 bits"); + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + type(uint256).max, + 1209600, + CLAIM_VALIDITY_DATE, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with underflow/overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + type(uint32).max, + CLAIM_VALIDITY_DATE, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with start date in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createClaimableVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + CLAIM_VALIDITY_DATE, + 604800, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + } + + function test_executeCliffAndFlow_claimableScheduleWithCliffAmount_receiverClaim() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createClaimableVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + + vm.prank(bob); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + emit Transfer(alice, bob, adjustedAmountClosing); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE-CLIFF_DATE) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT ; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function test_executeCliffAndFlow_claimableScheduleWithCliffAmount_senderClaim() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createClaimableVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + assertTrue(success, "executeVesting should return true"); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + emit Transfer(alice, bob, adjustedAmountClosing); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE-CLIFF_DATE) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT ; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function test_executeCliffAndFlow_claimableScheduleWithCliffAmount_cannotClaimOnBehalf(address _claimer) public { + vm.assume(_claimer != address(0) && _claimer != alice && _claimer != bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createClaimableVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + vm.prank(_claimer); + vm.expectRevert(IVestingSchedulerV2.CannotClaimScheduleOnBehalf.selector); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertEq(success, false); + } + + function test_executeCliffAndFlow_claimableScheduleWithCliffAmount_claimBeforeStart() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createClaimableVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + uint256 startTimestamp = vestingScheduler.getVestingSchedule(address(superToken), alice, bob).cliffAndFlowDate; + vm.warp(startTimestamp - 1); + + vm.prank(bob); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertEq(success, false); + } + + function test_executeCliffAndFlow_claimableScheduleWithCliffAmount_claimAfterValidityDate() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createClaimableVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + vm.warp(CLAIM_VALIDITY_DATE + 1); + vm.prank(bob); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertEq(success, false); + } + + + function _getExpectedSchedule( + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + endDate: endDate, + claimValidityDate: 0, + flowRate: flowRate, + cliffAmount: cliffAmount, + remainderAmount: 0 + }); + } + + function _getExpectedScheduleFromAmountAndDuration( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + + uint32 cliffDate; + uint32 cliffAndFlowDate; + uint256 cliffAmount; + if (cliffPeriod > 0) { + cliffDate = startDate + cliffPeriod; + cliffAmount = cliffPeriod * SafeCast.toUint256(flowRate); + cliffAndFlowDate = cliffDate; + } else { + cliffDate = 0; + cliffAmount = 0; + cliffAndFlowDate = startDate; + } + + uint32 endDate = startDate + totalDuration; + + uint256 remainderAmount = totalAmount - SafeCast.toUint256(flowRate) * totalDuration; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + endDate: endDate, + claimValidityDate: 0, + flowRate: flowRate, + cliffAmount: cliffAmount, remainderAmount: remainderAmount }); } diff --git a/yarn.lock b/yarn.lock index a91339e0bc..36ffce6bf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13668,34 +13668,7 @@ mnemonist@^0.38.0: dependencies: obliterator "^2.0.0" -mocha@10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" - integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== - dependencies: - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.4" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.2.0" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "5.0.1" - ms "2.1.3" - nanoid "3.3.3" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - workerpool "6.2.1" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" - -mocha@^10.0.0, mocha@^10.2.0: +mocha@10.1.0, mocha@^10.0.0, mocha@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==