From d416f905b721842a878d00199c19b050a1bd354a Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Mon, 10 Jun 2024 14:32:31 +0300 Subject: [PATCH] [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 --- .../autowrap/.env-example | 9 - .../autowrap/.env.example | 11 + .../autowrap/hardhat.config.js | 46 +- .../autowrap/package.json | 1 + .../scheduler/.env-example | 9 - .../scheduler/.env.example | 15 + .../scheduler/audit/generate_diffs.sh | 4 + .../scheduler/contracts/FlowScheduler.sol | 4 +- .../scheduler/contracts/VestingScheduler.sol | 2 +- .../contracts/VestingSchedulerV2.sol | 508 +++++++++ .../contracts/interface/IVestingScheduler.sol | 2 +- .../interface/IVestingSchedulerV2.sol | 310 +++++ .../scheduler/deploy/deploy.js | 77 +- .../scheduler/hardhat.config.js | 46 +- .../scheduler/package.json | 3 +- .../scheduler/test/FlowScheduler.t.sol | 2 +- .../test/FlowSchedulerResolver.t.sol | 2 +- .../scheduler/test/VestingScheduler.t.sol | 4 +- .../scheduler/test/VestingSchedulerV2.t.sol | 1011 +++++++++++++++++ 19 files changed, 2009 insertions(+), 57 deletions(-) delete mode 100644 packages/automation-contracts/autowrap/.env-example create mode 100644 packages/automation-contracts/autowrap/.env.example delete mode 100644 packages/automation-contracts/scheduler/.env-example create mode 100644 packages/automation-contracts/scheduler/.env.example create mode 100755 packages/automation-contracts/scheduler/audit/generate_diffs.sh create mode 100644 packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol create mode 100644 packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol create mode 100644 packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol diff --git a/packages/automation-contracts/autowrap/.env-example b/packages/automation-contracts/autowrap/.env-example deleted file mode 100644 index 4b025569de..0000000000 --- a/packages/automation-contracts/autowrap/.env-example +++ /dev/null @@ -1,9 +0,0 @@ -# .env-example - -POLYGON_PRIVATE_KEY= -BSC_PRIVATE_KEY= - -POLYGON_URL= -BSC_URL= - -ETHERSCAN_API_KEY = diff --git a/packages/automation-contracts/autowrap/.env.example b/packages/automation-contracts/autowrap/.env.example new file mode 100644 index 0000000000..6caf6fef41 --- /dev/null +++ b/packages/automation-contracts/autowrap/.env.example @@ -0,0 +1,11 @@ +# .env-example + +PRIVATE_KEY= + +MUMBAI_URL= +POLYGON_URL= +BSC_URL= +OPSEPOLIA_URL= +BASE_URL=https://mainnet.base.org + +ETHERSCAN_API_KEY= \ No newline at end of file diff --git a/packages/automation-contracts/autowrap/hardhat.config.js b/packages/automation-contracts/autowrap/hardhat.config.js index ef1e4d8be5..6d6e85f978 100644 --- a/packages/automation-contracts/autowrap/hardhat.config.js +++ b/packages/automation-contracts/autowrap/hardhat.config.js @@ -18,8 +18,8 @@ module.exports = { optimizer: { enabled: true, runs: 200, - } - } + }, + }, }, networks: { localhost: { @@ -29,15 +29,33 @@ module.exports = { polygon: { url: process.env.POLYGON_URL || "", accounts: - process.env.POLYGON_PRIVATE_KEY !== undefined ? [process.env.POLYGON_PRIVATE_KEY] : [], + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], }, bsc: { url: process.env.BSC_URL || "", accounts: - process.env.BSC_PRIVATE_KEY !== undefined ? [process.env.BSC_PRIVATE_KEY] : [], + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], + }, + opsepolia: { + url: process.env.OPSEPOLIA_URL || "", + accounts: + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], + }, + "base-mainnet": { + url: process.env.BASE_URL || "", + accounts: + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], + gasPrice: 1000000000 }, }, - namedAccounts: { deployer: { default: 0, @@ -45,5 +63,23 @@ module.exports = { }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY, + customChains: [ + { + network: "opsepolia", + chainId: 11155420, + urls: { + apiURL: "https://api-sepolia-optimistic.etherscan.io/api", + browserURL: "https://sepolia-optimism.etherscan.io/", + }, + }, + { + network: "base-mainnet", + chainId: 8453, + urls: { + apiURL: "https://api.basescan.org/api", + browserURL: "https://basescan.org/", + }, + }, + ], }, }; diff --git a/packages/automation-contracts/autowrap/package.json b/packages/automation-contracts/autowrap/package.json index 9696343209..9dad79fb2e 100644 --- a/packages/automation-contracts/autowrap/package.json +++ b/packages/automation-contracts/autowrap/package.json @@ -6,6 +6,7 @@ "scripts": { "test": "forge test", "build": "forge build", + "deploy": "npx hardhat deploy --network", "lint": "run-s lint:*", "lint:sol": "solhint -w 0 contracts/*.sol contracts/*/*.sol && echo '✔ Your .sol files look good.'", "pre-commit": "if [ ! -z \"$(git status -s .)\" ];then run-s pre-commit:*;else true;fi", diff --git a/packages/automation-contracts/scheduler/.env-example b/packages/automation-contracts/scheduler/.env-example deleted file mode 100644 index 4b025569de..0000000000 --- a/packages/automation-contracts/scheduler/.env-example +++ /dev/null @@ -1,9 +0,0 @@ -# .env-example - -POLYGON_PRIVATE_KEY= -BSC_PRIVATE_KEY= - -POLYGON_URL= -BSC_URL= - -ETHERSCAN_API_KEY = diff --git a/packages/automation-contracts/scheduler/.env.example b/packages/automation-contracts/scheduler/.env.example new file mode 100644 index 0000000000..8b21c653d7 --- /dev/null +++ b/packages/automation-contracts/scheduler/.env.example @@ -0,0 +1,15 @@ +# .env-example + +PRIVATE_KEY= + +MUMBAI_URL= +POLYGON_URL= +BSC_URL= +OPSEPOLIA_URL= +BASE_URL=https://mainnet.base.org + +ETHERSCAN_API_KEY= + +DEPLOY_FLOW_SCHEDULER= +DEPLOY_VESTING_SCHEDULER= +DEPLOY_VESTING_SCHEDULER_V2= \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/audit/generate_diffs.sh b/packages/automation-contracts/scheduler/audit/generate_diffs.sh new file mode 100755 index 0000000000..e257ba3be9 --- /dev/null +++ b/packages/automation-contracts/scheduler/audit/generate_diffs.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +git diff -U9999 --no-index --minimal --ignore-cr-at-eol --ignore-space-at-eol ./../contracts/interface/IVestingScheduler.sol ./../contracts/interface/IVestingSchedulerV2.sol > diff_IVestingScheduler_vs_IVestingSchedulerV2.txt +git diff -U9999 --no-index --minimal --ignore-cr-at-eol --ignore-space-at-eol ./../contracts/VestingScheduler.sol ./../contracts/VestingSchedulerV2.sol > diff_VestingScheduler_vs_VestingSchedulerV2.txt \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/FlowScheduler.sol b/packages/automation-contracts/scheduler/contracts/FlowScheduler.sol index 2f7cf24435..263a967ccd 100644 --- a/packages/automation-contracts/scheduler/contracts/FlowScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/FlowScheduler.sol @@ -19,7 +19,7 @@ contract FlowScheduler is IFlowScheduler, SuperAppBase { using CFAv1Library for CFAv1Library.InitData; CFAv1Library.InitData public cfaV1; //initialize cfaV1 variable - constructor(ISuperfluid host, string memory registrationKey) { + constructor(ISuperfluid host) { // Initialize CFA Library cfaV1 = CFAv1Library.InitData( host, @@ -40,7 +40,7 @@ contract FlowScheduler is IFlowScheduler, SuperAppBase { SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; - host.registerAppWithKey(configWord, registrationKey); + host.registerApp(configWord); } /// @dev IFlowScheduler.createFlowSchedule implementation. diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 5bb4ee2e5f..031e0d4ab2 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -254,4 +254,4 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { (,int96 flowRate,,) = cfaV1.cfa.getFlow(superToken, sender, receiver); return flowRate != 0; } -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol new file mode 100644 index 0000000000..072cbe15be --- /dev/null +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -0,0 +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 { IVestingSchedulerV2 } from "./interface/IVestingSchedulerV2.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { + + using CFAv1Library for CFAv1Library.InitData; + CFAv1Library.InitData public cfaV1; + mapping(bytes32 => VestingSchedule) public vestingSchedules; // id = keccak(supertoken, sender, receiver) + + uint32 public constant MIN_VESTING_DURATION = 7 days; + uint32 public constant START_DATE_VALID_AFTER = 3 days; + uint32 public constant END_DATE_VALID_BEFORE = 1 days; + + constructor(ISuperfluid host) { + 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, + 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; + // 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, + remainderAmount + ); + + emit VestingScheduleCreated( + superToken, + sender, + receiver, + startDate, + cliffDate, + flowRate, + endDate, + 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) + 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) { + // 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; + } +} diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index 7e9dc37616..9cb5b20802 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -203,4 +203,4 @@ interface IVestingScheduler { external view returns (VestingSchedule memory); -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol new file mode 100644 index 0000000000..0840d8f819 --- /dev/null +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity ^0.8.0; + +import { + ISuperToken +} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; + +interface IVestingSchedulerV2 { + error TimeWindowInvalid(); + error AccountInvalid(); + error ZeroAddress(); + error HostInvalid(); + error FlowRateInvalid(); + error CliffInvalid(); + error ScheduleAlreadyExists(); + error ScheduleDoesNotExist(); + error ScheduleNotFlowing(); + + /** + * @dev Vesting configuration provided by user. + * @param cliffAndFlowDate Date of flow start and cliff execution (if a cliff was specified) + * @param endDate End date of the vesting + * @param flowRate For the stream + * @param cliffAmount Amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" + */ + struct VestingSchedule { + uint32 cliffAndFlowDate; + uint32 endDate; + int96 flowRate; + uint256 cliffAmount; + uint256 remainderAmount; // TODO: consider packing + } + + /** + * @dev Event emitted on creation of a new vesting schedule + * @param superToken SuperToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting starts + * @param cliffDate Timestamp of the cliff + * @param flowRate The flowRate for the stream + * @param endDate The timestamp when the stream should stop + * @param cliffAmount The amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" + */ + event VestingScheduleCreated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint32 endDate, + uint256 cliffAmount, + uint256 remainderAmount + ); + + /** + * @dev Creates a new vesting schedule + * @dev If a non-zero cliffDate is set, the startDate has no effect other than being logged in an event. + * @dev If cliffDate is set to zero, the startDate becomes the cliff (transfer cliffAmount and start stream). + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting should start + * @param cliffDate Timestamp of cliff exectution - if 0, startDate acts as cliff + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount to be transferred at the cliff + * @param endDate The timestamp when the stream should stop. + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createVestingSchedule overload for more details. + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external; + + /** + * @dev Creates a new vesting schedule + * @dev The function makes it more intuitive to create a vesting schedule compared to the original function. + * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param totalAmount The total amount to be vested + * @param totalDuration The total duration of the vestingß + * @param cliffPeriod The cliff period of the vesting + * @param startDate Timestamp when the vesting should start + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + * Cliff period is not applied. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + + /** + * @dev Creates a new vesting schedule + * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. + * @dev The function creates the vesting schedule with start date set to current timestamp, + * @dev and executes the start (i.e. creation of the flow) immediately. + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param totalAmount The total amount to be vested + * @param totalDuration The total duration of the vestingß + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + */ + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + + /** + * @dev Event emitted on update of a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param oldEndDate Old timestamp when the stream should stop + * @param endDate New timestamp when the stream should stop + */ + event VestingScheduleUpdated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 oldEndDate, + uint32 endDate + ); + + /** + * @dev Updates the end date for a vesting schedule which already reached the cliff + * @notice When updating, there's no restriction to the end date other than not being in the past + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function updateVestingSchedule(ISuperToken superToken, address receiver, uint32 endDate, bytes memory ctx) + external + returns (bytes memory newCtx); + + /** + * @dev Event emitted on deletion of a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + event VestingScheduleDeleted(ISuperToken indexed superToken, address indexed sender, address indexed receiver); + + /** + * @dev Event emitted on end of a vesting that failed because there was no running stream + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + */ + event VestingEndFailed( + ISuperToken indexed superToken, address indexed sender, address indexed receiver, uint32 endDate + ); + + /** + * @dev Deletes a vesting schedule + * @param superToken The superToken to be vested + * @param receiver Vesting receiver + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function deleteVestingSchedule(ISuperToken superToken, address receiver, bytes memory ctx) + external + returns (bytes memory newCtx); + + /** + * @dev Emitted when the cliff of a scheduled vesting is executed + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param cliffAndFlowDate The timestamp when the stream should start + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount you would like to transfer at the startDate when you start streaming + * @param flowDelayCompensation Adjusted amount transferred to receiver. (elapse time from config and tx timestamp) + */ + event VestingCliffAndFlowExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 cliffAndFlowDate, + int96 flowRate, + uint256 cliffAmount, + uint256 flowDelayCompensation + ); + + /** + * @dev Executes a cliff (transfer and stream start) + * @notice Intended to be invoked by a backend service + * @param superToken SuperToken to be streamed + * @param sender Account who will be send the stream + * @param receiver Account who will be receiving the stream + */ + function executeCliffAndFlow(ISuperToken superToken, address sender, address receiver) + external + returns (bool success); + + /** + * @dev Emitted when the end of a scheduled vesting is executed + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + * @param earlyEndCompensation adjusted close amount transferred to receiver. + * @param didCompensationFail adjusted close amount transfer fail. + */ + event VestingEndExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate, + uint256 earlyEndCompensation, + bool didCompensationFail + ); + + /** + * @dev Executes the end of a vesting (stop stream) + * @notice Intended to be invoked by a backend service + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + function executeEndVesting(ISuperToken superToken, address sender, address receiver) + external + returns (bool success); + + /** + * @dev Gets data currently stored for a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + function getVestingSchedule(address superToken, address sender, address receiver) + external + view + returns (VestingSchedule memory); +} diff --git a/packages/automation-contracts/scheduler/deploy/deploy.js b/packages/automation-contracts/scheduler/deploy/deploy.js index 718fd771d9..a094182a33 100644 --- a/packages/automation-contracts/scheduler/deploy/deploy.js +++ b/packages/automation-contracts/scheduler/deploy/deploy.js @@ -15,7 +15,6 @@ module.exports = async function ({ deployments, getNamedAccounts }) { const chainId = await hre.getChainId(); const host = metadata.networks.filter((item) => item.chainId == chainId)[0] .contractsV1.host; - const registrationKey = ""; if (host === undefined) { console.log("Host contract not found for this network"); return; @@ -24,41 +23,79 @@ module.exports = async function ({ deployments, getNamedAccounts }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); + console.log(`network: ${hre.network.name}`); console.log(`chainId: ${chainId}`); console.log(`rpc: ${hre.network.config.url}`); console.log(`host: ${host}`); - const FlowScheduler = await deploy("FlowScheduler", { - from: deployer, - args: [host, registrationKey], - log: true, - skipIfAlreadyDeployed: false, - }); - const VestingScheduler = await deploy("VestingScheduler", { - from: deployer, - args: [host, registrationKey], - log: true, - skipIfAlreadyDeployed: false, - }); + const deployFlowScheduler = process.env.DEPLOY_FLOW_SCHEDULER?.toLowerCase() === "true"; + const deployVestingScheduler = process.env.DEPLOY_VESTING_SCHEDULER?.toLowerCase() === "true"; + const deployVestingSchedulerV2 = process.env.DEPLOY_VESTING_SCHEDULER_V2?.toLowerCase() === "true"; + console.log(`deployFlowScheduler: ${deployFlowScheduler}`); + console.log(`deployVestingScheduler: ${deployVestingScheduler}`); + console.log(`deployVestingSchedulerV2: ${deployVestingSchedulerV2}`); + + if (deployFlowScheduler) { + console.log("Deploying FlowScheduler..."); + const FlowScheduler = await deploy("FlowScheduler", { + from: deployer, + args: [host], + log: true, + skipIfAlreadyDeployed: false + }); - // wait for 15 seconds to allow etherscan to indexed the contracts - await sleep(15000); + // wait for 15 seconds to allow etherscan to indexed the contracts + await sleep(15000); - try { + console.log("Verifying FlowScheduler..."); await hre.run("verify:verify", { address: FlowScheduler.address, - constructorArguments: [host, registrationKey], + constructorArguments: [host], contract: "contracts/FlowScheduler.sol:FlowScheduler", }); + } + + if (deployVestingScheduler) { + console.log("Deploying VestingScheduler..."); + const VestingScheduler = await deploy("VestingScheduler", { + from: deployer, + args: [host], + log: true, + skipIfAlreadyDeployed: false + }); + + // wait for 15 seconds to allow etherscan to indexed the contracts + await sleep(15000); + console.log("Verifying VestingScheduler..."); await hre.run("verify:verify", { address: VestingScheduler.address, - constructorArguments: [host, registrationKey], + constructorArguments: [host], contract: "contracts/VestingScheduler.sol:VestingScheduler", }); - } catch (err) { - console.error(err); } + + if (deployVestingSchedulerV2) { + console.log("Deploying VestingSchedulerV2..."); + const VestingSchedulerV2 = await deploy("VestingSchedulerV2", { + from: deployer, + args: [host], + log: true, + skipIfAlreadyDeployed: false, + }); + + // wait for 15 seconds to allow etherscan to indexed the contracts + await sleep(15000); + + console.log("Verifying VestingSchedulerV2..."); + await hre.run("verify:verify", { + address: VestingSchedulerV2.address, + constructorArguments: [host], + contract: "contracts/VestingSchedulerV2.sol:VestingSchedulerV2", + }); + } + + console.log("Finished."); }; diff --git a/packages/automation-contracts/scheduler/hardhat.config.js b/packages/automation-contracts/scheduler/hardhat.config.js index 93eb9b8e9a..441c84d1c5 100644 --- a/packages/automation-contracts/scheduler/hardhat.config.js +++ b/packages/automation-contracts/scheduler/hardhat.config.js @@ -17,8 +17,8 @@ module.exports = { optimizer: { enabled: true, runs: 200, - } - } + }, + }, }, networks: { localhost: { @@ -28,15 +28,33 @@ module.exports = { polygon: { url: process.env.POLYGON_URL || "", accounts: - process.env.POLYGON_PRIVATE_KEY !== undefined ? [process.env.POLYGON_PRIVATE_KEY] : [], + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], }, bsc: { url: process.env.BSC_URL || "", accounts: - process.env.BSC_PRIVATE_KEY !== undefined ? [process.env.BSC_PRIVATE_KEY] : [], + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], + }, + opsepolia: { + url: process.env.OPSEPOLIA_URL || "", + accounts: + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], + }, + "base-mainnet": { + url: process.env.BASE_URL || "", + accounts: + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], + gasPrice: 1000000000 }, }, - namedAccounts: { deployer: { default: 0, @@ -44,5 +62,23 @@ module.exports = { }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY, + customChains: [ + { + network: "opsepolia", + chainId: 11155420, + urls: { + apiURL: "https://api-sepolia-optimistic.etherscan.io/api", + browserURL: "https://sepolia-optimism.etherscan.io/", + }, + }, + { + network: "base-mainnet", + chainId: 8453, + urls: { + apiURL: "https://api.basescan.org/api", + browserURL: "https://basescan.org/", + }, + }, + ], }, }; diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index f2bd2e15e4..b7b699071c 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/package.json @@ -1,11 +1,12 @@ { "name": "scheduler", - "version": "0.0.1", + "version": "1.2.0", "description": "Open contracts that allow scheduling streams and vestings onchain", "license": "MIT", "scripts": { "test": "forge test", "build": "forge build", + "deploy": "npx hardhat deploy --network", "lint": "run-s lint:*", "lint:sol": "solhint -w 0 contracts/*.sol contracts/*/*.sol && echo '✔ Your .sol files look good.'", "pre-commit": "if [ ! -z \"$(git status -s .)\" ];then run-s pre-commit:*;else true;fi", diff --git a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol index 881bd0dd9c..10c63dabc2 100644 --- a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol @@ -57,7 +57,7 @@ contract FlowSchedulerTest is FoundrySuperfluidTester { function setUp() override public virtual { super.setUp(); - flowScheduler = new FlowScheduler(sf.host, ""); + flowScheduler = new FlowScheduler(sf.host); } function getHashID( diff --git a/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol b/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol index eec76dd1ea..ae98f43848 100644 --- a/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol +++ b/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol @@ -26,7 +26,7 @@ contract FlowSchedulerResolverTest is FoundrySuperfluidTester { function setUp() override public virtual { super.setUp(); - flowScheduler = new FlowScheduler(sf.host, ""); + flowScheduler = new FlowScheduler(sf.host); flowSchedulerResolver = new FlowSchedulerResolver(address(flowScheduler)); createPayload = abi.encodeCall( FlowScheduler.executeCreateFlow, ( diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index c1c1853b21..756523c954 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -79,7 +79,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint256 internal _expectedTotalSupply = 0; constructor() FoundrySuperfluidTester(3) { - vestingScheduler = new VestingScheduler(sf.host, ""); + vestingScheduler = new VestingScheduler(sf.host); } /// SETUP AND HELPERS @@ -561,4 +561,4 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); } -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol new file mode 100644 index 0000000000..695479cd6c --- /dev/null +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -0,0 +1,1011 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; +import { FlowOperatorDefinitions } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import { IVestingSchedulerV2 } from "./../contracts/interface/IVestingSchedulerV2.sol"; +import { VestingSchedulerV2 } from "./../contracts/VestingSchedulerV2.sol"; +import { FoundrySuperfluidTester } from "@superfluid-finance/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol"; +import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "forge-std/console.sol"; + +/// @title VestingSchedulerTests +/// @notice Look at me , I am the captain now - Elvijs +contract VestingSchedulerV2Tests is FoundrySuperfluidTester { + using SuperTokenV1Library for ISuperToken; + + event VestingScheduleCreated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint32 endDate, + uint256 cliffAmount, + uint256 remainderAmount + ); + + event VestingScheduleUpdated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 oldEndDate, + uint32 endDate + ); + + event VestingScheduleDeleted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver + ); + + event VestingCliffAndFlowExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 cliffAndFlowDate, + int96 flowRate, + uint256 cliffAmount, + uint256 flowDelayCompensation + ); + + event VestingEndExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate, + uint256 earlyEndCompensation, + bool didCompensationFail + ); + + event VestingEndFailed( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate + ); + + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev This is required by solidity for using the SuperTokenV1Library in the tester + VestingSchedulerV2 public vestingScheduler; + + /// @dev Constants for Testing + uint256 immutable BLOCK_TIMESTAMP = 100; + uint32 immutable START_DATE = uint32(BLOCK_TIMESTAMP + 1); + uint32 immutable CLIFF_DATE = uint32(BLOCK_TIMESTAMP + 10 days); + int96 constant FLOW_RATE = 1000000000; + uint256 constant CLIFF_TRANSFER_AMOUNT = 1 ether; + uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); + bytes constant EMPTY_CTX = ""; + uint256 internal _expectedTotalSupply = 0; + + constructor() FoundrySuperfluidTester(3) { + vestingScheduler = new VestingSchedulerV2(sf.host); + } + + /// SETUP AND HELPERS + function setUp() override public virtual { + super.setUp(); + vm.warp(BLOCK_TIMESTAMP); + } + + function _setACL_AUTHORIZE_FULL_CONTROL(address user, int96 flowRate) private { + vm.startPrank(user); + sf.host.callAgreement( + sf.cfa, + abi.encodeCall( + sf.cfa.updateFlowOperatorPermissions, + ( + superToken, + address(vestingScheduler), + FlowOperatorDefinitions.AUTHORIZE_FULL_CONTROL, + flowRate, + new bytes(0) + ) + ), + new bytes(0) + ); + vm.stopPrank(); + } + + function _arrangeAllowances(address sender, int96 flowRate) private { + // ## Superfluid ACL allowance and permissions + _setACL_AUTHORIZE_FULL_CONTROL(sender, flowRate); + + // ## ERC-20 allowance for cliff and compensation transfers + vm.startPrank(sender); + superToken.approve(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + } + + function _createVestingScheduleWithDefaultData(address sender, address receiver) private { + vm.startPrank(sender); + vestingScheduler.createVestingSchedule( + superToken, + receiver, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); + } + + /// TESTS + + function testCreateVestingSchedule() public { + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 + ); + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + //assert storage data + VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == CLIFF_DATE , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE , "schedule.endDate"); + assertTrue(schedule.flowRate == FLOW_RATE , "schedule.flowRate"); + assertTrue(schedule.cliffAmount == CLIFF_TRANSFER_AMOUNT , "schedule.cliffAmount"); + } + + function testCannotCreateVestingScheduleWithWrongData() public { + vm.startPrank(alice); + // revert with superToken = 0 + vm.expectRevert(IVestingSchedulerV2.ZeroAddress.selector); + vestingScheduler.createVestingSchedule( + ISuperToken(address(0)), + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = sender + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + alice, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = address(0) + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + address(0), + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with flowRate = 0 + vm.expectRevert(IVestingSchedulerV2.FlowRateInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + 0, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffDate = 0 but cliffAmount != 0 + vm.expectRevert(IVestingSchedulerV2.CliffInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + 0, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with startDate < block.timestamp && cliffDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + uint32(block.timestamp - 1), + 0, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with endDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + 0, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate < block.timestamp + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + uint32(block.timestamp) - 1, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate >= endDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate + startDateValidFor >= endDate - endDateValidBefore + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with startDate > cliffDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + CLIFF_DATE + 1, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + + // revert with vesting duration < 7 days + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE + 2 days, + EMPTY_CTX + ); + } + + function testCannotCreateVestingScheduleIfDataExist() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.expectRevert(IVestingSchedulerV2.ScheduleAlreadyExists.selector); + _createVestingScheduleWithDefaultData(alice, bob); + } + + function testUpdateVestingSchedule() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 + ); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + vm.startPrank(alice); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE + 1000, EMPTY_CTX); + //assert storage data + IVestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == 0 , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE + 1000 , "schedule.endDate"); + } + + function testCannotUpdateVestingScheduleIfNotRunning() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleNotFlowing.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE, EMPTY_CTX); + } + + function testCannotUpdateVestingScheduleIfDataDontExist() public { + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleNotFlowing.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE, EMPTY_CTX); + } + + function testDeleteVestingSchedule() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + vm.expectEmit(true, true, true, true); + emit VestingScheduleDeleted(superToken, alice, bob); + vestingScheduler.deleteVestingSchedule(superToken, bob, EMPTY_CTX); + } + + function testCannotDeleteVestingScheduleIfDataDontExist() public { + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleDoesNotExist.selector); + vestingScheduler.deleteVestingSchedule( + superToken, + bob, + EMPTY_CTX + ); + } + + function testExecuteCliffAndFlowWithCliffAmount() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + emit Transfer(alice, bob, adjustedAmountClosing); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE-CLIFF_DATE) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT ; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowWithoutCliffAmountOrAdjustment() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + vm.startPrank(alice); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + vm.startPrank(admin); + vm.warp(CLIFF_DATE); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, 0, 0 + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.warp(END_DATE); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted(superToken, alice, bob, END_DATE, 0, false); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE-CLIFF_DATE) * uint96(FLOW_RATE); + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowWithUpdatedEndDate() public { + uint32 NEW_END_DATE = END_DATE - 1000; + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit VestingScheduleUpdated(superToken, alice, bob, END_DATE, NEW_END_DATE); + vestingScheduler.updateVestingSchedule(superToken, bob, NEW_END_DATE, EMPTY_CTX); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = NEW_END_DATE > block.timestamp ? NEW_END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + emit Transfer(alice, bob, adjustedAmountClosing); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, NEW_END_DATE, adjustedAmountClosing, false + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (NEW_END_DATE-CLIFF_DATE) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT ; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowRevertClosingTransfer() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.startPrank(alice); + superToken.transferAll(eve); + vm.stopPrank(); + vm.startPrank(admin); + uint256 earlyEndTimestamp = block.timestamp + 10 days - 3600; + vm.warp(earlyEndTimestamp); + + vm.expectRevert(); + vestingScheduler.executeEndVesting(superToken, alice, bob); + + uint256 finalTimestamp = END_DATE + 1; + vm.warp(finalTimestamp); + + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, 0, true + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + } + + function testCannotExecuteEndVestingBeforeTime() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.executeEndVesting(superToken, alice, bob); + } + + function testCannotExecuteCliffAndFlowBeforeTime() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + } + + function testCannotExecuteEndWithoutStreamRunning() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.startPrank(alice); + superToken.deleteFlow(alice, bob); + vm.stopPrank(); + vm.startPrank(admin); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + emit VestingEndFailed( + superToken, alice, bob, END_DATE + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + } + + // # Vesting Scheduler 1.2 tests + + function testCreateAndExecuteImmediately() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + + // # Create schedule + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + uint32 startAndCliffDate = uint32(block.timestamp); + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); + + vestingScheduler.createVestingSchedule( + superToken, + bob, + startAndCliffDate, + startAndCliffDate, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + // --- + + // # Execute start + vm.expectEmit(); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT); + + vm.expectEmit(); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, startAndCliffDate, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, uint256(0) + ); + vm.stopPrank(); + + vm.startPrank(admin); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + + assertTrue(success, "executeVesting should return true"); + // --- + + // # Execute end + uint256 finalTimestamp = END_DATE - 3600; + vm.warp(finalTimestamp); + + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + + vm.expectEmit(); + emit Transfer(alice, bob, adjustedAmountClosing); + + vm.expectEmit(); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + vm.startPrank(admin); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + vm.stopPrank(); + assertTrue(success, "executeCloseVesting should return true"); + + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE - startAndCliffDate) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + // --- + } + + function test_createScheduleFromAmountAndDuration_reverts() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + vm.expectRevert(); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 0, + 1209600, + 604800, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with cliff and start in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 0, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + + console.log("Revert with overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + type(uint256).max, + 1209600, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with underflow/overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + type(uint32).max, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with start date in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 604800, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + } + + function testNewFunctionScheduleCreationWithoutCliff(uint8 randomizer) public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint32 startDate = uint32(block.timestamp); + uint256 totalVestedAmount = 105_840_000; // a value perfectly divisible by a week + uint32 vestingDuration = 1 weeks; + int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + 0, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + 0, + startDate + ); + } + vm.stopPrank(); + } + + function testNewFunctionScheduleCreationWithCliff(uint8 randomizer) public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint32 startDate = uint32(block.timestamp); + uint256 totalVestedAmount = 103_680_000; // a value perfectly divisible + uint32 vestingDuration = 1 weeks + 1 days; + uint32 cliffPeriod = 1 days; + + int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) + uint256 expectedCliffAmount = 12960000; + uint32 expectedCliffDate = startDate + cliffPeriod; + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + cliffPeriod, + startDate + ); + } + vm.stopPrank(); + } + + function test_createScheduleFromAmountAndDuration_executeCliffAndFlow_executeEndVesting( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + uint8 randomizer + ) public { + // Assume + vm.assume(randomizer != 0); + + vm.assume(startDate == 0 || startDate >= block.timestamp); + vm.assume(startDate < 2524600800 /* year 2050 */); + + vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(totalDuration < 18250 days /* 50 years */); + + uint256 beforeSenderBalance = superToken.balanceOf(alice); + uint256 beforeReceiverBalance = superToken.balanceOf(bob); + + vm.assume(totalAmount > 1); + vm.assume(totalAmount >= totalDuration); + vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); + vm.assume(totalAmount <= beforeSenderBalance); + + IVestingSchedulerV2.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); + + // Arrange + IVestingSchedulerV2.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + uint32 expectedCliffDate = cliffPeriod == 0 ? 0 : expectedSchedule.cliffAndFlowDate; + uint32 expectedStartDate = startDate == 0 ? uint32(block.timestamp) : startDate; + + // Assume we're not getting liquidated at the end: + vm.assume(totalAmount <= (beforeSenderBalance - vestingScheduler.END_DATE_VALID_BEFORE() * SafeCast.toUint256(expectedSchedule.flowRate))); + + console.log("Total amount: %s", totalAmount); + console.log("Total duration: %s", totalDuration); + console.log("Cliff period: %s", cliffPeriod); + console.log("Start date: %s", startDate); + console.log("Randomizer: %s", randomizer); + console.log("Expected start date: %s", expectedStartDate); + console.log("Expected cliff date: %s", expectedCliffDate); + console.log("Expected cliff & flow date: %s", expectedSchedule.cliffAndFlowDate); + console.log("Expected end date: %s", expectedSchedule.endDate); + console.log("Expected flow rate: %s", SafeCast.toUint256(expectedSchedule.flowRate)); + console.log("Expected cliff amount: %s", expectedSchedule.cliffAmount); + console.log("Expected remainder amount: %s", expectedSchedule.remainderAmount); + console.log("Sender balance: %s", beforeSenderBalance); + + _arrangeAllowances(alice, expectedSchedule.flowRate); + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, expectedSchedule.remainderAmount); + + // Act + vm.startPrank(alice); + if (startDate == 0 && randomizer % 2 == 0) { + console.log("Using the overload without start date."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod + ); + } else { + if (randomizer % 3 == 0) { + console.log("Using the overload without superfluid context."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + } else { + console.log("Using the overload with superfluid context."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } + } + vm.stopPrank(); + + // Assert + IVestingSchedulerV2.VestingSchedule memory actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, expectedSchedule.cliffAndFlowDate, "schedule created: cliffAndFlowDate not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule created: flowRate not expected"); + assertEq(actualSchedule.cliffAmount, expectedSchedule.cliffAmount, "schedule created: cliffAmount not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule created: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.cliffAndFlowDate + (vestingScheduler.START_DATE_VALID_AFTER() - (vestingScheduler.START_DATE_VALID_AFTER() / randomizer))); + assertTrue(vestingScheduler.executeCliffAndFlow(superToken, alice, bob)); + + // Assert + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule started: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule started: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule started: flowRate not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule started: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule started: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.endDate - (vestingScheduler.END_DATE_VALID_BEFORE() - (vestingScheduler.END_DATE_VALID_BEFORE() / randomizer))); + assertTrue(vestingScheduler.executeEndVesting(superToken, alice, bob)); + + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule ended: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule ended: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, 0, "schedule ended: flowRate not expected"); + assertEq(actualSchedule.endDate, 0, "schedule ended: endDate not expected"); + assertEq(actualSchedule.remainderAmount, 0, "schedule ended: remainderAmount not expected"); + + // Assert + uint256 afterSenderBalance = superToken.balanceOf(alice); + uint256 afterReceiverBalance = superToken.balanceOf(bob); + + assertEq(afterSenderBalance, beforeSenderBalance - totalAmount, "Sender balance should decrease by totalAmount"); + assertEq(afterReceiverBalance, beforeReceiverBalance + totalAmount, "Receiver balance should increase by totalAmount"); + + vm.warp(type(uint32).max); + assertEq(afterSenderBalance, superToken.balanceOf(alice), "After the schedule has ended, the sender's balance should never change."); + } + + function _getExpectedSchedule( + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: 0 + }); + } + + function _getExpectedScheduleFromAmountAndDuration( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + + uint32 cliffDate; + uint32 cliffAndFlowDate; + uint256 cliffAmount; + if (cliffPeriod > 0) { + cliffDate = startDate + cliffPeriod; + cliffAmount = cliffPeriod * SafeCast.toUint256(flowRate); + cliffAndFlowDate = cliffDate; + } else { + cliffDate = 0; + cliffAmount = 0; + cliffAndFlowDate = startDate; + } + + uint32 endDate = startDate + totalDuration; + + uint256 remainderAmount = totalAmount - SafeCast.toUint256(flowRate) * totalDuration; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: remainderAmount + }); + } +}