Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[automations] Vesting Scheduler V2 ergonomic improvements #1904

Merged
merged 26 commits into from
Jun 10, 2024

Conversation

kasparkallas
Copy link
Contributor

@kasparkallas kasparkallas commented Mar 21, 2024

Context

What?
We're cleaning up the vesting scheduler contract with some small fixes and ergonomic improvements based on lessons learned from the VestingScheduler (V1) being in production for a year.

Why?
We have a concrete requirement for a change to enable creation and execution of a vesting schedule in the same block and transaction.

List of Changes

  • 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
  • Add createVestingSchedule overload without bytes memory ctx
  • Add createVestingScheduleFromAmountAndDuration function (with 4 overload variants)
  • Add createAndExecuteVestingScheduleFromAmountAndDuration function (with 2 overload variants) (combines createVestingScheduleFromAmountAndDuration and executeCliffAndFlow)
    • Replace host.registerAppWithKey(configWord, registrationKey); (deprecated) with host.registerApp(configWord); in the constructor
  • Remove string memory registrationKey from constructor parameters (not needed anymore)
  • Default to current block timestamp if start date is not provided (i.e. it's 0).
  • Revert if startDate is below current block timestamp.
  • Allow cliffAndFlowDate to be in the current block timestamp (this enables schedule creation and execution in the same transaction).
  • Remove try-catch from the early end execution (prefer reverting until "early end" is not needed anymore)

TODOs

  • Allow vesting schedule creation in the same block
  • Add function to create vesting schedule from amounts (total amount and duration)
  • Add easy-to-use overloads for better devx (without ctx for example)
  • Add function that combines both creating and execution for minimal devx
  • Remove try-catch to resolve the gas issue
  • Add dust transfer solution
  • Deploy to testnets
  • Implement subgraph
  • Test the front-end
  • Test with the OP merkle contract (https://optimistic.etherscan.io/address/0xfb4d5a94b516df77fbdbcf3cfeb262baaf7d4db7#code)
  • Add 100% coverage test suite (in progress)
  • Configure VALID_BEFORE and VALID_AFTER arguments (?)

Deployments

Superfluid Dashboard PR: https://github.com/superfluid-finance/superfluid-dashboard/pull/732
Subgraph PR: superfluid-finance/automation-subgraphs#6

April 10th

Optimism-Seplia
Contract: https://sepolia-optimism.etherscan.io/address/0x908D8B2A9eDCE5ef2f449e9100b09b8446B9664D#code
Subgraph: https://api.goldsky.com/api/public/project_clsnd6xsoma5j012qepvucfpp/subgraphs/vesting-v1-optimism-sepolia/1.1.0/gn

Polygon-Mumbai
Contract: https://mumbai.polygonscan.com/address/0x445A26833794d8086A758D8BE330a5e563B2D857#code
Subgraph: https://api.goldsky.com/api/public/project_clsnd6xsoma5j012qepvucfpp/subgraphs/vesting-v1-polygon-mumbai/1.1.0/gn

March 28th

Optimism-Sepolia
Contract: https://sepolia-optimism.etherscan.io/address/0xAb6c6b7D7033e0cb8C693ACFd471614313eAE342

Polygon-Mumbai
Contract: https://mumbai.polygonscan.com/address/0x2584A8976911d6d8E9D330C4F7a4D5163329cB25

Comparison of Contracts

Run generate_diffs.sh under /audit to get the diff of the latest changes.

VestingScheduler.sol compared to VestingSchedulerV2.sol

--- a/./../contracts/VestingScheduler.sol
+++ b/./../contracts/VestingSchedulerV2.sol
@@ -1,257 +1,508 @@
 // 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";
+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 VestingScheduler is IVestingScheduler, SuperAppBase {
+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) {
+    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.registerAppWithKey(configWord, registrationKey);
+        host.registerApp(configWord);
     }
 
     /// @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("")
+        );
+    }
+
+    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);
         
+        // Default to current block timestamp if no start date is provided.
+        if (startDate == 0) {
+            startDate = uint32(block.timestamp);
+        }
+
+        // Note: Vesting Scheduler V2 doesn't allow start date to be in the past.
+        // V1 did but didn't allow cliff and flow to be in the past though.
+        if (startDate < block.timestamp) revert TimeWindowInvalid();
+
         if (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 ||
+        // Note: Vesting Scheduler V2 allows cliff and flow to be in the schedule creation block, V1 didn't.
+        if (cliffAndFlowDate < block.timestamp ||
             cliffAndFlowDate >= endDate ||
             cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE ||
             endDate - cliffAndFlowDate < MIN_VESTING_DURATION
         ) revert TimeWindowInvalid();
 
         bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver));
         if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists();
         vestingSchedules[hashConfig] = VestingSchedule(
             cliffAndFlowDate,
             endDate,
             flowRate,
-            cliffAmount
+            cliffAmount,
+            remainderAmount
         );
 
         emit VestingScheduleCreated(
             superToken,
             sender,
             receiver,
             startDate,
             cliffDate,
             flowRate,
             endDate,
-            cliffAmount
+            cliffAmount,
+            remainderAmount
+        );
+    }
+
+        /// @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
+        );
+    }
+
+    function _createVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        address receiver,
+        uint256 totalAmount,
+        uint32 totalDuration,
+        uint32 cliffPeriod,
+        uint32 startDate,
+        bytes memory ctx
+    ) private returns (bytes memory newCtx) {
+        // Default to current block timestamp if no start date is provided.
+        if (startDate == 0) {
+            startDate = uint32(block.timestamp);
+        }
+
+        uint32 endDate = startDate + totalDuration;
+        int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration));
+        uint256 remainderAmount = totalAmount - (SafeCast.toUint256(flowRate) * totalDuration);
+
+        if (cliffPeriod == 0) {
+            newCtx = _createVestingSchedule(
+                superToken, 
+                receiver, 
+                startDate, 
+                0 /* cliffDate */, 
+                flowRate, 
+                0 /* cliffAmount */, 
+                endDate,
+                remainderAmount,
+                ctx
+            );
+        } else {
+            uint32 cliffDate = startDate + cliffPeriod;
+            uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate));
+            newCtx = _createVestingSchedule(
+                superToken, 
+                receiver, 
+                startDate, 
+                cliffDate, 
+                flowRate, 
+                cliffAmount, 
+                endDate, 
+                remainderAmount,
+                ctx
+            );
+        }
+    }
+
+    /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration.
+    function createAndExecuteVestingScheduleFromAmountAndDuration(
+        ISuperToken superToken,
+        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 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;
+        // Note: Nullify the remainder amount when complexity of updates is introduced.
+        vestingSchedules[configHash].remainderAmount = 0;
+
         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) : 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
     ) 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;
     }
 }

IVestingScheduler.sol compared to IVestingSchedulerV2.sol

--- a/./../contracts/interface/IVestingScheduler.sol
+++ b/./../contracts/interface/IVestingSchedulerV2.sol
@@ -1,206 +1,310 @@
 // 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();
 
     /**
      * @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 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);
 }

@kasparkallas kasparkallas self-assigned this Mar 21, 2024
@kasparkallas kasparkallas requested a review from a team as a code owner March 21, 2024 18:07
@kasparkallas kasparkallas marked this pull request as draft March 25, 2024 09:52
* prefer reverting the early end until stream can be closed without needing the transfer (i.e. it will slightly overflow in that case)
* needs proper test cover
* consider the log events
@@ -49,7 +52,8 @@ interface IVestingScheduler {
uint32 cliffDate,
int96 flowRate,
uint32 endDate,
uint256 cliffAmount
uint256 cliffAmount,
uint256 remainderAmount
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Better packing while being cognisant of backwards compatibility.

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch removed, will revert now until "early end" is not possible

try superToken.transferFrom(sender, receiver, earlyEndCompensation)
// solhint-disable-next-line no-empty-blocks
{} catch {
didCompensationFail = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is false now only when "early end" wasn't executed, i.e block.timestamp > schedule.endDate

@kasparkallas kasparkallas changed the title [wip][automations] Vesting Scheduler 1.2 [wip][automations] Vesting Scheduler V2 Apr 10, 2024
@kasparkallas kasparkallas changed the title [wip][automations] Vesting Scheduler V2 [automations] Vesting Scheduler V2 May 17, 2024
@kasparkallas kasparkallas changed the title [automations] Vesting Scheduler V2 [automations] Vesting Scheduler V2 ergonomic fixes Jun 10, 2024
@kasparkallas kasparkallas changed the title [automations] Vesting Scheduler V2 ergonomic fixes [automations] Vesting Scheduler V2 ergonomic improvements Jun 10, 2024
@kasparkallas kasparkallas changed the base branch from dev to feature-set-vesting-scheduler-v2 June 10, 2024 11:30
@kasparkallas kasparkallas marked this pull request as ready for review June 10, 2024 11:31
@kasparkallas kasparkallas merged commit d416f90 into feature-set-vesting-scheduler-v2 Jun 10, 2024
12 of 15 checks passed
@kasparkallas kasparkallas deleted the vesting-scheduler-1_2 branch June 10, 2024 11:32
Copy link

XKCD Comic Relif

Link: https://xkcd.com/1904
https://xkcd.com/1904

@kasparkallas kasparkallas restored the vesting-scheduler-1_2 branch June 10, 2024 11:35
hellwolf pushed a commit that referenced this pull request Jul 23, 2024
* allow creation and execution of the vesting schedule in the current block

* add createVestingSchedule function which works with totalAmount and totalDuration

* add overloads without ctx

* need to improve testing coverage

* add more overloads with fewer parameters

* reorganize the functions

* add create and execute schedule mvp

* work in progress, needs proper testing

* remove try-catch from early end

* prefer reverting the early end until stream can be closed without needing the transfer (i.e. it will slightly overflow in that case)

* add dust amount fix (wip)

* needs proper test cover
* consider the log events

* rename from dustFixAmount to remainderAmount

* add to log as well

* fix test issues

* tiny comment rename

* remove functions create and execute functions with cliff period

* add a comprehensive fuzzed test for createScheduleFromAmountAndDuration

* slightly change end compensation & remainder handling

* use greater or equal handling for case when only remainder needs to be transferred
* assert transferFrom success result
* add todo-s, improve tests

* keep V1 contract, separate V2 explicitly

* update deploy script for v2

* unify deploy scripts

* use newer host.registerApp & unify deploy scripts

- add base-mainnet option

* clean-up

* add diff generation script & completely revert VestingScheduler.sol

Signed-off-by: Miao, ZhiCheng <[email protected]>
github-merge-queue bot pushed a commit that referenced this pull request Jul 23, 2024
* chore: no-op for pull request diff

Signed-off-by: Miao, ZhiCheng <[email protected]>

* [automations] Vesting Scheduler V2 ergonomic improvements (#1904)

* allow creation and execution of the vesting schedule in the current block

* add createVestingSchedule function which works with totalAmount and totalDuration

* add overloads without ctx

* need to improve testing coverage

* add more overloads with fewer parameters

* reorganize the functions

* add create and execute schedule mvp

* work in progress, needs proper testing

* remove try-catch from early end

* prefer reverting the early end until stream can be closed without needing the transfer (i.e. it will slightly overflow in that case)

* add dust amount fix (wip)

* needs proper test cover
* consider the log events

* rename from dustFixAmount to remainderAmount

* add to log as well

* fix test issues

* tiny comment rename

* remove functions create and execute functions with cliff period

* add a comprehensive fuzzed test for createScheduleFromAmountAndDuration

* slightly change end compensation & remainder handling

* use greater or equal handling for case when only remainder needs to be transferred
* assert transferFrom success result
* add todo-s, improve tests

* keep V1 contract, separate V2 explicitly

* update deploy script for v2

* unify deploy scripts

* use newer host.registerApp & unify deploy scripts

- add base-mainnet option

* clean-up

* add diff generation script & completely revert VestingScheduler.sol

Signed-off-by: Miao, ZhiCheng <[email protected]>

* [AUTOMATIONS] Vesting Scheduler - add claimable schedule feature (#1944)

* added claimable vesting feature

* add check on `_executeCliffAndFlow` for claimable schedules

* updated time window condition on schedule claim

* fix typo

* add some unit tests for claiming schedules

* increased test coverage

* added claimValidityDate feature

* updated tests

* add claimValidityDate param to createSchedules function

* updated unit tests

* refactor internal function params (stack too deep) + add claimValidityDate to schedule creation event

* removed `isClaimable` boolean from VestingSchedule data structure

* remove internal function creating dupplication

* updated claim validity date check logic

* refactor: re-order the claimValidityDate in the event

- keep it as one of the last for backwards compatibility

* refactor: rename error

CannotClaimFlowOnBehalf to CannotClaimScheduleOnBehalf

* fix: remove merge issues from hardhat configs

* fix: remove duplication from hardhat config

* fix: moved & rename params struct into VestingSchedulerV2 contract

---------

Co-authored-by: Kaspar Kallas <[email protected]>
Co-authored-by: Kaspar Kallas <[email protected]>
Signed-off-by: Miao, ZhiCheng <[email protected]>

* [AUTOMATIONS] VestingSchdulerV2 improvements (#1963)

* update: change claimValidityDate to claimPeriod in function that takes amount and duration as params

* fix: clear claimValidityDate on claim + add checks to execute* functions

* update: add VestingClaimed event to `_executeCliffAndFlow` function

Signed-off-by: Miao, ZhiCheng <[email protected]>

* [automations] VestingSchedulerV2 add helpful view functions (#1965)

* refactor: use uint96 for remainderAmount & change packing order

* feat: add `getMaximumNeededTokenAllowance` helper function with a test

* refactor: converge on view function usage

* chore: add comments

* chore: clean-up

* chore: reset whitespace

Signed-off-by: Miao, ZhiCheng <[email protected]>

* [AUTOMATIONS] VestingSchedulerV2 claim after end date (#1964)

* update: change claimValidityDate to claimPeriod in function that takes amount and duration as params

* fix: clear claimValidityDate on claim + add checks to execute* functions

* update: add VestingClaimed event to `_executeCliffAndFlow` function

* feature: add capabilities to have claimValidityDate after endDate

* fix: rearrange `_executeCliffAndFlow` logic

* test: increased coverage for executeCliffAndFlow

* test: added revert check on `executeEndVesting` test

* refactor: clean-up

- add additional asserts
- change log event order (to match other situation)

---------

Co-authored-by: Kaspar Kallas <[email protected]>
Signed-off-by: Miao, ZhiCheng <[email protected]>

* fix: check if schedule is claimed on executeEndVesting

Signed-off-by: Miao, ZhiCheng <[email protected]>

* test: increased `getMaximumNeededTokenAllowance` coverage

Signed-off-by: Miao, ZhiCheng <[email protected]>

* feat: added remainderAmount in `VestingScheduleUpdated`

Signed-off-by: Miao, ZhiCheng <[email protected]>

* add tests & fixes

Signed-off-by: Miao, ZhiCheng <[email protected]>

* [automations] Vesting Scheduler V2 refactoring after single-transfer feature (#1969)

* refactor: explicit functions

- _claim
- _exececuteAtSingleTransfer
- _getTotalVestedAmount

* chore: test that schedule is deleted in more places

* refactor: use more foundry bound in tests

* chore: test better the scenario where the schedule is not ended on time

* refactor: refactor to using aggregate object

- make executeCliffAndFlow public

* chore: improve the test further

* refactor: use aggregate in all places

* refactor: re-order some functions based on visibility

* chore: add small comment

* refactor: small whitespace fix

* refactor: use named parameters when using structs

* refactor: remove unnecessary comments

* fix: change type in log event

* chore: test claim event

* chore: add version

Signed-off-by: Miao, ZhiCheng <[email protected]>

* chore: add optimism hardhat config

Signed-off-by: Miao, ZhiCheng <[email protected]>

* refactor: unify `createClaimableVestingSchedule` and `createVestingSchedule` functions

Signed-off-by: Miao, ZhiCheng <[email protected]>

* refactor: reoder `createVestingScheduleFromAmountAndDuration` function params

Signed-off-by: Miao, ZhiCheng <[email protected]>

* refactor: unify `createVestingScheduleFormAmountAndDuration` and `createClaimableVestingScheduleFormAmountAndDuration` functions

Signed-off-by: Miao, ZhiCheng <[email protected]>

* [automations] Vesting Scheduler V2 final clean-up (#1973)

* refactor: unify `createClaimableVestingSchedule` and `createVestingSchedule` functions

* refactor: reoder `createVestingScheduleFromAmountAndDuration` function params

* refactor: unify `createVestingScheduleFormAmountAndDuration` and `createClaimableVestingScheduleFormAmountAndDuration` functions

* refactor: remove confusing overloads

* feat: add cliffPeriod to createAndExecute functions

* unify modifiers & remove a helper function

* refactor: use normalizeStartDate function to get a function to be pure

* refactor: remove unnecessary passing of ctx

* refactor: rename

* add more claim fuzz tests

* change log event semantics slightly for single transfer

* remove version

---------

Co-authored-by: Pilou <[email protected]>
Signed-off-by: Miao, ZhiCheng <[email protected]>

* [automations] Vesting scheduler v2 - fix & v1 compatibility (#1977)

* refactor: unify `createClaimableVestingSchedule` and `createVestingSchedule` functions

* refactor: reoder `createVestingScheduleFromAmountAndDuration` function params

* refactor: unify `createVestingScheduleFormAmountAndDuration` and `createClaimableVestingScheduleFormAmountAndDuration` functions

* chore: add `createVestingSchedule` v1 overload for backward compatibility

* fix: remove `cliffPeriod` parameter in `createAndExecuteVestingScheduleFromAmountAndDuration` function

* refactor: replace `_getSender(bytes(""))` by `msg.sender`

Signed-off-by: Miao, ZhiCheng <[email protected]>

* refactor: re-order functions for better readability

Signed-off-by: Miao, ZhiCheng <[email protected]>

---------

Signed-off-by: Miao, ZhiCheng <[email protected]>
Co-authored-by: Pilou <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant