superfluid-finance / protocol-monorepo

Superfluid Protocol Monorepo: the specification, implementations, peripherals and development kits.
https://www.superfluid.finance
Other
875 stars 240 forks source link

[automations][epic] Vesting Scheduler V2 #1952

Closed kasparkallas closed 4 months ago

kasparkallas commented 5 months ago

Another PR, why?

We're releasing V2 for the vesting scheduler; the initial plan was to release it with only ergonomic improvements; but we'll release it bundled with the "claimable vesting schedule" feature. This PR is an epic feature-set which upon merging will confirm the delivery of the feature.

List of Changes

  1. Handle flow rate remainder dust handling (caused by totalAmount not being perfectly divisible by the calculated flowRate)
    • Add remainderAmount to the stored VestingSchedule struct
    • Transfer remainderAmount during the early end execution
  2. Add createVestingSchedule overload without bytes memory ctx
  3. Add createVestingScheduleFromAmountAndDuration function (with overload variants)
  4. Add createAndExecuteVestingScheduleFromAmountAndDuration function (with overload variants) (combines createVestingScheduleFromAmountAndDuration and executeCliffAndFlow)
  5. Replace host.registerAppWithKey(configWord, registrationKey); (deprecated) with host.registerApp(configWord); in the constructor
  6. Remove string memory registrationKey from constructor parameters (not needed anymore)
  7. Default to current block timestamp if start date is not provided (i.e. it's 0).
  8. Revert if startDate is below current block timestamp.
  9. Allow cliffAndFlowDate to be in the current block timestamp (this enables schedule creation and execution in the same transaction).
  10. Remove try-catch from the early end execution (prefer reverting until "early end" is not needed anymore)
  11. Add feature to "claim" vesting schedule, requiring the receiver to claim the vesting schedule after the "cliff and flow" date.
  12. Add feature to transfer the whole amount of the vesting schedule when the claiming happens effectively after the end of the vesting schedule. This works only if the claim validity date is still fulfilled.
  13. Add getMaximumNeededTokenAllowance utility function.
  14. Expose mapCreateVestingScheduleParams utility function.

Included PRs

Deployments

Diffs

Interfaces (IVestingScheduler.sol vs IVestingSchedulerV2.sol)

diff --git a/./../contracts/interface/IVestingScheduler.sol b/./../contracts/interface/IVestingSchedulerV2.sol
index 9cb5b208..3f7d2c40 100644
--- a/./../contracts/interface/IVestingScheduler.sol
+++ b/./../contracts/interface/IVestingSchedulerV2.sol
@@ -1,206 +1,387 @@
 // SPDX-License-Identifier: AGPLv3
 pragma solidity ^0.8.0;

 import {
     ISuperToken
 } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";

-interface IVestingScheduler {
+interface IVestingSchedulerV2 {
     error TimeWindowInvalid();
     error AccountInvalid();
     error ZeroAddress();
     error HostInvalid();
     error FlowRateInvalid();
     error CliffInvalid();
     error ScheduleAlreadyExists();
     error ScheduleDoesNotExist();
     error ScheduleNotFlowing();
+    error CannotClaimScheduleOnBehalf();
+    error AlreadyExecuted();
+    error ScheduleNotClaimed();

     /**
      * @dev Vesting configuration provided by user.
      * @param cliffAndFlowDate Date of flow start and cliff execution (if a cliff was specified)
      * @param endDate End date of the vesting
+     * @param claimValidityDate Date before which the claimable schedule must be claimed
      * @param flowRate For the stream
      * @param cliffAmount Amount to be transferred at the cliff
+     * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount"
      */
     struct VestingSchedule {
         uint32 cliffAndFlowDate;
         uint32 endDate;
         int96 flowRate;
+
         uint256 cliffAmount;
+
+        uint96 remainderAmount;
+        uint32 claimValidityDate;
+    }
+
+        /**
+     * @dev Parameters used to create vesting schedules
+     * @param superToken SuperToken to be vested
+     * @param receiver Vesting receiver
+     * @param startDate Timestamp when the vesting should start
+     * @param claimValidityDate Date before which the claimable schedule must be claimed
+     * @param cliffDate Timestamp of cliff exectution - if 0, startDate acts as cliff
+     * @param flowRate The flowRate for the stream
+     * @param cliffAmount The amount to be transferred at the cliff
+     * @param endDate The timestamp when the stream should stop.
+     * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount"
+     * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching)
+     */
+    struct ScheduleCreationParams {
+        ISuperToken superToken;
+        address sender;
+        address receiver;
+        uint32 startDate;
+        uint32 claimValidityDate;
+        uint32 cliffDate;
+        int96 flowRate;
+        uint256 cliffAmount;
+        uint32 endDate;
+        uint96 remainderAmount;
     }

     /**
      * @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 claimValidityDate Date before which the claimable schedule must be claimed
      * @param cliffDate Timestamp of the cliff
      * @param flowRate The flowRate for the stream
      * @param endDate The timestamp when the stream should stop
      * @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 cliffAmount,
+        uint32 claimValidityDate,
+        uint96 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 claimValidityDate Date before which the claimable schedule must be claimed
      * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching)
      */
+    function createVestingSchedule(
+        ISuperToken superToken,
+        address receiver,
+        uint32 startDate,
+        uint32 cliffDate,
+        int96 flowRate,
+        uint256 cliffAmount,
+        uint32 endDate,
+        uint32 claimValidityDate,
+        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,
         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,
+        uint32 claimValidityDate
+    ) 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 startDate Timestamp when the vesting should start
+     * @param cliffPeriod The cliff period of the vesting
+     * @param claimPeriod The claim availability period
+     * @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 startDate,
+        uint32 cliffPeriod,
+        uint32 claimPeriod,
+        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 startDate,
+        uint32 cliffPeriod,
+        uint32 claimPeriod
+    ) external;
+
+    /**
+     * @dev Returns all relevant information related to a new vesting schedule creation 
+     * @dev based on the amounts and durations.
+     * @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 startDate Timestamp when the vesting should start
+     * @param cliffPeriod The cliff period of the vesting
+     * @param claimPeriod The claim availability period
+     */
+    function mapCreateVestingScheduleParams(
+        ISuperToken superToken,
+        address sender,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        uint32 startDate,
+        uint32 cliffPeriod,
+        uint32 claimPeriod
+    ) external returns (ScheduleCreationParams memory params);
+
+    /**
+     * @dev Estimates the maximum possible ERC-20 token allowance needed for the vesting schedule 
+     * @dev to work properly under all circumstances.
+     * @param vestingSchedule A vesting schedule (doesn't have to exist)
+     */
+    function getMaximumNeededTokenAllowance(
+        VestingSchedule memory vestingSchedule
+    ) external returns (uint256);
+
+    /**
+     * @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
+        uint32 endDate,
+        uint96 remainderAmount
     );

     /**
      * @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 Emitted when a claimable vesting schedule is claimed
+     * @param superToken The superToken to be vested
+     * @param sender Vesting sender
+     * @param receiver Vesting receiver
+     * @param claimer Account that claimed the vesting (can only be sender or receiver)
+     */
+    event VestingClaimed(
+        ISuperToken indexed superToken,
+        address indexed sender,
+        address indexed receiver,
+        address claimer
+    );
+
     /**
      * @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);
 }

Contracts (VestingScheduler.sol vs VestingSchedulerV2.sol)

diff --git a/./../contracts/VestingScheduler.sol b/./../contracts/VestingSchedulerV2.sol
index 9f6fd0c6..3033dc7c 100644
--- a/./../contracts/VestingScheduler.sol
+++ b/./../contracts/VestingSchedulerV2.sol
@@ -1,257 +1,716 @@
 // 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 { IVestingScheduler } from "./interface/IVestingScheduler.sol";
-
-contract VestingScheduler is IVestingScheduler, SuperAppBase {
+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;

+    struct ScheduleAggregate {
+        ISuperToken superToken;
+        address sender;
+        address receiver;
+        bytes32 id;
+        VestingSchedule schedule;
+    }
+
     constructor(ISuperfluid host) {
         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.registerApp(configWord);
     }

     /// @dev IVestingScheduler.createVestingSchedule implementation.
     function createVestingSchedule(
         ISuperToken superToken,
         address receiver,
         uint32 startDate,
         uint32 cliffDate,
         int96 flowRate,
         uint256 cliffAmount,
         uint32 endDate,
+        uint32 claimValidityDate,
         bytes memory ctx
     ) external returns (bytes memory newCtx) {
         newCtx = ctx;
         address sender = _getSender(ctx);

-        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();
+        _validateAndCreateVestingSchedule(
+            ScheduleCreationParams({
+                superToken: superToken,
+                sender: sender,
+                receiver: receiver,
+                startDate: _normalizeStartDate(startDate),
+                claimValidityDate: claimValidityDate,
+                cliffDate: cliffDate,
+                flowRate: flowRate,
+                cliffAmount: cliffAmount,
+                endDate: endDate,
+                remainderAmount: 0
+            })
+        );
+    }

-        bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver));
-        if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists();
-        vestingSchedules[hashConfig] = VestingSchedule(
-            cliffAndFlowDate,
-            endDate,
-            flowRate,
-            cliffAmount
+    /// @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 = ctx;
+        address sender = _getSender(ctx);
+
+        _validateAndCreateVestingSchedule(
+            ScheduleCreationParams({
+                superToken: superToken,
+                sender: sender,
+                receiver: receiver,
+                startDate: _normalizeStartDate(startDate),
+                claimValidityDate: 0,
+                cliffDate: cliffDate,
+                flowRate: flowRate,
+                cliffAmount: cliffAmount,
+                endDate: endDate,
+                remainderAmount: 0
+            })
         );
+    }
+
+    /// @dev IVestingScheduler.createVestingSchedule implementation.
+    function createVestingSchedule(
+        ISuperToken superToken,
+        address receiver,
+        uint32 startDate,
+        uint32 cliffDate,
+        int96 flowRate,
+        uint256 cliffAmount,
+        uint32 endDate,
+        uint32 claimValidityDate
+    ) external {
+        _validateAndCreateVestingSchedule(
+            ScheduleCreationParams({
+                superToken: superToken,
+                sender: msg.sender,
+                receiver: receiver,
+                startDate: _normalizeStartDate(startDate),
+                claimValidityDate: claimValidityDate,
+                cliffDate: cliffDate,
+                flowRate: flowRate,
+                cliffAmount: cliffAmount,
+                endDate: endDate,
+                remainderAmount: 0
+            })
+        );
+    }
+
+    function _validateAndCreateVestingSchedule(
+        ScheduleCreationParams memory params
+    ) private {
+        // Note: Vesting Scheduler V2 doesn't allow start date to be in the past.
+        // V1 did but didn't allow cliff and flow to be in the past though.
+        if (params.startDate < block.timestamp) revert TimeWindowInvalid();
+        if (params.endDate <= END_DATE_VALID_BEFORE) revert TimeWindowInvalid();
+
+        if (params.receiver == address(0) || params.receiver == params.sender) revert AccountInvalid();
+        if (address(params.superToken) == address(0)) revert ZeroAddress();
+        if (params.flowRate <= 0) revert FlowRateInvalid();
+        if (params.cliffDate != 0 && params.startDate > params.cliffDate) revert TimeWindowInvalid();
+        if (params.cliffDate == 0 && params.cliffAmount != 0) revert CliffInvalid();
+
+        uint32 cliffAndFlowDate = params.cliffDate == 0 ? params.startDate : params.cliffDate;
+        // Note: Vesting Scheduler V2 allows cliff and flow to be in the schedule creation block, V1 didn't.
+        if (cliffAndFlowDate < block.timestamp ||
+            cliffAndFlowDate >= params.endDate ||
+            cliffAndFlowDate + START_DATE_VALID_AFTER >= params.endDate - END_DATE_VALID_BEFORE ||
+            params.endDate - cliffAndFlowDate < MIN_VESTING_DURATION
+        ) revert TimeWindowInvalid();
+
+        // Note : claimable schedule created with a claim validity date equal to 0 is considered regular schedule
+        if (params.claimValidityDate != 0 && params.claimValidityDate < cliffAndFlowDate) 
+            revert TimeWindowInvalid();
+
+        bytes32 id = _getId(address(params.superToken), params.sender, params.receiver);
+        if (vestingSchedules[id].endDate != 0) revert ScheduleAlreadyExists();
+
+        vestingSchedules[id] = VestingSchedule({
+            cliffAndFlowDate: cliffAndFlowDate,
+            endDate: params.endDate,
+            flowRate: params.flowRate,
+            cliffAmount: params.cliffAmount,
+            remainderAmount: params.remainderAmount,
+            claimValidityDate: params.claimValidityDate
+        });

         emit VestingScheduleCreated(
+            params.superToken,
+            params.sender,
+            params.receiver,
+            params.startDate,
+            params.cliffDate,
+            params.flowRate,
+            params.endDate,
+            params.cliffAmount,
+            params.claimValidityDate,
+            params.remainderAmount
+        );
+    }
+
+    /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation.
+    function createVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        uint32 startDate,
+        uint32 cliffPeriod,
+        uint32 claimPeriod,
+        bytes memory ctx
+    ) external returns (bytes memory newCtx) {
+        newCtx = ctx;
+        address sender = _getSender(ctx);
+        
+        _validateAndCreateVestingSchedule(
+            mapCreateVestingScheduleParams(
+                superToken,
+                sender,
+                receiver,
+                totalAmount,
+                totalDuration,
+                _normalizeStartDate(startDate),
+                cliffPeriod,
+                claimPeriod
+            )
+        );
+    }
+
+    /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation.
+    function createVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        uint32 startDate,
+        uint32 cliffPeriod,
+        uint32 claimPeriod
+    ) external {
+        _validateAndCreateVestingSchedule(
+            mapCreateVestingScheduleParams(
+                superToken,
+                msg.sender,
+                receiver,
+                totalAmount,
+                totalDuration,
+                _normalizeStartDate(startDate),
+                cliffPeriod,
+                claimPeriod
+            )
+        );
+    }
+
+    /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration.
+    function createAndExecuteVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        bytes memory ctx
+    ) external returns (bytes memory newCtx) {
+        newCtx = _validateAndCreateAndExecuteVestingScheduleFromAmountAndDuration(
             superToken,
-            sender,
             receiver,
-            startDate,
-            cliffDate,
-            flowRate,
-            endDate,
-            cliffAmount
+            totalAmount,
+            totalDuration,
+            ctx
+        );
+    }
+
+    /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration.
+    function createAndExecuteVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration
+    ) external {
+        _validateAndCreateAndExecuteVestingScheduleFromAmountAndDuration(
+            superToken,
+            receiver,
+            totalAmount,
+            totalDuration,
+            bytes("")
         );
     }

+    /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration.
+    function _validateAndCreateAndExecuteVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        bytes memory ctx
+    ) private returns (bytes memory newCtx) {
+        newCtx = ctx;
+        address sender = _getSender(ctx);
+
+        _validateAndCreateVestingSchedule(
+            mapCreateVestingScheduleParams(
+                superToken,
+                sender,
+                receiver,
+                totalAmount,
+                totalDuration,
+                _normalizeStartDate(0),
+                0, // cliffPeriod
+                0 // claimValidityDate
+            )
+        );
+
+        ScheduleAggregate memory agg = _getVestingScheduleAggregate(superToken, sender, receiver);
+
+        _validateBeforeCliffAndFlow(agg.schedule, /* disableClaimCheck: */ false);
+        assert(_executeCliffAndFlow(agg));
+    }
+
+    /// @dev IVestingScheduler.updateVestingSchedule implementation.
     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];
+        ScheduleAggregate memory agg = _getVestingScheduleAggregate(superToken, sender, receiver);
+        VestingSchedule memory schedule = agg.schedule;

         if (endDate <= block.timestamp) revert TimeWindowInvalid();

+        // Note: Claimable schedules that have not been claimed cannot be updated
+
         // 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[agg.id].endDate = endDate;
+        // Note: Nullify the remainder amount when complexity of updates is introduced.
+        vestingSchedules[agg.id].remainderAmount = 0;
+
         emit VestingScheduleUpdated(
             superToken,
             sender,
             receiver,
             schedule.endDate,
-            endDate
+            endDate,
+            0 // remainderAmount
         );
     }

     /// @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));
+        ScheduleAggregate memory agg = _getVestingScheduleAggregate(superToken, sender, receiver);
+        VestingSchedule memory schedule = agg.schedule;

-        if (vestingSchedules[configHash].endDate != 0) {
-            delete vestingSchedules[configHash];
+        if (schedule.endDate != 0) {
+            delete vestingSchedules[agg.id];
             emit VestingScheduleDeleted(superToken, sender, receiver);
         } else {
             revert ScheduleDoesNotExist();
         }
     }

     /// @dev IVestingScheduler.executeCliffAndFlow implementation.
     function executeCliffAndFlow(
         ISuperToken superToken,
         address sender,
         address receiver
     ) external returns (bool success) {
-        bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver));
-        VestingSchedule memory schedule = vestingSchedules[configHash];
+        ScheduleAggregate memory agg = _getVestingScheduleAggregate(superToken, sender, receiver);
+        VestingSchedule memory schedule = agg.schedule;
+
+        if (schedule.claimValidityDate != 0) {
+            _validateAndClaim(agg);
+            _validateBeforeCliffAndFlow(schedule, /* disableClaimCheck: */ true);
+            if (block.timestamp >= _gteDateToExecuteEndVesting(schedule)) {
+                _validateBeforeEndVesting(schedule, /* disableClaimCheck: */ true);
+                success = _executeVestingAsSingleTransfer(agg);
+            } else {
+                success = _executeCliffAndFlow(agg);
+            }
+        } else {
+            _validateBeforeCliffAndFlow(schedule, /* disableClaimCheck: */ false);
+            success = _executeCliffAndFlow(agg);
+        }
+    }
+
+    function _validateBeforeCliffAndFlow(
+        VestingSchedule memory schedule,
+        bool disableClaimCheck
+    ) private view {
+        if (schedule.cliffAndFlowDate == 0) 
+            revert AlreadyExecuted();
+
+        if (!disableClaimCheck && schedule.claimValidityDate != 0) 
+            revert ScheduleNotClaimed();

+        // Ensure that that the claming date is after the cliff/flow date and before the claim validity date
         if (schedule.cliffAndFlowDate > block.timestamp || 
-            schedule.cliffAndFlowDate + START_DATE_VALID_AFTER < block.timestamp
-        ) revert TimeWindowInvalid();
+            _lteDateToExecuteCliffAndFlow(schedule) < block.timestamp)
+                revert TimeWindowInvalid();
+    }
+
+    function _validateAndClaim(
+        ScheduleAggregate memory agg
+    ) private {
+        VestingSchedule memory schedule = agg.schedule;
+
+        // Ensure that the caller is the sender or the receiver if the vesting schedule requires claiming.
+        if (msg.sender != agg.sender && msg.sender != agg.receiver)
+            revert CannotClaimScheduleOnBehalf();
+
+        if (schedule.claimValidityDate < block.timestamp)
+            revert TimeWindowInvalid();
+        
+        delete vestingSchedules[agg.id].claimValidityDate;
+        emit VestingClaimed(agg.superToken, agg.sender, agg.receiver, msg.sender);
+    }
+
+    /// @dev IVestingScheduler.executeCliffAndFlow implementation.
+    function _executeCliffAndFlow(
+        ScheduleAggregate memory agg
+    ) private returns (bool success) {
+        VestingSchedule memory schedule = agg.schedule;

         // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry.
-        delete vestingSchedules[configHash].cliffAndFlowDate;
-        delete vestingSchedules[configHash].cliffAmount;
+        delete vestingSchedules[agg.id].cliffAndFlowDate;
+        delete vestingSchedules[agg.id].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);
+        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
-            );
+            // Note: Super Tokens revert, not return false, i.e. we expect always true here.
+            assert(
+                agg.superToken.transferFrom(
+                    agg.sender, agg.receiver, schedule.cliffAmount + flowDelayCompensation));
         }
-
         // Create a flow according to the vesting schedule configuration.
-        cfaV1.createFlowByOperator(sender, receiver, superToken, schedule.flowRate);
-
+        cfaV1.createFlowByOperator(agg.sender, agg.receiver, agg.superToken, schedule.flowRate);
         emit VestingCliffAndFlowExecuted(
-            superToken,
-            sender,
-            receiver,
+            agg.superToken,
+            agg.sender,
+            agg.receiver,
             schedule.cliffAndFlowDate,
             schedule.flowRate,
             schedule.cliffAmount,
             flowDelayCompensation
         );

         return true;
     }

+    function _executeVestingAsSingleTransfer(
+        ScheduleAggregate memory agg
+    ) private returns (bool success) {
+        VestingSchedule memory schedule = agg.schedule;
+
+        delete vestingSchedules[agg.id];
+
+        uint256 totalVestedAmount = _getTotalVestedAmount(schedule);
+
+        // Note: Super Tokens revert, not return false, i.e. we expect always true here.
+        assert(agg.superToken.transferFrom(agg.sender, agg.receiver, totalVestedAmount));
+
+        emit VestingCliffAndFlowExecuted(
+            agg.superToken,
+            agg.sender,
+            agg.receiver,
+            schedule.cliffAndFlowDate,
+            0, // flow rate
+            schedule.cliffAmount,
+            totalVestedAmount - schedule.cliffAmount // flow delay compensation
+        );
+
+        emit VestingEndExecuted(
+            agg.superToken,
+            agg.sender,
+            agg.receiver,
+            schedule.endDate,
+            0, // Early end compensation
+            false // Did end fail
+        );
+
+        return true;
+    }
+
+    function _validateBeforeEndVesting(
+        VestingSchedule memory schedule,
+        bool disableClaimCheck
+    ) private view {
+        if (schedule.endDate == 0) 
+            revert AlreadyExecuted();
+
+        if (!disableClaimCheck && schedule.claimValidityDate != 0) 
+            revert ScheduleNotClaimed();
+
+        if (_gteDateToExecuteEndVesting(schedule) > block.timestamp)
+            revert TimeWindowInvalid();
+    }
+
     /// @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];
+    ) external returns (bool success) {
+        ScheduleAggregate memory agg = _getVestingScheduleAggregate(superToken, sender, receiver);
+        VestingSchedule memory schedule = agg.schedule;

-        if (schedule.endDate - END_DATE_VALID_BEFORE > block.timestamp) revert TimeWindowInvalid();
+        _validateBeforeEndVesting(schedule, /* disableClaimCheck: */ false);

         // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry.
-        delete vestingSchedules[configHash];
+        delete vestingSchedules[agg.id];
+
         // If vesting is not running, we can't do anything, just emit failing event.
-        if(_isFlowOngoing(superToken, sender, receiver)) {
+        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) : 0;
-            bool didCompensationFail;
+            uint256 earlyEndCompensation = schedule.endDate >= block.timestamp 
+                ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount
+                : 0;
+
+            // Note: we consider the compensation as failed if the stream is still ongoing after the end date.
+            bool didCompensationFail = schedule.endDate < block.timestamp;
             if (earlyEndCompensation != 0) {
-                // 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;
-                }
+                // Note: Super Tokens revert, not return false, i.e. we expect always true here.
+                assert(superToken.transferFrom(sender, receiver, earlyEndCompensation));
             }

             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
+        address superToken, address sender, address receiver
     ) external view returns (VestingSchedule memory) {
-        return vestingSchedules[keccak256(abi.encodePacked(supertoken, sender, receiver))];
+        return vestingSchedules[_getId(address(superToken), sender, receiver)];
+    }
+
+    function _getVestingScheduleAggregate(
+        ISuperToken superToken, address sender, address receiver
+    ) private view returns (ScheduleAggregate memory) {
+        bytes32 id = _getId(address(superToken), sender, receiver);
+        return ScheduleAggregate({
+            superToken: superToken,
+            sender: sender,
+            receiver: receiver,
+            id: id,
+            schedule: vestingSchedules[id]
+        });
+    }
+
+    function _getId(
+        address superToken, address sender, address receiver
+    ) private pure returns (bytes32) {
+        return keccak256(abi.encodePacked(superToken, sender, receiver));
+    }
+
+    function _normalizeStartDate(uint32 startDate) private view returns (uint32) {
+        // Default to current block timestamp if no start date is provided.
+        if (startDate == 0) {
+            return uint32(block.timestamp);
+        }
+        return startDate;
+    }
+
+    /// @dev IVestingScheduler.mapCreateVestingScheduleParams implementation.
+    function mapCreateVestingScheduleParams(
+        ISuperToken superToken,
+        address sender,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        uint32 startDate,
+        uint32 cliffPeriod,
+        uint32 claimPeriod
+    ) public pure override returns (ScheduleCreationParams memory params) {
+        uint32 claimValidityDate = claimPeriod != 0
+            ? startDate + claimPeriod
+            : 0;
+
+        uint32 endDate = startDate + totalDuration;
+        int96 flowRate = SafeCast.toInt96(
+            SafeCast.toInt256(totalAmount / totalDuration)
+        );
+        uint96 remainderAmount = SafeCast.toUint96(
+            totalAmount - (SafeCast.toUint256(flowRate) * totalDuration)
+        );
+
+        if (cliffPeriod == 0) {
+            params = ScheduleCreationParams({
+                superToken: superToken,
+                sender: sender,
+                receiver: receiver,
+                startDate: startDate,
+                claimValidityDate: claimValidityDate,
+                cliffDate: 0,
+                flowRate: flowRate,
+                cliffAmount: 0,
+                endDate: endDate,
+                remainderAmount: remainderAmount
+            });
+        } else {
+            uint256 cliffAmount = SafeMath.mul(
+                cliffPeriod,
+                SafeCast.toUint256(flowRate)
+            );
+            params = ScheduleCreationParams({
+                superToken: superToken,
+                sender: sender,
+                receiver: receiver,
+                startDate: startDate,
+                claimValidityDate: claimValidityDate,
+                cliffDate: startDate + cliffPeriod,
+                flowRate: flowRate,
+                cliffAmount: cliffAmount,
+                endDate: endDate,
+                remainderAmount: remainderAmount
+            });
+        }
+    }
+
+    /// @dev IVestingScheduler.getMaximumNeededTokenAllowance implementation.
+    function getMaximumNeededTokenAllowance(
+        VestingSchedule memory schedule
+    ) external pure override returns (uint256) {
+        uint256 maxFlowDelayCompensationAmount = 
+            schedule.cliffAndFlowDate == 0 
+                ? 0 
+                : START_DATE_VALID_AFTER * SafeCast.toUint256(schedule.flowRate);
+        uint256 maxEarlyEndCompensationAmount = 
+            schedule.endDate == 0 
+                ? 0 
+                : END_DATE_VALID_BEFORE * SafeCast.toUint256(schedule.flowRate);
+
+        if (schedule.claimValidityDate == 0) {
+            return
+                schedule.cliffAmount +
+                schedule.remainderAmount +
+                maxFlowDelayCompensationAmount +
+                maxEarlyEndCompensationAmount;
+        } else if (schedule.claimValidityDate >= _gteDateToExecuteEndVesting(schedule)) {
+            return _getTotalVestedAmount(schedule);
+        } else {
+            return schedule.cliffAmount +
+                   schedule.remainderAmount +
+                   (schedule.claimValidityDate - schedule.cliffAndFlowDate) * SafeCast.toUint256(schedule.flowRate) +
+                   maxEarlyEndCompensationAmount;
+        }
+    }
+
+    function _getTotalVestedAmount(
+        VestingSchedule memory schedule
+    ) private pure returns (uint256) {
+        return
+            schedule.cliffAmount + 
+            schedule.remainderAmount + 
+            (schedule.endDate - schedule.cliffAndFlowDate) * SafeCast.toUint256(schedule.flowRate);
+    }
+
+    function _lteDateToExecuteCliffAndFlow(
+        VestingSchedule memory schedule
+    ) private pure returns (uint32) {
+        if (schedule.cliffAndFlowDate == 0) 
+            revert AlreadyExecuted();
+
+        if (schedule.claimValidityDate != 0) {
+            return schedule.claimValidityDate;
+        } else {
+            return schedule.cliffAndFlowDate + START_DATE_VALID_AFTER;
+        }
+    }
+
+    function _gteDateToExecuteEndVesting(
+        VestingSchedule memory schedule
+    ) private pure returns (uint32) {
+        if (schedule.endDate == 0)
+            revert AlreadyExecuted();
+
+        return schedule.endDate - END_DATE_VALID_BEFORE;
     }

     /// @dev get sender of transaction from Superfluid Context or transaction itself.
-    function _getSender(bytes memory ctx) internal view returns (address sender) {
+    function _getSender(bytes memory ctx) private 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) {
+    function _isFlowOngoing(ISuperToken superToken, address sender, address receiver) private view returns (bool) {
         (,int96 flowRate,,) = cfaV1.cfa.getFlow(superToken, sender, receiver);
         return flowRate != 0;
     }
 }
github-actions[bot] commented 5 months ago

📦 PR Packages

Install this PR (you need to setup Github packages):

yarn add @superfluid-finance/ethereum-contracts@PR1952
yarn add @superfluid-finance/sdk-core@PR1952
yarn add @superfluid-finance/sdk-redux@PR1952
:octocat: Click to learn how to use Github packages To use the Github package registry, create a token with "read:packages" permission. See [Creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) for help. Next add these lines to your `.npmrc` file, replacing TOKEN with your personal access token. See [Installing a package from Github](https://docs.github.com/en/packages/guides/configuring-npm-for-use-with-github-packages#installing-a-package) if you get stuck. ``` @superfluid-finance:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=TOKEN ```
hellwolf commented 4 months ago

@0xPilou

btw, please setup signed commits:

image

hellwolf commented 4 months ago

Used $ git rebase dev --signoff to fix unsigned commits.

github-actions[bot] commented 4 months ago

XKCD Comic Relif

Link: https://xkcd.com/1952 https://xkcd.com/1952