From bd008a20ddc2816e52cad3537002b4ed1e8c6088 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 10 Apr 2024 11:55:03 +0300 Subject: [PATCH] keep V1 contract, separate V2 explicitly --- .../scheduler/contracts/VestingScheduler.sol | 273 +---- .../contracts/VestingSchedulerV2.sol | 502 ++++++++ .../contracts/interface/IVestingScheduler.sol | 108 +- .../interface/IVestingSchedulerV2.sol | 310 +++++ .../scheduler/test/VestingScheduler.t.sol | 479 +------- .../scheduler/test/VestingSchedulerV2.t.sol | 1011 +++++++++++++++++ 6 files changed, 1855 insertions(+), 828 deletions(-) create mode 100644 packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol create mode 100644 packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol create mode 100644 packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 6f2c854df9..031e0d4ab2 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -7,8 +7,6 @@ import { import { SuperAppBase } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperAppBase.sol"; import { CFAv1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; import { IVestingScheduler } from "./interface/IVestingScheduler.sol"; -import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; contract VestingScheduler is IVestingScheduler, SuperAppBase { @@ -53,239 +51,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 endDate, bytes memory ctx ) external returns (bytes memory newCtx) { - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - 0, // remainderAmount - ctx - ); - } - - /// @dev IVestingScheduler.createVestingSchedule implementation. - function createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate - ) external { - _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - 0, // remainderAmount - bytes("") - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) external returns (bytes memory newCtx) { - newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - ctx - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) external { - _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - bytes("") - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod - ) external { - _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - 0, // startDate - bytes("") - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external { - _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - bytes("") - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) external returns (bytes memory newCtx) { - newCtx = _createAndExecuteVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - ctx - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external { - _createAndExecuteVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - bytes("") - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function _createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) private returns (bytes memory newCtx) { - newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - ctx - ); - - address sender = _getSender(ctx); - assert(_executeCliffAndFlow(superToken, sender, receiver)); - } - - function _createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) private returns (bytes memory newCtx) { - 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)); // cliffPeriod * flowRate - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - remainderAmount, - ctx - ); - } - } - - function _createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate, - uint256 remainderAmount, - bytes memory ctx - ) private returns (bytes memory newCtx) { newCtx = ctx; address sender = _getSender(ctx); - - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - if (startDate < block.timestamp) revert TimeWindowInvalid(); if (receiver == address(0) || receiver == sender) revert AccountInvalid(); if (address(superToken) == address(0)) revert ZeroAddress(); @@ -294,7 +61,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - if (cliffAndFlowDate < block.timestamp || + if (cliffAndFlowDate <= block.timestamp || cliffAndFlowDate >= endDate || cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || endDate - cliffAndFlowDate < MIN_VESTING_DURATION @@ -306,8 +73,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cliffAndFlowDate, endDate, flowRate, - cliffAmount, - remainderAmount + cliffAmount ); emit VestingScheduleCreated( @@ -318,8 +84,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cliffDate, flowRate, endDate, - cliffAmount, - remainderAmount + cliffAmount ); } @@ -340,9 +105,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); vestingSchedules[configHash].endDate = endDate; - vestingSchedules[configHash].remainderAmount = 0; - // Note: Nullify the remainder amount if complexity of updates is introduced. - emit VestingScheduleUpdated( superToken, sender, @@ -376,15 +138,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { address sender, address receiver ) external returns (bool success) { - return _executeCliffAndFlow(superToken, sender, receiver); - } - - /// @dev IVestingScheduler.executeCliffAndFlow implementation. - function _executeCliffAndFlow( - ISuperToken superToken, - address sender, - address receiver - ) private returns (bool success) { bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); VestingSchedule memory schedule = vestingSchedules[configHash]; @@ -424,7 +177,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { return true; } - /// @dev IVestingScheduler.executeEndVesting implementation. function executeEndVesting( ISuperToken superToken, @@ -443,14 +195,17 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // delete first the stream and unlock deposit amount. cfaV1.deleteFlowByOperator(sender, receiver, superToken); - uint256 earlyEndCompensation = schedule.endDate >= block.timestamp - ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount - : 0; - - bool didCompensationFail = schedule.endDate < block.timestamp; + uint256 earlyEndCompensation = schedule.endDate > block.timestamp ? + (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) : 0; + bool didCompensationFail; if (earlyEndCompensation != 0) { - assert(superToken.transferFrom(sender, receiver, earlyEndCompensation)); - // TODO: Assert? Revert? SafeERC20? + // try-catch this because if the account does not have tokens for earlyEndCompensation + // we should delete the flow anyway. + try superToken.transferFrom(sender, receiver, earlyEndCompensation) + // solhint-disable-next-line no-empty-blocks + {} catch { + didCompensationFail = true; + } } emit VestingEndExecuted( @@ -499,4 +254,4 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { (,int96 flowRate,,) = cfaV1.cfa.getFlow(superToken, sender, receiver); return flowRate != 0; } -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol new file mode 100644 index 0000000000..b231add6da --- /dev/null +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable not-rely-on-time +pragma solidity ^0.8.0; +import { + ISuperfluid, ISuperToken, SuperAppDefinitions, IConstantFlowAgreementV1 +} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import { SuperAppBase } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperAppBase.sol"; +import { CFAv1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; +import { IVestingSchedulerV2 } from "./interface/IVestingSchedulerV2.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { + + using CFAv1Library for CFAv1Library.InitData; + CFAv1Library.InitData public cfaV1; + mapping(bytes32 => VestingSchedule) public vestingSchedules; // id = keccak(supertoken, sender, receiver) + + uint32 public constant MIN_VESTING_DURATION = 7 days; + uint32 public constant START_DATE_VALID_AFTER = 3 days; + uint32 public constant END_DATE_VALID_BEFORE = 1 days; + + constructor(ISuperfluid host, string memory registrationKey) { + cfaV1 = CFAv1Library.InitData( + host, + IConstantFlowAgreementV1( + address( + host.getAgreementClass( + keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") + ) + ) + ) + ); + // Superfluid SuperApp registration. This is a dumb SuperApp, only for front-end tx batch calls. + uint256 configWord = SuperAppDefinitions.APP_LEVEL_FINAL | + SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP | + SuperAppDefinitions.AFTER_AGREEMENT_CREATED_NOOP | + SuperAppDefinitions.BEFORE_AGREEMENT_UPDATED_NOOP | + SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | + SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | + SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; + host.registerAppWithKey(configWord, registrationKey); + } + + /// @dev IVestingScheduler.createVestingSchedule implementation. + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + 0, // remainderAmount + ctx + ); + } + + /// @dev IVestingScheduler.createVestingSchedule implementation. + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external { + _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + 0, // remainderAmount + bytes("") + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + ctx + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + bytes("") + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + 0, // startDate + bytes("") + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + bytes("") + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + ctx + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external { + _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + bytes("") + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function _createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + ctx + ); + + address sender = _getSender(ctx); + assert(_executeCliffAndFlow(superToken, sender, receiver)); + } + + function _createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) private returns (bytes memory newCtx) { + 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)); // cliffPeriod * flowRate + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + remainderAmount, + ctx + ); + } + } + + function _createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + uint256 remainderAmount, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + if (startDate < block.timestamp) 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 (cliffAndFlowDate < block.timestamp || + cliffAndFlowDate >= endDate || + cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || + endDate - cliffAndFlowDate < MIN_VESTING_DURATION + ) revert TimeWindowInvalid(); + + bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver)); + if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists(); + vestingSchedules[hashConfig] = VestingSchedule( + cliffAndFlowDate, + endDate, + flowRate, + cliffAmount, + remainderAmount + ); + + emit VestingScheduleCreated( + superToken, + sender, + receiver, + startDate, + cliffDate, + flowRate, + endDate, + cliffAmount, + remainderAmount + ); + } + + function updateVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + VestingSchedule memory schedule = vestingSchedules[configHash]; + + if (endDate <= block.timestamp) revert TimeWindowInvalid(); + + // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called + if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); + vestingSchedules[configHash].endDate = endDate; + vestingSchedules[configHash].remainderAmount = 0; + // Note: Nullify the remainder amount if complexity of updates is introduced. + + emit VestingScheduleUpdated( + superToken, + sender, + receiver, + schedule.endDate, + endDate + ); + } + + /// @dev IVestingScheduler.deleteVestingSchedule implementation. + function deleteVestingSchedule( + ISuperToken superToken, + address receiver, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + + if (vestingSchedules[configHash].endDate != 0) { + delete vestingSchedules[configHash]; + emit VestingScheduleDeleted(superToken, sender, receiver); + } else { + revert ScheduleDoesNotExist(); + } + } + + /// @dev IVestingScheduler.executeCliffAndFlow implementation. + function executeCliffAndFlow( + ISuperToken superToken, + address sender, + address receiver + ) external returns (bool success) { + return _executeCliffAndFlow(superToken, sender, receiver); + } + + /// @dev IVestingScheduler.executeCliffAndFlow implementation. + function _executeCliffAndFlow( + ISuperToken superToken, + address sender, + address receiver + ) private returns (bool success) { + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + VestingSchedule memory schedule = vestingSchedules[configHash]; + + if (schedule.cliffAndFlowDate > block.timestamp || + schedule.cliffAndFlowDate + START_DATE_VALID_AFTER < block.timestamp + ) revert TimeWindowInvalid(); + + // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry. + delete vestingSchedules[configHash].cliffAndFlowDate; + delete vestingSchedules[configHash].cliffAmount; + + // Compensate for the fact that flow will almost always be executed slightly later than scheduled. + uint256 flowDelayCompensation = (block.timestamp - schedule.cliffAndFlowDate) * uint96(schedule.flowRate); + + // If there's cliff or compensation then transfer that amount. + if (schedule.cliffAmount != 0 || flowDelayCompensation != 0) { + superToken.transferFrom( + sender, + receiver, + schedule.cliffAmount + flowDelayCompensation + ); + } + + // Create a flow according to the vesting schedule configuration. + cfaV1.createFlowByOperator(sender, receiver, superToken, schedule.flowRate); + + emit VestingCliffAndFlowExecuted( + superToken, + sender, + receiver, + schedule.cliffAndFlowDate, + schedule.flowRate, + schedule.cliffAmount, + flowDelayCompensation + ); + + return true; + } + + + /// @dev IVestingScheduler.executeEndVesting implementation. + function executeEndVesting( + ISuperToken superToken, + address sender, + address receiver + ) external returns (bool success){ + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + VestingSchedule memory schedule = vestingSchedules[configHash]; + + if (schedule.endDate - END_DATE_VALID_BEFORE > block.timestamp) revert TimeWindowInvalid(); + + // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry. + delete vestingSchedules[configHash]; + // If vesting is not running, we can't do anything, just emit failing event. + if(_isFlowOngoing(superToken, sender, receiver)) { + // delete first the stream and unlock deposit amount. + cfaV1.deleteFlowByOperator(sender, receiver, superToken); + + uint256 earlyEndCompensation = schedule.endDate >= block.timestamp + ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount + : 0; + + bool didCompensationFail = schedule.endDate < block.timestamp; + if (earlyEndCompensation != 0) { + assert(superToken.transferFrom(sender, receiver, earlyEndCompensation)); + // TODO: Assert? Revert? SafeERC20? + } + + emit VestingEndExecuted( + superToken, + sender, + receiver, + schedule.endDate, + earlyEndCompensation, + didCompensationFail + ); + } else { + emit VestingEndFailed( + superToken, + sender, + receiver, + schedule.endDate + ); + } + + return true; + } + + /// @dev IVestingScheduler.getVestingSchedule implementation. + function getVestingSchedule( + address supertoken, + address sender, + address receiver + ) external view returns (VestingSchedule memory) { + return vestingSchedules[keccak256(abi.encodePacked(supertoken, sender, receiver))]; + } + + /// @dev get sender of transaction from Superfluid Context or transaction itself. + function _getSender(bytes memory ctx) internal view returns (address sender) { + if (ctx.length != 0) { + if (msg.sender != address(cfaV1.host)) revert HostInvalid(); + sender = cfaV1.host.decodeCtx(ctx).msgSender; + } else { + sender = msg.sender; + } + // This is an invariant and should never happen. + assert(sender != address(0)); + } + + /// @dev get flowRate of stream + function _isFlowOngoing(ISuperToken superToken, address sender, address receiver) internal view returns (bool) { + (,int96 flowRate,,) = cfaV1.cfa.getFlow(superToken, sender, receiver); + return flowRate != 0; + } +} diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index f83c2f83b2..9cb5b20802 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -22,14 +22,12 @@ interface IVestingScheduler { * @param endDate End date of the vesting * @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" */ struct VestingSchedule { uint32 cliffAndFlowDate; uint32 endDate; int96 flowRate; uint256 cliffAmount; - uint256 remainderAmount; // TODO: consider packing } /** @@ -42,7 +40,6 @@ interface IVestingScheduler { * @param flowRate The flowRate for the stream * @param endDate The timestamp when the stream should stop * @param cliffAmount The amount to be transferred at the cliff - * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" */ event VestingScheduleCreated( ISuperToken indexed superToken, @@ -52,8 +49,7 @@ interface IVestingScheduler { uint32 cliffDate, int96 flowRate, uint32 endDate, - uint256 cliffAmount, - uint256 remainderAmount + uint256 cliffAmount ); /** @@ -80,106 +76,6 @@ interface IVestingScheduler { bytes memory ctx ) external returns (bytes memory newCtx); - /** - * @dev See IVestingScheduler.createVestingSchedule overload for more details. - */ - function createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate - ) external; - - /** - * @dev Creates a new vesting schedule - * @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 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 createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) external returns (bytes memory newCtx); - - /** - * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) external; - - /** - * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - * The startDate is set to current block timestamp. - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod - ) external; - - /** - * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - * The startDate is set to current block timestamp. - * Cliff period is not applied. - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external; - - /** - * @dev Creates a new vesting schedule - * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. - * @dev The function creates the vesting schedule with start date set to current timestamp, - * @dev and executes the start (i.e. creation of the flow) immediately. - * @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 ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) - */ - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) external returns (bytes memory newCtx); - - /** - * @dev See IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - */ - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external; - /** * @dev Event emitted on update of a vesting schedule * @param superToken The superToken to be vested @@ -307,4 +203,4 @@ interface IVestingScheduler { external view returns (VestingSchedule memory); -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol new file mode 100644 index 0000000000..0840d8f819 --- /dev/null +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity ^0.8.0; + +import { + ISuperToken +} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; + +interface IVestingSchedulerV2 { + error TimeWindowInvalid(); + error AccountInvalid(); + error ZeroAddress(); + error HostInvalid(); + error FlowRateInvalid(); + error CliffInvalid(); + error ScheduleAlreadyExists(); + error ScheduleDoesNotExist(); + error ScheduleNotFlowing(); + + /** + * @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 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" + */ + struct VestingSchedule { + uint32 cliffAndFlowDate; + uint32 endDate; + int96 flowRate; + uint256 cliffAmount; + uint256 remainderAmount; // TODO: consider packing + } + + /** + * @dev Event emitted on creation of a new vesting schedule + * @param superToken SuperToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting starts + * @param cliffDate Timestamp of the cliff + * @param flowRate The flowRate for the stream + * @param endDate The timestamp when the stream should stop + * @param cliffAmount The amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" + */ + event VestingScheduleCreated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint32 endDate, + uint256 cliffAmount, + uint256 remainderAmount + ); + + /** + * @dev Creates a new vesting schedule + * @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 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 createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createVestingSchedule overload for more details. + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external; + + /** + * @dev Creates a new vesting schedule + * @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 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 createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + * Cliff period is not applied. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + + /** + * @dev Creates a new vesting schedule + * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. + * @dev The function creates the vesting schedule with start date set to current timestamp, + * @dev and executes the start (i.e. creation of the flow) immediately. + * @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 ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + */ + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + + /** + * @dev Event emitted on update of a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param oldEndDate Old timestamp when the stream should stop + * @param endDate New timestamp when the stream should stop + */ + event VestingScheduleUpdated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 oldEndDate, + uint32 endDate + ); + + /** + * @dev Updates the end date for a vesting schedule which already reached the cliff + * @notice When updating, there's no restriction to the end date other than not being in the past + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @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 updateVestingSchedule(ISuperToken superToken, address receiver, uint32 endDate, bytes memory ctx) + external + returns (bytes memory newCtx); + + /** + * @dev Event emitted on deletion of a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + event VestingScheduleDeleted(ISuperToken indexed superToken, address indexed sender, address indexed receiver); + + /** + * @dev Event emitted on end of a vesting that failed because there was no running stream + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + */ + event VestingEndFailed( + ISuperToken indexed superToken, address indexed sender, address indexed receiver, uint32 endDate + ); + + /** + * @dev Deletes a vesting schedule + * @param superToken The superToken to be vested + * @param receiver Vesting receiver + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function deleteVestingSchedule(ISuperToken superToken, address receiver, bytes memory ctx) + external + returns (bytes memory newCtx); + + /** + * @dev Emitted when the cliff of a scheduled vesting is executed + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param cliffAndFlowDate The timestamp when the stream should start + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount you would like to transfer at the startDate when you start streaming + * @param flowDelayCompensation Adjusted amount transferred to receiver. (elapse time from config and tx timestamp) + */ + event VestingCliffAndFlowExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 cliffAndFlowDate, + int96 flowRate, + uint256 cliffAmount, + uint256 flowDelayCompensation + ); + + /** + * @dev Executes a cliff (transfer and stream start) + * @notice Intended to be invoked by a backend service + * @param superToken SuperToken to be streamed + * @param sender Account who will be send the stream + * @param receiver Account who will be receiving the stream + */ + function executeCliffAndFlow(ISuperToken superToken, address sender, address receiver) + external + returns (bool success); + + /** + * @dev Emitted when the end of a scheduled vesting is executed + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + * @param earlyEndCompensation adjusted close amount transferred to receiver. + * @param didCompensationFail adjusted close amount transfer fail. + */ + event VestingEndExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate, + uint256 earlyEndCompensation, + bool didCompensationFail + ); + + /** + * @dev Executes the end of a vesting (stop stream) + * @notice Intended to be invoked by a backend service + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + function executeEndVesting(ISuperToken superToken, address sender, address receiver) + external + returns (bool success); + + /** + * @dev Gets data currently stored for a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + function getVestingSchedule(address superToken, address sender, address receiver) + external + view + returns (VestingSchedule memory); +} diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index 673e7233d9..e65f5d0310 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -7,9 +7,6 @@ import { IVestingScheduler } from "./../contracts/interface/IVestingScheduler.so import { VestingScheduler } from "./../contracts/VestingScheduler.sol"; import { FoundrySuperfluidTester } from "@superfluid-finance/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol"; import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; -import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import "forge-std/console.sol"; /// @title VestingSchedulerTests /// @notice Look at me , I am the captain now - Elvijs @@ -24,8 +21,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint32 cliffDate, int96 flowRate, uint32 endDate, - uint256 cliffAmount, - uint256 remainderAmount + uint256 cliffAmount ); event VestingScheduleUpdated( @@ -71,15 +67,14 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { event Transfer(address indexed from, address indexed to, uint256 value); /// @dev This is required by solidity for using the SuperTokenV1Library in the tester - VestingScheduler public vestingScheduler; + VestingScheduler internal vestingScheduler; /// @dev Constants for Testing - uint256 immutable BLOCK_TIMESTAMP = 100; - uint32 immutable START_DATE = uint32(BLOCK_TIMESTAMP + 1); - uint32 immutable CLIFF_DATE = uint32(BLOCK_TIMESTAMP + 10 days); + uint32 immutable START_DATE = uint32(block.timestamp + 1); + uint32 immutable CLIFF_DATE = uint32(block.timestamp + 10 days); int96 constant FLOW_RATE = 1000000000; uint256 constant CLIFF_TRANSFER_AMOUNT = 1 ether; - uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); + uint32 immutable END_DATE = uint32(block.timestamp + 20 days); bytes constant EMPTY_CTX = ""; uint256 internal _expectedTotalSupply = 0; @@ -90,7 +85,6 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { /// SETUP AND HELPERS function setUp() override public virtual { super.setUp(); - vm.warp(BLOCK_TIMESTAMP); } function _setACL_AUTHORIZE_FULL_CONTROL(address user, int96 flowRate) private { @@ -112,16 +106,6 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vm.stopPrank(); } - function _arrangeAllowances(address sender, int96 flowRate) private { - // ## Superfluid ACL allowance and permissions - _setACL_AUTHORIZE_FULL_CONTROL(sender, flowRate); - - // ## ERC-20 allowance for cliff and compensation transfers - vm.startPrank(sender); - superToken.approve(address(vestingScheduler), type(uint256).max); - vm.stopPrank(); - } - function _createVestingScheduleWithDefaultData(address sender, address receiver) private { vm.startPrank(sender); vestingScheduler.createVestingSchedule( @@ -142,7 +126,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { 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 ); _createVestingScheduleWithDefaultData(alice, bob); vm.startPrank(alice); @@ -208,7 +192,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { EMPTY_CTX ); - // revert with cliffDate = 0 but cliffAmount != 0 + // revert with startDate && cliffDate = 0 vm.expectRevert(IVestingScheduler.CliffInvalid.selector); vestingScheduler.createVestingSchedule( superToken, @@ -221,12 +205,12 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { EMPTY_CTX ); - // revert with startDate < block.timestamp && cliffDate = 0 + // revert with startDate && cliffDate = 0 vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); vestingScheduler.createVestingSchedule( superToken, bob, - uint32(block.timestamp - 1), + 0, 0, FLOW_RATE, 0, @@ -252,8 +236,8 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vestingScheduler.createVestingSchedule( superToken, bob, - 0, uint32(block.timestamp) - 1, + 0, FLOW_RATE, 0, END_DATE, @@ -324,7 +308,7 @@ contract VestingSchedulerTests 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 ); _createVestingScheduleWithDefaultData(alice, bob); vm.prank(alice); @@ -514,18 +498,13 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { superToken.transferAll(eve); vm.stopPrank(); vm.startPrank(admin); - uint256 earlyEndTimestamp = block.timestamp + 10 days - 3600; - vm.warp(earlyEndTimestamp); - - vm.expectRevert(); - vestingScheduler.executeEndVesting(superToken, alice, bob); - - uint256 finalTimestamp = END_DATE + 1; + uint256 finalTimestamp = block.timestamp + 10 days - 3600; vm.warp(finalTimestamp); - + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); vm.expectEmit(true, true, true, true); emit VestingEndExecuted( - superToken, alice, bob, END_DATE, 0, true + superToken, alice, bob, END_DATE, adjustedAmountClosing, true ); success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); @@ -559,17 +538,13 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vm.startPrank(admin); 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); assertTrue(success, "executeVesting should return true"); vm.stopPrank(); @@ -586,426 +561,4 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); } - - // # Vesting Scheduler 1.2 tests - - function testCreateAndExecuteImmediately() public { - uint256 aliceInitialBalance = superToken.balanceOf(alice); - uint256 bobInitialBalance = superToken.balanceOf(bob); - - // # Create schedule - _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); - - vm.startPrank(alice); - superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - - uint32 startAndCliffDate = uint32(block.timestamp); - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); - - vestingScheduler.createVestingSchedule( - superToken, - bob, - startAndCliffDate, - startAndCliffDate, - FLOW_RATE, - CLIFF_TRANSFER_AMOUNT, - END_DATE, - EMPTY_CTX - ); - // --- - - // # Execute start - vm.expectEmit(); - emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT); - - vm.expectEmit(); - emit VestingCliffAndFlowExecuted( - superToken, alice, bob, startAndCliffDate, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, uint256(0) - ); - vm.stopPrank(); - - vm.startPrank(admin); - bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); - vm.stopPrank(); - - assertTrue(success, "executeVesting should return true"); - // --- - - // # Execute end - uint256 finalTimestamp = END_DATE - 3600; - vm.warp(finalTimestamp); - - uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; - uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); - - vm.expectEmit(); - emit Transfer(alice, bob, adjustedAmountClosing); - - vm.expectEmit(); - emit VestingEndExecuted( - superToken, alice, bob, END_DATE, adjustedAmountClosing, false - ); - vm.startPrank(admin); - success = vestingScheduler.executeEndVesting(superToken, alice, bob); - vm.stopPrank(); - assertTrue(success, "executeCloseVesting should return true"); - - uint256 aliceFinalBalance = superToken.balanceOf(alice); - uint256 bobFinalBalance = superToken.balanceOf(bob); - uint256 aliceShouldStream = (END_DATE - startAndCliffDate) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT; - assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); - assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); - // --- - } - - function test_createScheduleFromAmountAndDuration_reverts() public { - _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); - superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - - vm.expectRevert(); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 0, - 1209600, - 604800, - uint32(block.timestamp), - EMPTY_CTX - ); - - console.log("Revert with cliff and start in history."); - vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - 1209600, - 0, - uint32(block.timestamp - 1), - EMPTY_CTX - ); - - console.log("Revert with overflow."); - vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - type(uint256).max, - 1209600, - 0, - uint32(block.timestamp), - EMPTY_CTX - ); - - console.log("Revert with underflow/overflow."); - vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - type(uint32).max, - 0, - uint32(block.timestamp), - EMPTY_CTX - ); - - console.log("Revert with start date in history."); - vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - 1209600, - 604800, - uint32(block.timestamp - 1), - EMPTY_CTX - ); - } - - function testNewFunctionScheduleCreationWithoutCliff(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; - int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration - uint32 expectedEndDate = startDate + vestingDuration; - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); - - vm.startPrank(alice); - bool useCtx = randomizer % 2 == 0; - if (useCtx) { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - 0, - startDate, - EMPTY_CTX - ); - } else { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - 0, - startDate - ); - } - vm.stopPrank(); - } - - function testNewFunctionScheduleCreationWithCliff(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; - - int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) - uint256 expectedCliffAmount = 12960000; - uint32 expectedCliffDate = startDate + cliffPeriod; - uint32 expectedEndDate = startDate + vestingDuration; - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); - - vm.startPrank(alice); - bool useCtx = randomizer % 2 == 0; - if (useCtx) { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - cliffPeriod, - startDate, - EMPTY_CTX - ); - } else { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - cliffPeriod, - startDate - ); - } - vm.stopPrank(); - } - - function test_createScheduleFromAmountAndDuration_executeCliffAndFlow_executeEndVesting( - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - uint8 randomizer - ) public { - // Assume - vm.assume(randomizer != 0); - - vm.assume(startDate == 0 || startDate >= block.timestamp); - vm.assume(startDate < 2524600800 /* year 2050 */); - - vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); - vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); - vm.assume(totalDuration < 18250 days /* 50 years */); - - uint256 beforeSenderBalance = superToken.balanceOf(alice); - uint256 beforeReceiverBalance = superToken.balanceOf(bob); - - vm.assume(totalAmount > 1); - vm.assume(totalAmount >= totalDuration); - vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); - vm.assume(totalAmount <= beforeSenderBalance); - - IVestingScheduler.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); - - // Arrange - IVestingScheduler.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( - totalAmount, - totalDuration, - cliffPeriod, - startDate - ); - uint32 expectedCliffDate = cliffPeriod == 0 ? 0 : expectedSchedule.cliffAndFlowDate; - uint32 expectedStartDate = startDate == 0 ? uint32(block.timestamp) : startDate; - - // Assume we're not getting liquidated at the end: - vm.assume(totalAmount <= (beforeSenderBalance - vestingScheduler.END_DATE_VALID_BEFORE() * SafeCast.toUint256(expectedSchedule.flowRate))); - - console.log("Total amount: %s", totalAmount); - console.log("Total duration: %s", totalDuration); - console.log("Cliff period: %s", cliffPeriod); - console.log("Start date: %s", startDate); - console.log("Randomizer: %s", randomizer); - console.log("Expected start date: %s", expectedStartDate); - console.log("Expected cliff date: %s", expectedCliffDate); - console.log("Expected cliff & flow date: %s", expectedSchedule.cliffAndFlowDate); - console.log("Expected end date: %s", expectedSchedule.endDate); - console.log("Expected flow rate: %s", SafeCast.toUint256(expectedSchedule.flowRate)); - console.log("Expected cliff amount: %s", expectedSchedule.cliffAmount); - console.log("Expected remainder amount: %s", expectedSchedule.remainderAmount); - console.log("Sender balance: %s", beforeSenderBalance); - - _arrangeAllowances(alice, expectedSchedule.flowRate); - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, expectedSchedule.remainderAmount); - - // Act - vm.startPrank(alice); - if (startDate == 0 && randomizer % 2 == 0) { - console.log("Using the overload without start date."); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalAmount, - totalDuration, - cliffPeriod - ); - } else { - if (randomizer % 3 == 0) { - console.log("Using the overload without superfluid context."); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalAmount, - totalDuration, - cliffPeriod, - startDate - ); - } else { - console.log("Using the overload with superfluid context."); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - EMPTY_CTX - ); - } - } - vm.stopPrank(); - - // Assert - IVestingScheduler.VestingSchedule memory actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertEq(actualSchedule.cliffAndFlowDate, expectedSchedule.cliffAndFlowDate, "schedule created: cliffAndFlowDate not expected"); - assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule created: flowRate not expected"); - assertEq(actualSchedule.cliffAmount, expectedSchedule.cliffAmount, "schedule created: cliffAmount not expected"); - assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule created: endDate not expected"); - assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); - - // Act - vm.warp(expectedSchedule.cliffAndFlowDate + (vestingScheduler.START_DATE_VALID_AFTER() - (vestingScheduler.START_DATE_VALID_AFTER() / randomizer))); - assertTrue(vestingScheduler.executeCliffAndFlow(superToken, alice, bob)); - - // Assert - actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule started: cliffAndFlowDate not expected"); - assertEq(actualSchedule.cliffAmount, 0, "schedule started: cliffAmount not expected"); - assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule started: flowRate not expected"); - assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule started: endDate not expected"); - assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule started: remainderAmount not expected"); - - // Act - vm.warp(expectedSchedule.endDate - (vestingScheduler.END_DATE_VALID_BEFORE() - (vestingScheduler.END_DATE_VALID_BEFORE() / randomizer))); - assertTrue(vestingScheduler.executeEndVesting(superToken, alice, bob)); - - actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule ended: cliffAndFlowDate not expected"); - assertEq(actualSchedule.cliffAmount, 0, "schedule ended: cliffAmount not expected"); - assertEq(actualSchedule.flowRate, 0, "schedule ended: flowRate not expected"); - assertEq(actualSchedule.endDate, 0, "schedule ended: endDate not expected"); - assertEq(actualSchedule.remainderAmount, 0, "schedule ended: remainderAmount not expected"); - - // Assert - uint256 afterSenderBalance = superToken.balanceOf(alice); - uint256 afterReceiverBalance = superToken.balanceOf(bob); - - assertEq(afterSenderBalance, beforeSenderBalance - totalAmount, "Sender balance should decrease by totalAmount"); - assertEq(afterReceiverBalance, beforeReceiverBalance + totalAmount, "Receiver balance should increase by totalAmount"); - - vm.warp(type(uint32).max); - 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 (IVestingScheduler.VestingSchedule memory expectedSchedule) { - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - - uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - - expectedSchedule = IVestingScheduler.VestingSchedule({ - cliffAndFlowDate: cliffAndFlowDate, - flowRate: flowRate, - cliffAmount: cliffAmount, - endDate: endDate, - remainderAmount: 0 - }); - } - - function _getExpectedScheduleFromAmountAndDuration( - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) public view returns (IVestingScheduler.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 = IVestingScheduler.VestingSchedule({ - cliffAndFlowDate: cliffAndFlowDate, - flowRate: flowRate, - cliffAmount: cliffAmount, - endDate: endDate, - remainderAmount: remainderAmount - }); - } -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol new file mode 100644 index 0000000000..846742c1fe --- /dev/null +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -0,0 +1,1011 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; +import { FlowOperatorDefinitions } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import { IVestingSchedulerV2 } from "./../contracts/interface/IVestingSchedulerV2.sol"; +import { VestingSchedulerV2 } from "./../contracts/VestingSchedulerV2.sol"; +import { FoundrySuperfluidTester } from "@superfluid-finance/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol"; +import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "forge-std/console.sol"; + +/// @title VestingSchedulerTests +/// @notice Look at me , I am the captain now - Elvijs +contract VestingSchedulerV2Tests is FoundrySuperfluidTester { + using SuperTokenV1Library for ISuperToken; + + event VestingScheduleCreated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint32 endDate, + uint256 cliffAmount, + uint256 remainderAmount + ); + + event VestingScheduleUpdated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 oldEndDate, + uint32 endDate + ); + + event VestingScheduleDeleted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver + ); + + event VestingCliffAndFlowExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 cliffAndFlowDate, + int96 flowRate, + uint256 cliffAmount, + uint256 flowDelayCompensation + ); + + event VestingEndExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate, + uint256 earlyEndCompensation, + bool didCompensationFail + ); + + event VestingEndFailed( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate + ); + + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev This is required by solidity for using the SuperTokenV1Library in the tester + VestingSchedulerV2 public vestingScheduler; + + /// @dev Constants for Testing + uint256 immutable BLOCK_TIMESTAMP = 100; + uint32 immutable START_DATE = uint32(BLOCK_TIMESTAMP + 1); + uint32 immutable CLIFF_DATE = uint32(BLOCK_TIMESTAMP + 10 days); + int96 constant FLOW_RATE = 1000000000; + uint256 constant CLIFF_TRANSFER_AMOUNT = 1 ether; + uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); + bytes constant EMPTY_CTX = ""; + uint256 internal _expectedTotalSupply = 0; + + constructor() FoundrySuperfluidTester(3) { + vestingScheduler = new VestingSchedulerV2(sf.host, ""); + } + + /// SETUP AND HELPERS + function setUp() override public virtual { + super.setUp(); + vm.warp(BLOCK_TIMESTAMP); + } + + function _setACL_AUTHORIZE_FULL_CONTROL(address user, int96 flowRate) private { + vm.startPrank(user); + sf.host.callAgreement( + sf.cfa, + abi.encodeCall( + sf.cfa.updateFlowOperatorPermissions, + ( + superToken, + address(vestingScheduler), + FlowOperatorDefinitions.AUTHORIZE_FULL_CONTROL, + flowRate, + new bytes(0) + ) + ), + new bytes(0) + ); + vm.stopPrank(); + } + + function _arrangeAllowances(address sender, int96 flowRate) private { + // ## Superfluid ACL allowance and permissions + _setACL_AUTHORIZE_FULL_CONTROL(sender, flowRate); + + // ## ERC-20 allowance for cliff and compensation transfers + vm.startPrank(sender); + superToken.approve(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + } + + function _createVestingScheduleWithDefaultData(address sender, address receiver) private { + vm.startPrank(sender); + vestingScheduler.createVestingSchedule( + superToken, + receiver, + START_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 + ); + _createVestingScheduleWithDefaultData(alice, bob); + 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.cliffAmount == CLIFF_TRANSFER_AMOUNT , "schedule.cliffAmount"); + } + + function testCannotCreateVestingScheduleWithWrongData() public { + vm.startPrank(alice); + // revert with superToken = 0 + vm.expectRevert(IVestingSchedulerV2.ZeroAddress.selector); + vestingScheduler.createVestingSchedule( + ISuperToken(address(0)), + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = sender + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + alice, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = address(0) + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + address(0), + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with flowRate = 0 + vm.expectRevert(IVestingSchedulerV2.FlowRateInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + 0, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffDate = 0 but cliffAmount != 0 + vm.expectRevert(IVestingSchedulerV2.CliffInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + 0, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with startDate < block.timestamp && cliffDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + uint32(block.timestamp - 1), + 0, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with endDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + 0, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate < block.timestamp + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + uint32(block.timestamp) - 1, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate >= endDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate + startDateValidFor >= endDate - endDateValidBefore + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with startDate > cliffDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + CLIFF_DATE + 1, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + + // revert with vesting duration < 7 days + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE + 2 days, + EMPTY_CTX + ); + } + + function testCannotCreateVestingScheduleIfDataExist() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.expectRevert(IVestingSchedulerV2.ScheduleAlreadyExists.selector); + _createVestingScheduleWithDefaultData(alice, bob); + } + + function testUpdateVestingSchedule() 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 + ); + _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); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE + 1000, EMPTY_CTX); + //assert storage data + IVestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == 0 , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE + 1000 , "schedule.endDate"); + } + + function testCannotUpdateVestingScheduleIfNotRunning() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleNotFlowing.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE, EMPTY_CTX); + } + + function testCannotUpdateVestingScheduleIfDataDontExist() public { + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleNotFlowing.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE, EMPTY_CTX); + } + + function testDeleteVestingSchedule() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + vm.expectEmit(true, true, true, true); + emit VestingScheduleDeleted(superToken, alice, bob); + vestingScheduler.deleteVestingSchedule(superToken, bob, EMPTY_CTX); + } + + function testCannotDeleteVestingScheduleIfDataDontExist() public { + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleDoesNotExist.selector); + vestingScheduler.deleteVestingSchedule( + superToken, + bob, + EMPTY_CTX + ); + } + + function testExecuteCliffAndFlowWithCliffAmount() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _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); + 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); + 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 testExecuteCliffAndFlowWithoutCliffAmountOrAdjustment() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + vm.startPrank(alice); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + vm.startPrank(admin); + vm.warp(CLIFF_DATE); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, 0, 0 + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.warp(END_DATE); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted(superToken, alice, bob, END_DATE, 0, 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); + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowWithUpdatedEndDate() public { + uint32 NEW_END_DATE = END_DATE - 1000; + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _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); + 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); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit VestingScheduleUpdated(superToken, alice, bob, END_DATE, NEW_END_DATE); + vestingScheduler.updateVestingSchedule(superToken, bob, NEW_END_DATE, EMPTY_CTX); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = NEW_END_DATE > block.timestamp ? NEW_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, NEW_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 = (NEW_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 testExecuteCliffAndFlowRevertClosingTransfer() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _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); + 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); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.startPrank(alice); + superToken.transferAll(eve); + vm.stopPrank(); + vm.startPrank(admin); + uint256 earlyEndTimestamp = block.timestamp + 10 days - 3600; + vm.warp(earlyEndTimestamp); + + vm.expectRevert(); + vestingScheduler.executeEndVesting(superToken, alice, bob); + + uint256 finalTimestamp = END_DATE + 1; + vm.warp(finalTimestamp); + + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, 0, true + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + } + + function testCannotExecuteEndVestingBeforeTime() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.executeEndVesting(superToken, alice, bob); + } + + function testCannotExecuteCliffAndFlowBeforeTime() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + } + + function testCannotExecuteEndWithoutStreamRunning() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _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); + + 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); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.startPrank(alice); + superToken.deleteFlow(alice, bob); + vm.stopPrank(); + vm.startPrank(admin); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + emit VestingEndFailed( + superToken, alice, bob, END_DATE + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + } + + // # Vesting Scheduler 1.2 tests + + function testCreateAndExecuteImmediately() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + + // # Create schedule + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + uint32 startAndCliffDate = uint32(block.timestamp); + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); + + vestingScheduler.createVestingSchedule( + superToken, + bob, + startAndCliffDate, + startAndCliffDate, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + // --- + + // # Execute start + vm.expectEmit(); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT); + + vm.expectEmit(); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, startAndCliffDate, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, uint256(0) + ); + vm.stopPrank(); + + vm.startPrank(admin); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + + assertTrue(success, "executeVesting should return true"); + // --- + + // # Execute end + uint256 finalTimestamp = END_DATE - 3600; + vm.warp(finalTimestamp); + + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + + vm.expectEmit(); + emit Transfer(alice, bob, adjustedAmountClosing); + + vm.expectEmit(); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + vm.startPrank(admin); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + vm.stopPrank(); + assertTrue(success, "executeCloseVesting should return true"); + + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE - startAndCliffDate) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + // --- + } + + function test_createScheduleFromAmountAndDuration_reverts() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + vm.expectRevert(); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 0, + 1209600, + 604800, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with cliff and start in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 0, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + + console.log("Revert with overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + type(uint256).max, + 1209600, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with underflow/overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + type(uint32).max, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with start date in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 604800, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + } + + function testNewFunctionScheduleCreationWithoutCliff(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; + int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + 0, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + 0, + startDate + ); + } + vm.stopPrank(); + } + + function testNewFunctionScheduleCreationWithCliff(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; + + int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) + uint256 expectedCliffAmount = 12960000; + uint32 expectedCliffDate = startDate + cliffPeriod; + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + cliffPeriod, + startDate + ); + } + vm.stopPrank(); + } + + function test_createScheduleFromAmountAndDuration_executeCliffAndFlow_executeEndVesting( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + uint8 randomizer + ) public { + // Assume + vm.assume(randomizer != 0); + + vm.assume(startDate == 0 || startDate >= block.timestamp); + vm.assume(startDate < 2524600800 /* year 2050 */); + + vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(totalDuration < 18250 days /* 50 years */); + + uint256 beforeSenderBalance = superToken.balanceOf(alice); + uint256 beforeReceiverBalance = superToken.balanceOf(bob); + + vm.assume(totalAmount > 1); + vm.assume(totalAmount >= totalDuration); + vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); + vm.assume(totalAmount <= beforeSenderBalance); + + IVestingSchedulerV2.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); + + // Arrange + IVestingSchedulerV2.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + uint32 expectedCliffDate = cliffPeriod == 0 ? 0 : expectedSchedule.cliffAndFlowDate; + uint32 expectedStartDate = startDate == 0 ? uint32(block.timestamp) : startDate; + + // Assume we're not getting liquidated at the end: + vm.assume(totalAmount <= (beforeSenderBalance - vestingScheduler.END_DATE_VALID_BEFORE() * SafeCast.toUint256(expectedSchedule.flowRate))); + + console.log("Total amount: %s", totalAmount); + console.log("Total duration: %s", totalDuration); + console.log("Cliff period: %s", cliffPeriod); + console.log("Start date: %s", startDate); + console.log("Randomizer: %s", randomizer); + console.log("Expected start date: %s", expectedStartDate); + console.log("Expected cliff date: %s", expectedCliffDate); + console.log("Expected cliff & flow date: %s", expectedSchedule.cliffAndFlowDate); + console.log("Expected end date: %s", expectedSchedule.endDate); + console.log("Expected flow rate: %s", SafeCast.toUint256(expectedSchedule.flowRate)); + console.log("Expected cliff amount: %s", expectedSchedule.cliffAmount); + console.log("Expected remainder amount: %s", expectedSchedule.remainderAmount); + console.log("Sender balance: %s", beforeSenderBalance); + + _arrangeAllowances(alice, expectedSchedule.flowRate); + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, expectedSchedule.remainderAmount); + + // Act + vm.startPrank(alice); + if (startDate == 0 && randomizer % 2 == 0) { + console.log("Using the overload without start date."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod + ); + } else { + if (randomizer % 3 == 0) { + console.log("Using the overload without superfluid context."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + } else { + console.log("Using the overload with superfluid context."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } + } + vm.stopPrank(); + + // Assert + IVestingSchedulerV2.VestingSchedule memory actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, expectedSchedule.cliffAndFlowDate, "schedule created: cliffAndFlowDate not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule created: flowRate not expected"); + assertEq(actualSchedule.cliffAmount, expectedSchedule.cliffAmount, "schedule created: cliffAmount not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule created: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.cliffAndFlowDate + (vestingScheduler.START_DATE_VALID_AFTER() - (vestingScheduler.START_DATE_VALID_AFTER() / randomizer))); + assertTrue(vestingScheduler.executeCliffAndFlow(superToken, alice, bob)); + + // Assert + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule started: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule started: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule started: flowRate not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule started: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule started: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.endDate - (vestingScheduler.END_DATE_VALID_BEFORE() - (vestingScheduler.END_DATE_VALID_BEFORE() / randomizer))); + assertTrue(vestingScheduler.executeEndVesting(superToken, alice, bob)); + + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule ended: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule ended: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, 0, "schedule ended: flowRate not expected"); + assertEq(actualSchedule.endDate, 0, "schedule ended: endDate not expected"); + assertEq(actualSchedule.remainderAmount, 0, "schedule ended: remainderAmount not expected"); + + // Assert + uint256 afterSenderBalance = superToken.balanceOf(alice); + uint256 afterReceiverBalance = superToken.balanceOf(bob); + + assertEq(afterSenderBalance, beforeSenderBalance - totalAmount, "Sender balance should decrease by totalAmount"); + assertEq(afterReceiverBalance, beforeReceiverBalance + totalAmount, "Receiver balance should increase by totalAmount"); + + vm.warp(type(uint32).max); + 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); + } + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + 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, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: remainderAmount + }); + } +}