From e3a10132116d79f59f9a76338f366256fbc3ba0d Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Tue, 19 Mar 2024 14:22:44 +0200 Subject: [PATCH 01/20] allow creation and execution of the vesting schedule in the current block --- .../scheduler/contracts/VestingScheduler.sol | 2 +- .../scheduler/package.json | 2 +- .../scheduler/test/VestingScheduler.t.sol | 76 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 5bb4ee2e5f..c6b0146d61 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -61,7 +61,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - if (cliffAndFlowDate <= block.timestamp || + if (cliffAndFlowDate < block.timestamp || cliffAndFlowDate >= endDate || cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || endDate - cliffAndFlowDate < MIN_VESTING_DURATION diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index cf7c0a98a9..d44361a0f9 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/package.json @@ -1,6 +1,6 @@ { "name": "scheduler", - "version": "0.0.1", + "version": "1.1.0", "description": "Open contracts that allow scheduling streams and vestings onchain", "license": "MIT", "scripts": { diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index c1c1853b21..f7273c7554 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -538,13 +538,17 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vm.startPrank(admin); uint256 initialTimestamp = block.timestamp + 10 days + 1800; vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); emit VestingCliffAndFlowExecuted( superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); assertTrue(success, "executeVesting should return true"); vm.stopPrank(); @@ -561,4 +565,76 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); } + + // # Vesting Scheduler 1.1 tests + function testExecuteCliffAndFlowWithCliffAmountNow() 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); + + 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"); + // --- + } } From 2a4f363bc6d83969c98ad30d17d08fea7550a930 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Thu, 21 Mar 2024 16:41:43 +0200 Subject: [PATCH 02/20] add createVestingSchedule function which works with totalAmount and totalDuration --- .../scheduler/contracts/VestingScheduler.sol | 63 +++++++++ .../contracts/interface/IVestingScheduler.sol | 22 +++ .../scheduler/package.json | 2 +- .../scheduler/test/VestingScheduler.t.sol | 132 +++++++++++++++++- 4 files changed, 216 insertions(+), 3 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index c6b0146d61..ec277d7760 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -7,6 +7,8 @@ import { import { SuperAppBase } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperAppBase.sol"; import { CFAv1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; import { IVestingScheduler } from "./interface/IVestingScheduler.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; contract VestingScheduler is IVestingScheduler, SuperAppBase { @@ -51,6 +53,67 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 endDate, bytes memory ctx ) external returns (bytes memory newCtx) { + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + ctx + ); + } + + /// @dev IVestingScheduler.createVestingSchedule implementation. + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 startDate, + uint32 totalDuration, + uint32 cliffPeriod, + bytes memory ctx + ) external returns (bytes memory newCtx) { + uint32 endDate = startDate + totalDuration; + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + if (cliffPeriod == 0) { + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + 0 /* cliffDate */, + flowRate, + 0 /* cliffAmount */, + endDate, + ctx + ); + } else { + uint32 cliffDate = startDate + cliffPeriod; + uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); // cliffPeriod * flowRate + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + ctx + ); + } + } + + function _createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) private returns (bytes memory newCtx) { newCtx = ctx; address sender = _getSender(ctx); diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index 7e9dc37616..b403546493 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -76,6 +76,28 @@ interface IVestingScheduler { bytes memory ctx ) external returns (bytes memory newCtx); + /** + * @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 startDate Timestamp when the vesting should start + * @param totalDuration The total duration of the vesting + * @param cliffPeriod The cliff period of the vesting + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 startDate, + uint32 totalDuration, + uint32 cliffPeriod, + bytes memory ctx + ) external returns (bytes memory newCtx); + /** * @dev Event emitted on update of a vesting schedule * @param superToken The superToken to be vested diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index d44361a0f9..30059fad0c 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/package.json @@ -1,6 +1,6 @@ { "name": "scheduler", - "version": "1.1.0", + "version": "1.2.0", "description": "Open contracts that allow scheduling streams and vestings onchain", "license": "MIT", "scripts": { diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index f7273c7554..13f277ce2b 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -566,8 +566,9 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { assertTrue(success, "executeCloseVesting should return true"); } - // # Vesting Scheduler 1.1 tests - function testExecuteCliffAndFlowWithCliffAmountNow() public { + // # Vesting Scheduler 1.2 tests + + function testCreateAndExecuteImmediately() public { uint256 aliceInitialBalance = superToken.balanceOf(alice); uint256 bobInitialBalance = superToken.balanceOf(bob); @@ -637,4 +638,131 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); // --- } + + function testReverts_wip() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + // 0 flow rate + vm.expectRevert(IVestingScheduler.FlowRateInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + uint32(block.timestamp), + 1209600, + 604800, + EMPTY_CTX + ); + + // cliff and start in history + vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 1 ether, + uint32(block.timestamp - 1), + 1209600, + 0, + EMPTY_CTX + ); + + // overflow + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingSchedule( + superToken, + bob, + type(uint256).max, + uint32(block.timestamp), + 1209600, + 0, + EMPTY_CTX + ); + + // overflow/underflow + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingSchedule( + superToken, + bob, + 1 ether, + uint32(block.timestamp), + type(uint32).max, + 0, + EMPTY_CTX + ); + + // todo + // hmm, start date can be in history? + vestingScheduler.createVestingSchedule( + superToken, + bob, + 1 ether, + uint32(block.timestamp - 1), + 1209600, + 604800, + EMPTY_CTX + ); + } + + function testNewFunctionScheduleCreationWithoutCliff() 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); + + vm.startPrank(alice); + vestingScheduler.createVestingSchedule( + superToken, + bob, + totalVestedAmount, + startDate, + vestingDuration, + 0, + EMPTY_CTX + ); + vm.stopPrank(); + } + + function testNewFunctionScheduleCreationWithCliff() 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); + + vm.startPrank(alice); + vestingScheduler.createVestingSchedule( + superToken, + bob, + totalVestedAmount, + startDate, + vestingDuration, + cliffPeriod, + EMPTY_CTX + ); + vm.stopPrank(); + } } From 552a3dfaa66f11abf9195b287673045b2798bf01 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Thu, 21 Mar 2024 19:22:31 +0200 Subject: [PATCH 03/20] add overloads without ctx * need to improve testing coverage --- .../scheduler/contracts/VestingScheduler.sol | 132 +++++++++++++----- .../contracts/interface/IVestingScheduler.sol | 33 ++++- .../scheduler/test/VestingScheduler.t.sol | 84 +++++++---- 3 files changed, 180 insertions(+), 69 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index ec277d7760..a5c7d0b8dc 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -50,10 +50,9 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 cliffDate, int96 flowRate, uint256 cliffAmount, - uint32 endDate, - bytes memory ctx - ) external returns (bytes memory newCtx) { - newCtx = _createVestingSchedule( + uint32 endDate + ) external { + _createVestingSchedule( superToken, receiver, startDate, @@ -61,7 +60,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, - ctx + bytes("") ); } @@ -69,39 +68,23 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { function createVestingSchedule( ISuperToken superToken, address receiver, - uint256 totalAmount, uint32 startDate, - uint32 totalDuration, - uint32 cliffPeriod, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, bytes memory ctx ) external returns (bytes memory newCtx) { - uint32 endDate = startDate + totalDuration; - int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); - if (cliffPeriod == 0) { - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - 0 /* cliffDate */, - flowRate, - 0 /* cliffAmount */, - endDate, - ctx - ); - } else { - uint32 cliffDate = startDate + cliffPeriod; - uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); // cliffPeriod * flowRate - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - ctx - ); - } + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + ctx + ); } function _createVestingSchedule( @@ -151,6 +134,85 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ); } + /// @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, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + ctx + ); + } + + function _createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) private returns (bytes memory newCtx) { + uint32 endDate = startDate + totalDuration; + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + if (cliffPeriod == 0) { + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + 0 /* cliffDate */, + flowRate, + 0 /* cliffAmount */, + endDate, + ctx + ); + } else { + uint32 cliffDate = startDate + cliffPeriod; + uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); // cliffPeriod * flowRate + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + ctx + ); + } + } + function updateVestingSchedule( ISuperToken superToken, address receiver, diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index b403546493..96fececaba 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -63,6 +63,19 @@ interface IVestingScheduler { * @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. + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external; + + /** + * @dev See IVestingScheduler.createVestingSchedule overload for more details. * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) */ function createVestingSchedule( @@ -83,18 +96,30 @@ interface IVestingScheduler { * @param superToken SuperToken to be vested * @param receiver Vesting receiver * @param totalAmount The total amount to be vested - * @param startDate Timestamp when the vesting should start - * @param totalDuration The total duration of the vesting + * @param totalDuration The total duration of the vestingß * @param cliffPeriod The cliff period of the vesting + * @param startDate Timestamp when the vesting should start + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) */ - function createVestingSchedule( + function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, uint256 totalAmount, - uint32 startDate, uint32 totalDuration, uint32 cliffPeriod, + uint32 startDate, bytes memory ctx ) external returns (bytes memory newCtx); diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index 13f277ce2b..e2ab4153f2 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -645,66 +645,66 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { // 0 flow rate vm.expectRevert(IVestingScheduler.FlowRateInvalid.selector); - vestingScheduler.createVestingSchedule( + vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, 0, - uint32(block.timestamp), 1209600, 604800, + uint32(block.timestamp), EMPTY_CTX ); // cliff and start in history vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - vestingScheduler.createVestingSchedule( + vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, 1 ether, - uint32(block.timestamp - 1), 1209600, 0, + uint32(block.timestamp - 1), EMPTY_CTX ); // overflow vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingSchedule( + vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, type(uint256).max, - uint32(block.timestamp), 1209600, 0, + uint32(block.timestamp), EMPTY_CTX ); // overflow/underflow vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingSchedule( + vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, 1 ether, - uint32(block.timestamp), type(uint32).max, 0, + uint32(block.timestamp), EMPTY_CTX ); // todo // hmm, start date can be in history? - vestingScheduler.createVestingSchedule( + vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, 1 ether, - uint32(block.timestamp - 1), 1209600, 604800, + uint32(block.timestamp - 1), EMPTY_CTX ); } - function testNewFunctionScheduleCreationWithoutCliff() public { + function testNewFunctionScheduleCreationWithoutCliff(uint8 randomizer) public { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); vm.startPrank(alice); @@ -721,19 +721,31 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0); vm.startPrank(alice); - vestingScheduler.createVestingSchedule( - superToken, - bob, - totalVestedAmount, - startDate, - vestingDuration, - 0, - EMPTY_CTX - ); + 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() public { + function testNewFunctionScheduleCreationWithCliff(uint8 randomizer) public { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); vm.startPrank(alice); @@ -754,15 +766,27 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount); vm.startPrank(alice); - vestingScheduler.createVestingSchedule( - superToken, - bob, - totalVestedAmount, - startDate, - vestingDuration, - cliffPeriod, - EMPTY_CTX - ); + 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(); } } From ad7ce5885e8a082b5f4f0aa6a880a998587f621c Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Thu, 21 Mar 2024 19:57:55 +0200 Subject: [PATCH 04/20] add more overloads with fewer parameters --- .../scheduler/contracts/VestingScheduler.sol | 35 +++++++++++++++++++ .../contracts/interface/IVestingScheduler.sol | 25 +++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index a5c7d0b8dc..972673c84f 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -134,6 +134,41 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ); } + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + 0, + uint32(block.timestamp), + bytes("") + ); + } + + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + uint32(block.timestamp), + bytes("") + ); + } + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index 96fececaba..ed95b127b2 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -109,6 +109,31 @@ interface IVestingScheduler { 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. + * No cliff period is applied. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + /** * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) From b9ee878a33eaed7f87971b9c12acd4370e083b95 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Fri, 22 Mar 2024 18:41:36 +0200 Subject: [PATCH 05/20] reorganize the functions --- .../scheduler/contracts/VestingScheduler.sol | 150 +++++++++--------- .../contracts/interface/IVestingScheduler.sol | 37 +++-- 2 files changed, 94 insertions(+), 93 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 972673c84f..67a0d61076 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -42,28 +42,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { host.registerAppWithKey(configWord, registrationKey); } - /// @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, - bytes("") - ); - } - /// @dev IVestingScheduler.createVestingSchedule implementation. function createVestingSchedule( ISuperToken superToken, @@ -87,76 +65,57 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ); } - function _createVestingSchedule( + /// @dev IVestingScheduler.createVestingSchedule implementation. + function createVestingSchedule( ISuperToken superToken, address receiver, uint32 startDate, uint32 cliffDate, int96 flowRate, uint256 cliffAmount, - uint32 endDate, - bytes memory ctx - ) private returns (bytes memory newCtx) { - newCtx = ctx; - address sender = _getSender(ctx); - - if (receiver == address(0) || receiver == sender) revert AccountInvalid(); - if (address(superToken) == address(0)) revert ZeroAddress(); - if (flowRate <= 0) revert FlowRateInvalid(); - if (cliffDate != 0 && startDate > cliffDate) revert TimeWindowInvalid(); - if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); - - uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - if (cliffAndFlowDate < block.timestamp || - cliffAndFlowDate >= endDate || - cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || - endDate - cliffAndFlowDate < MIN_VESTING_DURATION - ) revert TimeWindowInvalid(); - - bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver)); - if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists(); - vestingSchedules[hashConfig] = VestingSchedule( - cliffAndFlowDate, - endDate, - flowRate, - cliffAmount - ); - - emit VestingScheduleCreated( + uint32 endDate + ) external { + _createVestingSchedule( superToken, - sender, receiver, startDate, cliffDate, flowRate, + cliffAmount, endDate, - cliffAmount + bytes("") ); } + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, uint256 totalAmount, - uint32 totalDuration - ) external { - _createVestingScheduleFromAmountAndDuration( + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( superToken, receiver, totalAmount, totalDuration, - 0, - uint32(block.timestamp), - bytes("") + cliffPeriod, + startDate, + ctx ); } + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, uint256 totalAmount, uint32 totalDuration, - uint32 cliffPeriod + uint32 cliffPeriod, + uint32 startDate ) external { _createVestingScheduleFromAmountAndDuration( superToken, @@ -164,7 +123,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { totalAmount, totalDuration, cliffPeriod, - uint32(block.timestamp), + startDate, bytes("") ); } @@ -175,8 +134,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { address receiver, uint256 totalAmount, uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate + uint32 cliffPeriod ) external { _createVestingScheduleFromAmountAndDuration( superToken, @@ -184,7 +142,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { totalAmount, totalDuration, cliffPeriod, - startDate, + uint32(block.timestamp), bytes("") ); } @@ -194,19 +152,16 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ISuperToken superToken, address receiver, uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) external returns (bytes memory newCtx) { - newCtx = _createVestingScheduleFromAmountAndDuration( + uint32 totalDuration + ) external { + _createVestingScheduleFromAmountAndDuration( superToken, receiver, totalAmount, totalDuration, - cliffPeriod, - startDate, - ctx + 0, + uint32(block.timestamp), + bytes("") ); } @@ -248,6 +203,53 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { } } + function _createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + + if (receiver == address(0) || receiver == sender) revert AccountInvalid(); + if (address(superToken) == address(0)) revert ZeroAddress(); + if (flowRate <= 0) revert FlowRateInvalid(); + if (cliffDate != 0 && startDate > cliffDate) revert TimeWindowInvalid(); + if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + if (cliffAndFlowDate < block.timestamp || + cliffAndFlowDate >= endDate || + cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || + endDate - cliffAndFlowDate < MIN_VESTING_DURATION + ) revert TimeWindowInvalid(); + + bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver)); + if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists(); + vestingSchedules[hashConfig] = VestingSchedule( + cliffAndFlowDate, + endDate, + flowRate, + cliffAmount + ); + + emit VestingScheduleCreated( + superToken, + sender, + receiver, + startDate, + cliffDate, + flowRate, + endDate, + cliffAmount + ); + } + function updateVestingSchedule( ISuperToken superToken, address receiver, diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index ed95b127b2..fae8af5638 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -63,6 +63,7 @@ interface IVestingScheduler { * @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, @@ -71,12 +72,12 @@ interface IVestingScheduler { uint32 cliffDate, int96 flowRate, uint256 cliffAmount, - uint32 endDate - ) external; + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx); /** * @dev See IVestingScheduler.createVestingSchedule overload for more details. - * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) */ function createVestingSchedule( ISuperToken superToken, @@ -85,9 +86,8 @@ interface IVestingScheduler { uint32 cliffDate, int96 flowRate, uint256 cliffAmount, - uint32 endDate, - bytes memory ctx - ) external returns (bytes memory newCtx); + uint32 endDate + ) external; /** * @dev Creates a new vesting schedule @@ -99,6 +99,7 @@ interface IVestingScheduler { * @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, @@ -106,47 +107,45 @@ interface IVestingScheduler { uint256 totalAmount, uint32 totalDuration, uint32 cliffPeriod, - uint32 startDate - ) external; - + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx); /** * @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 + uint32 cliffPeriod, + uint32 startDate ) external; /** * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. * The startDate is set to current block timestamp. - * No cliff period is applied. */ function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, uint256 totalAmount, - uint32 totalDuration + uint32 totalDuration, + uint32 cliffPeriod ) external; /** * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + * The startDate is set to current block timestamp. + * Cliff period is not applied. */ function createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) external returns (bytes memory newCtx); + uint32 totalDuration + ) external; /** * @dev Event emitted on update of a vesting schedule From 89a173a71f5858197fbac33f5b4215de5ae757f6 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Fri, 22 Mar 2024 20:19:52 +0200 Subject: [PATCH 06/20] add create and execute schedule mvp * work in progress, needs proper testing --- .../scheduler/contracts/VestingScheduler.sol | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 67a0d61076..01892a55b3 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -165,6 +165,83 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ); } + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + ctx + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external { + _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + bytes("") + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external { + _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + 0, + bytes("") + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function _createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + uint32(block.timestamp), + ctx + ); + + address sender = _getSender(ctx); + assert(_executeCliffAndFlow(superToken, sender, receiver)); + } + function _createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, @@ -300,6 +377,15 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { address sender, address receiver ) external returns (bool success) { + return _executeCliffAndFlow(superToken, sender, receiver); + } + + /// @dev IVestingScheduler.executeCliffAndFlow implementation. + function _executeCliffAndFlow( + ISuperToken superToken, + address sender, + address receiver + ) private returns (bool success) { bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); VestingSchedule memory schedule = vestingSchedules[configHash]; @@ -339,6 +425,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { return true; } + /// @dev IVestingScheduler.executeEndVesting implementation. function executeEndVesting( ISuperToken superToken, From b9464fb4cd1b0d5ac36d97eaaef51b5749e3e075 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Tue, 26 Mar 2024 10:32:07 +0200 Subject: [PATCH 07/20] 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) --- .../scheduler/contracts/VestingScheduler.sol | 10 ++-------- .../scheduler/test/VestingScheduler.t.sol | 13 +++++++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 01892a55b3..bcd80cab9c 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -446,15 +446,9 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint256 earlyEndCompensation = schedule.endDate > block.timestamp ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) : 0; - bool didCompensationFail; + 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; - } + superToken.transferFrom(sender, receiver, earlyEndCompensation); } emit VestingEndExecuted( diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index e2ab4153f2..0069f46cc1 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -498,13 +498,18 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { superToken.transferAll(eve); vm.stopPrank(); vm.startPrank(admin); - uint256 finalTimestamp = block.timestamp + 10 days - 3600; + 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); - uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; - uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); emit VestingEndExecuted( - superToken, alice, bob, END_DATE, adjustedAmountClosing, true + superToken, alice, bob, END_DATE, 0, true ); success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); From 648c5bf5c990d5d86ee2e2d4fb1edc470bf7cbc8 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Tue, 26 Mar 2024 14:39:06 +0200 Subject: [PATCH 08/20] add dust amount fix (wip) * needs proper test cover * consider the log events --- .../scheduler/contracts/VestingScheduler.sol | 16 +++++++++++++--- .../contracts/interface/IVestingScheduler.sol | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index bcd80cab9c..2431035088 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -61,6 +61,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, + 0, // dustAmount ctx ); } @@ -83,6 +84,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, + 0, // dustAmount bytes("") ); } @@ -253,6 +255,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ) private returns (bytes memory newCtx) { uint32 endDate = startDate + totalDuration; int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + uint256 dustAmount = totalAmount - (totalAmount / totalDuration * totalDuration); if (cliffPeriod == 0) { newCtx = _createVestingSchedule( superToken, @@ -261,7 +264,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { 0 /* cliffDate */, flowRate, 0 /* cliffAmount */, - endDate, + endDate, + dustAmount, ctx ); } else { @@ -275,6 +279,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, + dustAmount, ctx ); } @@ -288,6 +293,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { int96 flowRate, uint256 cliffAmount, uint32 endDate, + uint256 dustFixAmount, bytes memory ctx ) private returns (bytes memory newCtx) { newCtx = ctx; @@ -312,7 +318,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cliffAndFlowDate, endDate, flowRate, - cliffAmount + cliffAmount, + dustFixAmount ); emit VestingScheduleCreated( @@ -324,6 +331,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, endDate, cliffAmount + // todo: dust amount ); } @@ -344,6 +352,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); vestingSchedules[configHash].endDate = endDate; + // Note: Nullify the dust amount if complexity of updates is introduced. + vestingSchedules[configHash].dustFixAmount = 0; emit VestingScheduleUpdated( superToken, sender, @@ -445,7 +455,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cfaV1.deleteFlowByOperator(sender, receiver, superToken); uint256 earlyEndCompensation = schedule.endDate > block.timestamp ? - (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) : 0; + (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.dustFixAmount : 0; bool didCompensationFail = schedule.endDate < block.timestamp; if (earlyEndCompensation != 0) { superToken.transferFrom(sender, receiver, earlyEndCompensation); diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index fae8af5638..990ca4972d 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -28,6 +28,7 @@ interface IVestingScheduler { uint32 endDate; int96 flowRate; uint256 cliffAmount; + uint256 dustFixAmount; } /** From 59832dc362fc8f54be441c801e40ec0846fd47f3 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 27 Mar 2024 00:35:32 +0200 Subject: [PATCH 09/20] rename from dustFixAmount to remainderAmount * add to log as well --- .../scheduler/contracts/VestingScheduler.sol | 18 +++++++++--------- .../contracts/interface/IVestingScheduler.sol | 7 +++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 2431035088..c610304771 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -255,7 +255,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ) private returns (bytes memory newCtx) { uint32 endDate = startDate + totalDuration; int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); - uint256 dustAmount = totalAmount - (totalAmount / totalDuration * totalDuration); + uint256 remainderAmount = totalAmount % SafeCast.toUint256(flowRate); if (cliffPeriod == 0) { newCtx = _createVestingSchedule( superToken, @@ -265,7 +265,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, 0 /* cliffAmount */, endDate, - dustAmount, + remainderAmount, ctx ); } else { @@ -279,7 +279,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, - dustAmount, + remainderAmount, ctx ); } @@ -293,7 +293,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { int96 flowRate, uint256 cliffAmount, uint32 endDate, - uint256 dustFixAmount, + uint256 remainderAmount, bytes memory ctx ) private returns (bytes memory newCtx) { newCtx = ctx; @@ -319,7 +319,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { endDate, flowRate, cliffAmount, - dustFixAmount + remainderAmount ); emit VestingScheduleCreated( @@ -330,8 +330,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cliffDate, flowRate, endDate, - cliffAmount - // todo: dust amount + cliffAmount, + remainderAmount ); } @@ -352,8 +352,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); vestingSchedules[configHash].endDate = endDate; + vestingSchedules[configHash].remainderAmount = 0; // Note: Nullify the dust amount if complexity of updates is introduced. - vestingSchedules[configHash].dustFixAmount = 0; emit VestingScheduleUpdated( superToken, sender, @@ -455,7 +455,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cfaV1.deleteFlowByOperator(sender, receiver, superToken); uint256 earlyEndCompensation = schedule.endDate > block.timestamp ? - (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.dustFixAmount : 0; + (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount : 0; bool didCompensationFail = schedule.endDate < block.timestamp; if (earlyEndCompensation != 0) { superToken.transferFrom(sender, receiver, earlyEndCompensation); diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index 990ca4972d..776bbf5b7f 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -22,13 +22,14 @@ interface IVestingScheduler { * @param endDate End date of the vesting * @param flowRate For the stream * @param cliffAmount Amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" */ struct VestingSchedule { uint32 cliffAndFlowDate; uint32 endDate; int96 flowRate; uint256 cliffAmount; - uint256 dustFixAmount; + uint256 remainderAmount; } /** @@ -41,6 +42,7 @@ interface IVestingScheduler { * @param flowRate The flowRate for the stream * @param endDate The timestamp when the stream should stop * @param cliffAmount The amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" */ event VestingScheduleCreated( ISuperToken indexed superToken, @@ -50,7 +52,8 @@ interface IVestingScheduler { uint32 cliffDate, int96 flowRate, uint32 endDate, - uint256 cliffAmount + uint256 cliffAmount, + uint256 remainderAmount ); /** From 96d06fa08fa4f6c810a8ed13a236093d30a84f42 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 27 Mar 2024 16:46:32 +0200 Subject: [PATCH 10/20] fix test issues --- .../scheduler/test/VestingScheduler.t.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index 0069f46cc1..f3e290aa19 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -21,7 +21,8 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint32 cliffDate, int96 flowRate, uint32 endDate, - uint256 cliffAmount + uint256 cliffAmount, + uint256 remainderAmount ); event VestingScheduleUpdated( @@ -126,7 +127,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { function testCreateVestingSchedule() public { vm.expectEmit(true, true, true, true); emit VestingScheduleCreated( - superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 ); _createVestingScheduleWithDefaultData(alice, bob); vm.startPrank(alice); @@ -308,7 +309,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); vm.expectEmit(true, true, true, true); emit VestingScheduleCreated( - superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 ); _createVestingScheduleWithDefaultData(alice, bob); vm.prank(alice); @@ -586,7 +587,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint32 startAndCliffDate = uint32(block.timestamp); vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT); + emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); vestingScheduler.createVestingSchedule( superToken, @@ -648,8 +649,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - // 0 flow rate - vm.expectRevert(IVestingScheduler.FlowRateInvalid.selector); + vm.expectRevert(); vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, @@ -723,7 +723,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint32 expectedEndDate = startDate + vestingDuration; vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0); + emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); vm.startPrank(alice); bool useCtx = randomizer % 2 == 0; @@ -768,7 +768,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint32 expectedEndDate = startDate + vestingDuration; vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount); + emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); vm.startPrank(alice); bool useCtx = randomizer % 2 == 0; From 9cce7cf248eafce2f9d03a84080d7bf4113403a0 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 27 Mar 2024 16:56:31 +0200 Subject: [PATCH 11/20] tiny comment rename --- .../scheduler/contracts/VestingScheduler.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index c610304771..d316bd1de0 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -61,7 +61,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, - 0, // dustAmount + 0, // remainderAmount ctx ); } @@ -84,7 +84,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { flowRate, cliffAmount, endDate, - 0, // dustAmount + 0, // remainderAmount bytes("") ); } From 3c3c09cf6e83f80db02133936eeda692c05d60db Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Mon, 1 Apr 2024 23:04:45 +0300 Subject: [PATCH 12/20] remove functions create and execute functions with cliff period --- .../scheduler/contracts/VestingScheduler.sol | 24 +-------------- .../contracts/interface/IVestingScheduler.sol | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index d316bd1de0..1a8707ba95 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -173,7 +173,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { address receiver, uint256 totalAmount, uint32 totalDuration, - uint32 cliffPeriod, bytes memory ctx ) external returns (bytes memory newCtx) { newCtx = _createAndExecuteVestingScheduleFromAmountAndDuration( @@ -181,29 +180,10 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { receiver, totalAmount, totalDuration, - cliffPeriod, ctx ); } - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod - ) external { - _createAndExecuteVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - bytes("") - ); - } - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. function createAndExecuteVestingScheduleFromAmountAndDuration( ISuperToken superToken, @@ -216,7 +196,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { receiver, totalAmount, totalDuration, - 0, bytes("") ); } @@ -227,7 +206,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { address receiver, uint256 totalAmount, uint32 totalDuration, - uint32 cliffPeriod, bytes memory ctx ) private returns (bytes memory newCtx) { newCtx = _createVestingScheduleFromAmountAndDuration( @@ -235,7 +213,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { receiver, totalAmount, totalDuration, - cliffPeriod, + 0, uint32(block.timestamp), ctx ); diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index 776bbf5b7f..b7c2712b94 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -151,6 +151,35 @@ interface IVestingScheduler { 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 From 78dc300961a32fbe9b55b01b78ffa2c37f15ca12 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Tue, 2 Apr 2024 17:50:09 +0300 Subject: [PATCH 13/20] add a comprehensive fuzzed test for createScheduleFromAmountAndDuration --- .../scheduler/contracts/VestingScheduler.sol | 22 +- .../scheduler/test/VestingScheduler.t.sol | 339 ++++++++++++++---- 2 files changed, 284 insertions(+), 77 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 1a8707ba95..f0485fde4b 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -144,7 +144,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { totalAmount, totalDuration, cliffPeriod, - uint32(block.timestamp), + 0, // startDate bytes("") ); } @@ -161,8 +161,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { receiver, totalAmount, totalDuration, - 0, - uint32(block.timestamp), + 0, // cliffPeriod + 0, // startDate bytes("") ); } @@ -213,8 +213,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { receiver, totalAmount, totalDuration, - 0, - uint32(block.timestamp), + 0, // cliffPeriod + 0, // startDate ctx ); @@ -231,9 +231,14 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 startDate, bytes memory ctx ) private returns (bytes memory newCtx) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + uint32 endDate = startDate + totalDuration; int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); - uint256 remainderAmount = totalAmount % SafeCast.toUint256(flowRate); + uint256 remainderAmount = totalAmount - (SafeCast.toUint256(flowRate) * totalDuration); + if (cliffPeriod == 0) { newCtx = _createVestingSchedule( superToken, @@ -276,6 +281,11 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { ) private returns (bytes memory newCtx) { newCtx = ctx; address sender = _getSender(ctx); + + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + if (startDate < block.timestamp) revert TimeWindowInvalid(); if (receiver == address(0) || receiver == sender) revert AccountInvalid(); if (address(superToken) == address(0)) revert ZeroAddress(); diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index f3e290aa19..743739c00d 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -7,6 +7,9 @@ import { IVestingScheduler } from "./../contracts/interface/IVestingScheduler.so import { VestingScheduler } from "./../contracts/VestingScheduler.sol"; import { FoundrySuperfluidTester } from "@superfluid-finance/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol"; import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "forge-std/console.sol"; /// @title VestingSchedulerTests /// @notice Look at me , I am the captain now - Elvijs @@ -68,14 +71,15 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { event Transfer(address indexed from, address indexed to, uint256 value); /// @dev This is required by solidity for using the SuperTokenV1Library in the tester - VestingScheduler internal vestingScheduler; + VestingScheduler public vestingScheduler; /// @dev Constants for Testing - uint32 immutable START_DATE = uint32(block.timestamp + 1); - uint32 immutable CLIFF_DATE = uint32(block.timestamp + 10 days); + 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); + uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); bytes constant EMPTY_CTX = ""; uint256 internal _expectedTotalSupply = 0; @@ -86,6 +90,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { /// SETUP AND HELPERS function setUp() override public virtual { super.setUp(); + vm.warp(BLOCK_TIMESTAMP); } function _setACL_AUTHORIZE_FULL_CONTROL(address user, int96 flowRate) private { @@ -107,6 +112,16 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vm.stopPrank(); } + function _arrangeAllowances(address sender, int96 flowRate) private { + // ## Superfluid ACL allowance and permissions + _setACL_AUTHORIZE_FULL_CONTROL(sender, flowRate); + + // ## ERC-20 allowance for cliff and compensation transfers + vm.startPrank(sender); + superToken.approve(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + } + function _createVestingScheduleWithDefaultData(address sender, address receiver) private { vm.startPrank(sender); vestingScheduler.createVestingSchedule( @@ -193,7 +208,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { EMPTY_CTX ); - // revert with startDate && cliffDate = 0 + // revert with cliffDate = 0 but cliffAmount != 0 vm.expectRevert(IVestingScheduler.CliffInvalid.selector); vestingScheduler.createVestingSchedule( superToken, @@ -206,12 +221,12 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { EMPTY_CTX ); - // revert with startDate && cliffDate = 0 + // revert with startDate < block.timestamp && cliffDate = 0 vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); vestingScheduler.createVestingSchedule( superToken, bob, - 0, + uint32(block.timestamp - 1), 0, FLOW_RATE, 0, @@ -237,8 +252,8 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vestingScheduler.createVestingSchedule( superToken, bob, - uint32(block.timestamp) - 1, 0, + uint32(block.timestamp) - 1, FLOW_RATE, 0, END_DATE, @@ -645,69 +660,69 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { // --- } - function testReverts_wip() 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 - ); - - // cliff and start in history - vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - 1209600, - 0, - uint32(block.timestamp - 1), - EMPTY_CTX - ); - - // overflow - vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - type(uint256).max, - 1209600, - 0, - uint32(block.timestamp), - EMPTY_CTX - ); - - // overflow/underflow - vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - type(uint32).max, - 0, - uint32(block.timestamp), - EMPTY_CTX - ); - - // todo - // hmm, start date can be in history? - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - 1209600, - 604800, - uint32(block.timestamp - 1), - EMPTY_CTX - ); - } + // function testReverts_wip() 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 + // ); + + // // cliff and start in history + // vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); + // vestingScheduler.createVestingScheduleFromAmountAndDuration( + // superToken, + // bob, + // 1 ether, + // 1209600, + // 0, + // uint32(block.timestamp - 1), + // EMPTY_CTX + // ); + + // // overflow + // vm.expectRevert(); // todo: the right error + // vestingScheduler.createVestingScheduleFromAmountAndDuration( + // superToken, + // bob, + // type(uint256).max, + // 1209600, + // 0, + // uint32(block.timestamp), + // EMPTY_CTX + // ); + + // // overflow/underflow + // vm.expectRevert(); // todo: the right error + // vestingScheduler.createVestingScheduleFromAmountAndDuration( + // superToken, + // bob, + // 1 ether, + // type(uint32).max, + // 0, + // uint32(block.timestamp), + // EMPTY_CTX + // ); + + // // todo + // // hmm, start date can be in history? + // 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); @@ -794,4 +809,186 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { } vm.stopPrank(); } + + function test_createScheduleFromAmountAndDuration_executeCliffAndFlow_executeEndVesting( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + uint8 randomizer + ) public { + // Assume + vm.assume(totalAmount > 1); + vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(startDate == 0 || startDate >= block.timestamp); + vm.assume(totalAmount >= totalDuration); + vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); + vm.assume(startDate < 2524600800 /* year 2050 */); + vm.assume(totalDuration < 18250 days /* 50 years */); + + uint256 beforeSenderBalance = superToken.balanceOf(alice); + uint256 beforeReceiverBalance = superToken.balanceOf(bob); + + vm.assume(totalAmount <= beforeSenderBalance /* 50 years */); + + console.log(type(uint32).max); + + IVestingScheduler.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); + + + // Arrange + IVestingScheduler.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + uint32 expectedCliffDate = cliffPeriod == 0 ? 0 : expectedSchedule.cliffAndFlowDate; + uint32 expectedStartDate = startDate == 0 ? uint32(block.timestamp) : startDate; + + _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) { + // use the overload without start date + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod + ); + } else { + if (randomizer % 3 == 0) { + // use the overload without superfluid context + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + } else { + // use the overload with superfluid context + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } + } + vm.stopPrank(); + + // Assert + IVestingScheduler.VestingSchedule memory actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, expectedSchedule.cliffAndFlowDate, "schedule created: cliffAndFlowDate not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule created: flowRate not expected"); + assertEq(actualSchedule.cliffAmount, expectedSchedule.cliffAmount, "schedule created: cliffAmount not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule created: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.cliffAndFlowDate + (1 days / 2)); + 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 - (1 days / 2)); + assertTrue(vestingScheduler.executeEndVesting(superToken, alice, bob)); + + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule ended: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule ended: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, 0, "schedule ended: flowRate not expected"); + assertEq(actualSchedule.endDate, 0, "schedule ended: endDate not expected"); + assertEq(actualSchedule.remainderAmount, 0, "schedule ended: remainderAmount not expected"); + + // Assert + uint256 afterSenderBalance = superToken.balanceOf(alice); + uint256 afterReceiverBalance = superToken.balanceOf(bob); + + assertEq(afterSenderBalance, beforeSenderBalance - totalAmount, "Sender balance should decrease by totalAmount"); + assertEq(afterReceiverBalance, beforeReceiverBalance + totalAmount, "Receiver balance should increase by totalAmount"); + + vm.warp(type(uint32).max); + assertEq(afterSenderBalance, superToken.balanceOf(alice), "After the schedule has ended, the sender's balance should never change."); + } + + function _getExpectedSchedule( + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) public view returns (IVestingScheduler.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + + expectedSchedule = IVestingScheduler.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: 0 + }); + } + + function _getExpectedScheduleFromAmountAndDuration( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) public view returns (IVestingScheduler.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + + uint32 cliffDate; + uint32 cliffAndFlowDate; + uint256 cliffAmount; + if (cliffPeriod > 0) { + cliffDate = startDate + cliffPeriod; + cliffAmount = cliffPeriod * SafeCast.toUint256(flowRate); + cliffAndFlowDate = cliffDate; + } else { + cliffDate = 0; + cliffAmount = 0; + cliffAndFlowDate = startDate; + } + + uint32 endDate = startDate + totalDuration; + + uint256 remainderAmount = totalAmount - SafeCast.toUint256(flowRate) * totalDuration; + + expectedSchedule = IVestingScheduler.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: remainderAmount + }); + } } From cd264e5bbc47a3d437d99fdfb7d775b8b3c0f669 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 3 Apr 2024 17:11:34 +0300 Subject: [PATCH 14/20] 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 --- .../scheduler/contracts/VestingScheduler.sol | 12 +- .../contracts/interface/IVestingScheduler.sol | 2 +- .../scheduler/test/VestingScheduler.t.sol | 171 ++++++++++-------- 3 files changed, 103 insertions(+), 82 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index f0485fde4b..6f2c854df9 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -341,7 +341,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); vestingSchedules[configHash].endDate = endDate; vestingSchedules[configHash].remainderAmount = 0; - // Note: Nullify the dust amount if complexity of updates is introduced. + // Note: Nullify the remainder amount if complexity of updates is introduced. + emit VestingScheduleUpdated( superToken, sender, @@ -442,11 +443,14 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // delete first the stream and unlock deposit amount. cfaV1.deleteFlowByOperator(sender, receiver, superToken); - uint256 earlyEndCompensation = schedule.endDate > block.timestamp ? - (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount : 0; + uint256 earlyEndCompensation = schedule.endDate >= block.timestamp + ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount + : 0; + bool didCompensationFail = schedule.endDate < block.timestamp; if (earlyEndCompensation != 0) { - superToken.transferFrom(sender, receiver, earlyEndCompensation); + assert(superToken.transferFrom(sender, receiver, earlyEndCompensation)); + // TODO: Assert? Revert? SafeERC20? } emit VestingEndExecuted( diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index b7c2712b94..f83c2f83b2 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -29,7 +29,7 @@ interface IVestingScheduler { uint32 endDate; int96 flowRate; uint256 cliffAmount; - uint256 remainderAmount; + uint256 remainderAmount; // TODO: consider packing } /** diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index 743739c00d..673e7233d9 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -660,69 +660,69 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { // --- } - // function testReverts_wip() 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 - // ); - - // // cliff and start in history - // vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - // vestingScheduler.createVestingScheduleFromAmountAndDuration( - // superToken, - // bob, - // 1 ether, - // 1209600, - // 0, - // uint32(block.timestamp - 1), - // EMPTY_CTX - // ); - - // // overflow - // vm.expectRevert(); // todo: the right error - // vestingScheduler.createVestingScheduleFromAmountAndDuration( - // superToken, - // bob, - // type(uint256).max, - // 1209600, - // 0, - // uint32(block.timestamp), - // EMPTY_CTX - // ); - - // // overflow/underflow - // vm.expectRevert(); // todo: the right error - // vestingScheduler.createVestingScheduleFromAmountAndDuration( - // superToken, - // bob, - // 1 ether, - // type(uint32).max, - // 0, - // uint32(block.timestamp), - // EMPTY_CTX - // ); - - // // todo - // // hmm, start date can be in history? - // vestingScheduler.createVestingScheduleFromAmountAndDuration( - // superToken, - // bob, - // 1 ether, - // 1209600, - // 604800, - // uint32(block.timestamp - 1), - // EMPTY_CTX - // ); - // } + function test_createScheduleFromAmountAndDuration_reverts() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + vm.expectRevert(); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 0, + 1209600, + 604800, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with cliff and start in history."); + vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 0, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + + console.log("Revert with overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + type(uint256).max, + 1209600, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with underflow/overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + type(uint32).max, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with start date in history."); + vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 604800, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + } function testNewFunctionScheduleCreationWithoutCliff(uint8 randomizer) public { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); @@ -818,26 +818,26 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint8 randomizer ) public { // Assume - vm.assume(totalAmount > 1); - vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(randomizer != 0); + vm.assume(startDate == 0 || startDate >= block.timestamp); - vm.assume(totalAmount >= totalDuration); - vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); - vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); 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 <= beforeSenderBalance /* 50 years */); - - console.log(type(uint32).max); + vm.assume(totalAmount > 1); + vm.assume(totalAmount >= totalDuration); + vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); + vm.assume(totalAmount <= beforeSenderBalance); IVestingScheduler.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); - // Arrange IVestingScheduler.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( totalAmount, @@ -848,6 +848,23 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { 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(); @@ -856,7 +873,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { // Act vm.startPrank(alice); if (startDate == 0 && randomizer % 2 == 0) { - // use the overload without start date + console.log("Using the overload without start date."); vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, @@ -866,7 +883,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { ); } else { if (randomizer % 3 == 0) { - // use the overload without superfluid context + console.log("Using the overload without superfluid context."); vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, @@ -876,7 +893,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { startDate ); } else { - // use the overload with superfluid context + console.log("Using the overload with superfluid context."); vestingScheduler.createVestingScheduleFromAmountAndDuration( superToken, bob, @@ -899,7 +916,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); // Act - vm.warp(expectedSchedule.cliffAndFlowDate + (1 days / 2)); + vm.warp(expectedSchedule.cliffAndFlowDate + (vestingScheduler.START_DATE_VALID_AFTER() - (vestingScheduler.START_DATE_VALID_AFTER() / randomizer))); assertTrue(vestingScheduler.executeCliffAndFlow(superToken, alice, bob)); // Assert @@ -911,7 +928,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule started: remainderAmount not expected"); // Act - vm.warp(expectedSchedule.endDate - (1 days / 2)); + 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); From bd008a20ddc2816e52cad3537002b4ed1e8c6088 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 10 Apr 2024 11:55:03 +0300 Subject: [PATCH 15/20] keep V1 contract, separate V2 explicitly --- .../scheduler/contracts/VestingScheduler.sol | 273 +---- .../contracts/VestingSchedulerV2.sol | 502 ++++++++ .../contracts/interface/IVestingScheduler.sol | 108 +- .../interface/IVestingSchedulerV2.sol | 310 +++++ .../scheduler/test/VestingScheduler.t.sol | 479 +------- .../scheduler/test/VestingSchedulerV2.t.sol | 1011 +++++++++++++++++ 6 files changed, 1855 insertions(+), 828 deletions(-) create mode 100644 packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol create mode 100644 packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol create mode 100644 packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol diff --git a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 6f2c854df9..031e0d4ab2 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -7,8 +7,6 @@ import { import { SuperAppBase } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperAppBase.sol"; import { CFAv1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; import { IVestingScheduler } from "./interface/IVestingScheduler.sol"; -import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; contract VestingScheduler is IVestingScheduler, SuperAppBase { @@ -53,239 +51,8 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 endDate, bytes memory ctx ) external returns (bytes memory newCtx) { - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - 0, // remainderAmount - ctx - ); - } - - /// @dev IVestingScheduler.createVestingSchedule implementation. - function createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate - ) external { - _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - 0, // remainderAmount - bytes("") - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) external returns (bytes memory newCtx) { - newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - ctx - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) external { - _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - bytes("") - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod - ) external { - _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - cliffPeriod, - 0, // startDate - bytes("") - ); - } - - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external { - _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - bytes("") - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) external returns (bytes memory newCtx) { - newCtx = _createAndExecuteVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - ctx - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external { - _createAndExecuteVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - bytes("") - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function _createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) private returns (bytes memory newCtx) { - newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - ctx - ); - - address sender = _getSender(ctx); - assert(_executeCliffAndFlow(superToken, sender, receiver)); - } - - function _createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) private returns (bytes memory newCtx) { - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - - uint32 endDate = startDate + totalDuration; - int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); - uint256 remainderAmount = totalAmount - (SafeCast.toUint256(flowRate) * totalDuration); - - if (cliffPeriod == 0) { - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - 0 /* cliffDate */, - flowRate, - 0 /* cliffAmount */, - endDate, - remainderAmount, - ctx - ); - } else { - uint32 cliffDate = startDate + cliffPeriod; - uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); // cliffPeriod * flowRate - newCtx = _createVestingSchedule( - superToken, - receiver, - startDate, - cliffDate, - flowRate, - cliffAmount, - endDate, - remainderAmount, - ctx - ); - } - } - - function _createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate, - uint256 remainderAmount, - bytes memory ctx - ) private returns (bytes memory newCtx) { newCtx = ctx; address sender = _getSender(ctx); - - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - if (startDate < block.timestamp) revert TimeWindowInvalid(); if (receiver == address(0) || receiver == sender) revert AccountInvalid(); if (address(superToken) == address(0)) revert ZeroAddress(); @@ -294,7 +61,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - if (cliffAndFlowDate < block.timestamp || + if (cliffAndFlowDate <= block.timestamp || cliffAndFlowDate >= endDate || cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || endDate - cliffAndFlowDate < MIN_VESTING_DURATION @@ -306,8 +73,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cliffAndFlowDate, endDate, flowRate, - cliffAmount, - remainderAmount + cliffAmount ); emit VestingScheduleCreated( @@ -318,8 +84,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { cliffDate, flowRate, endDate, - cliffAmount, - remainderAmount + cliffAmount ); } @@ -340,9 +105,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); vestingSchedules[configHash].endDate = endDate; - vestingSchedules[configHash].remainderAmount = 0; - // Note: Nullify the remainder amount if complexity of updates is introduced. - emit VestingScheduleUpdated( superToken, sender, @@ -376,15 +138,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { address sender, address receiver ) external returns (bool success) { - return _executeCliffAndFlow(superToken, sender, receiver); - } - - /// @dev IVestingScheduler.executeCliffAndFlow implementation. - function _executeCliffAndFlow( - ISuperToken superToken, - address sender, - address receiver - ) private returns (bool success) { bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); VestingSchedule memory schedule = vestingSchedules[configHash]; @@ -424,7 +177,6 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { return true; } - /// @dev IVestingScheduler.executeEndVesting implementation. function executeEndVesting( ISuperToken superToken, @@ -443,14 +195,17 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { // delete first the stream and unlock deposit amount. cfaV1.deleteFlowByOperator(sender, receiver, superToken); - uint256 earlyEndCompensation = schedule.endDate >= block.timestamp - ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount - : 0; - - bool didCompensationFail = schedule.endDate < block.timestamp; + uint256 earlyEndCompensation = schedule.endDate > block.timestamp ? + (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) : 0; + bool didCompensationFail; if (earlyEndCompensation != 0) { - assert(superToken.transferFrom(sender, receiver, earlyEndCompensation)); - // TODO: Assert? Revert? SafeERC20? + // try-catch this because if the account does not have tokens for earlyEndCompensation + // we should delete the flow anyway. + try superToken.transferFrom(sender, receiver, earlyEndCompensation) + // solhint-disable-next-line no-empty-blocks + {} catch { + didCompensationFail = true; + } } emit VestingEndExecuted( @@ -499,4 +254,4 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { (,int96 flowRate,,) = cfaV1.cfa.getFlow(superToken, sender, receiver); return flowRate != 0; } -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol new file mode 100644 index 0000000000..b231add6da --- /dev/null +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable not-rely-on-time +pragma solidity ^0.8.0; +import { + ISuperfluid, ISuperToken, SuperAppDefinitions, IConstantFlowAgreementV1 +} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import { SuperAppBase } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperAppBase.sol"; +import { CFAv1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; +import { IVestingSchedulerV2 } from "./interface/IVestingSchedulerV2.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { + + using CFAv1Library for CFAv1Library.InitData; + CFAv1Library.InitData public cfaV1; + mapping(bytes32 => VestingSchedule) public vestingSchedules; // id = keccak(supertoken, sender, receiver) + + uint32 public constant MIN_VESTING_DURATION = 7 days; + uint32 public constant START_DATE_VALID_AFTER = 3 days; + uint32 public constant END_DATE_VALID_BEFORE = 1 days; + + constructor(ISuperfluid host, string memory registrationKey) { + cfaV1 = CFAv1Library.InitData( + host, + IConstantFlowAgreementV1( + address( + host.getAgreementClass( + keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") + ) + ) + ) + ); + // Superfluid SuperApp registration. This is a dumb SuperApp, only for front-end tx batch calls. + uint256 configWord = SuperAppDefinitions.APP_LEVEL_FINAL | + SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP | + SuperAppDefinitions.AFTER_AGREEMENT_CREATED_NOOP | + SuperAppDefinitions.BEFORE_AGREEMENT_UPDATED_NOOP | + SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | + SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | + SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; + host.registerAppWithKey(configWord, registrationKey); + } + + /// @dev IVestingScheduler.createVestingSchedule implementation. + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + 0, // remainderAmount + ctx + ); + } + + /// @dev IVestingScheduler.createVestingSchedule implementation. + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external { + _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + 0, // remainderAmount + bytes("") + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + ctx + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + bytes("") + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + cliffPeriod, + 0, // startDate + bytes("") + ); + } + + /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external { + _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + bytes("") + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + ctx + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external { + _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + bytes("") + ); + } + + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function _createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + ctx + ); + + address sender = _getSender(ctx); + assert(_executeCliffAndFlow(superToken, sender, receiver)); + } + + function _createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) private returns (bytes memory newCtx) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + uint32 endDate = startDate + totalDuration; + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + uint256 remainderAmount = totalAmount - (SafeCast.toUint256(flowRate) * totalDuration); + + if (cliffPeriod == 0) { + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + 0 /* cliffDate */, + flowRate, + 0 /* cliffAmount */, + endDate, + remainderAmount, + ctx + ); + } else { + uint32 cliffDate = startDate + cliffPeriod; + uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); // cliffPeriod * flowRate + newCtx = _createVestingSchedule( + superToken, + receiver, + startDate, + cliffDate, + flowRate, + cliffAmount, + endDate, + remainderAmount, + ctx + ); + } + } + + function _createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + uint256 remainderAmount, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + if (startDate < block.timestamp) revert TimeWindowInvalid(); + + if (receiver == address(0) || receiver == sender) revert AccountInvalid(); + if (address(superToken) == address(0)) revert ZeroAddress(); + if (flowRate <= 0) revert FlowRateInvalid(); + if (cliffDate != 0 && startDate > cliffDate) revert TimeWindowInvalid(); + if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + if (cliffAndFlowDate < block.timestamp || + cliffAndFlowDate >= endDate || + cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || + endDate - cliffAndFlowDate < MIN_VESTING_DURATION + ) revert TimeWindowInvalid(); + + bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver)); + if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists(); + vestingSchedules[hashConfig] = VestingSchedule( + cliffAndFlowDate, + endDate, + flowRate, + cliffAmount, + remainderAmount + ); + + emit VestingScheduleCreated( + superToken, + sender, + receiver, + startDate, + cliffDate, + flowRate, + endDate, + cliffAmount, + remainderAmount + ); + } + + function updateVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + VestingSchedule memory schedule = vestingSchedules[configHash]; + + if (endDate <= block.timestamp) revert TimeWindowInvalid(); + + // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called + if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); + vestingSchedules[configHash].endDate = endDate; + vestingSchedules[configHash].remainderAmount = 0; + // Note: Nullify the remainder amount if complexity of updates is introduced. + + emit VestingScheduleUpdated( + superToken, + sender, + receiver, + schedule.endDate, + endDate + ); + } + + /// @dev IVestingScheduler.deleteVestingSchedule implementation. + function deleteVestingSchedule( + ISuperToken superToken, + address receiver, + bytes memory ctx + ) external returns (bytes memory newCtx) { + newCtx = ctx; + address sender = _getSender(ctx); + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + + if (vestingSchedules[configHash].endDate != 0) { + delete vestingSchedules[configHash]; + emit VestingScheduleDeleted(superToken, sender, receiver); + } else { + revert ScheduleDoesNotExist(); + } + } + + /// @dev IVestingScheduler.executeCliffAndFlow implementation. + function executeCliffAndFlow( + ISuperToken superToken, + address sender, + address receiver + ) external returns (bool success) { + return _executeCliffAndFlow(superToken, sender, receiver); + } + + /// @dev IVestingScheduler.executeCliffAndFlow implementation. + function _executeCliffAndFlow( + ISuperToken superToken, + address sender, + address receiver + ) private returns (bool success) { + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + VestingSchedule memory schedule = vestingSchedules[configHash]; + + if (schedule.cliffAndFlowDate > block.timestamp || + schedule.cliffAndFlowDate + START_DATE_VALID_AFTER < block.timestamp + ) revert TimeWindowInvalid(); + + // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry. + delete vestingSchedules[configHash].cliffAndFlowDate; + delete vestingSchedules[configHash].cliffAmount; + + // Compensate for the fact that flow will almost always be executed slightly later than scheduled. + uint256 flowDelayCompensation = (block.timestamp - schedule.cliffAndFlowDate) * uint96(schedule.flowRate); + + // If there's cliff or compensation then transfer that amount. + if (schedule.cliffAmount != 0 || flowDelayCompensation != 0) { + superToken.transferFrom( + sender, + receiver, + schedule.cliffAmount + flowDelayCompensation + ); + } + + // Create a flow according to the vesting schedule configuration. + cfaV1.createFlowByOperator(sender, receiver, superToken, schedule.flowRate); + + emit VestingCliffAndFlowExecuted( + superToken, + sender, + receiver, + schedule.cliffAndFlowDate, + schedule.flowRate, + schedule.cliffAmount, + flowDelayCompensation + ); + + return true; + } + + + /// @dev IVestingScheduler.executeEndVesting implementation. + function executeEndVesting( + ISuperToken superToken, + address sender, + address receiver + ) external returns (bool success){ + bytes32 configHash = keccak256(abi.encodePacked(superToken, sender, receiver)); + VestingSchedule memory schedule = vestingSchedules[configHash]; + + if (schedule.endDate - END_DATE_VALID_BEFORE > block.timestamp) revert TimeWindowInvalid(); + + // Invalidate configuration straight away -- avoid any chance of re-execution or re-entry. + delete vestingSchedules[configHash]; + // If vesting is not running, we can't do anything, just emit failing event. + if(_isFlowOngoing(superToken, sender, receiver)) { + // delete first the stream and unlock deposit amount. + cfaV1.deleteFlowByOperator(sender, receiver, superToken); + + uint256 earlyEndCompensation = schedule.endDate >= block.timestamp + ? (schedule.endDate - block.timestamp) * uint96(schedule.flowRate) + schedule.remainderAmount + : 0; + + bool didCompensationFail = schedule.endDate < block.timestamp; + if (earlyEndCompensation != 0) { + assert(superToken.transferFrom(sender, receiver, earlyEndCompensation)); + // TODO: Assert? Revert? SafeERC20? + } + + emit VestingEndExecuted( + superToken, + sender, + receiver, + schedule.endDate, + earlyEndCompensation, + didCompensationFail + ); + } else { + emit VestingEndFailed( + superToken, + sender, + receiver, + schedule.endDate + ); + } + + return true; + } + + /// @dev IVestingScheduler.getVestingSchedule implementation. + function getVestingSchedule( + address supertoken, + address sender, + address receiver + ) external view returns (VestingSchedule memory) { + return vestingSchedules[keccak256(abi.encodePacked(supertoken, sender, receiver))]; + } + + /// @dev get sender of transaction from Superfluid Context or transaction itself. + function _getSender(bytes memory ctx) internal view returns (address sender) { + if (ctx.length != 0) { + if (msg.sender != address(cfaV1.host)) revert HostInvalid(); + sender = cfaV1.host.decodeCtx(ctx).msgSender; + } else { + sender = msg.sender; + } + // This is an invariant and should never happen. + assert(sender != address(0)); + } + + /// @dev get flowRate of stream + function _isFlowOngoing(ISuperToken superToken, address sender, address receiver) internal view returns (bool) { + (,int96 flowRate,,) = cfaV1.cfa.getFlow(superToken, sender, receiver); + return flowRate != 0; + } +} diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol index f83c2f83b2..9cb5b20802 100644 --- a/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingScheduler.sol @@ -22,14 +22,12 @@ interface IVestingScheduler { * @param endDate End date of the vesting * @param flowRate For the stream * @param cliffAmount Amount to be transferred at the cliff - * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" */ struct VestingSchedule { uint32 cliffAndFlowDate; uint32 endDate; int96 flowRate; uint256 cliffAmount; - uint256 remainderAmount; // TODO: consider packing } /** @@ -42,7 +40,6 @@ interface IVestingScheduler { * @param flowRate The flowRate for the stream * @param endDate The timestamp when the stream should stop * @param cliffAmount The amount to be transferred at the cliff - * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" */ event VestingScheduleCreated( ISuperToken indexed superToken, @@ -52,8 +49,7 @@ interface IVestingScheduler { uint32 cliffDate, int96 flowRate, uint32 endDate, - uint256 cliffAmount, - uint256 remainderAmount + uint256 cliffAmount ); /** @@ -80,106 +76,6 @@ interface IVestingScheduler { bytes memory ctx ) external returns (bytes memory newCtx); - /** - * @dev See IVestingScheduler.createVestingSchedule overload for more details. - */ - function createVestingSchedule( - ISuperToken superToken, - address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate - ) external; - - /** - * @dev Creates a new vesting schedule - * @dev The function makes it more intuitive to create a vesting schedule compared to the original function. - * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. - * @param superToken SuperToken to be vested - * @param receiver Vesting receiver - * @param totalAmount The total amount to be vested - * @param totalDuration The total duration of the vestingß - * @param cliffPeriod The cliff period of the vesting - * @param startDate Timestamp when the vesting should start - * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - bytes memory ctx - ) external returns (bytes memory newCtx); - - /** - * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) external; - - /** - * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - * The startDate is set to current block timestamp. - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod - ) external; - - /** - * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. - * The startDate is set to current block timestamp. - * Cliff period is not applied. - */ - function createVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external; - - /** - * @dev Creates a new vesting schedule - * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. - * @dev The function creates the vesting schedule with start date set to current timestamp, - * @dev and executes the start (i.e. creation of the flow) immediately. - * @param superToken SuperToken to be vested - * @param receiver Vesting receiver - * @param totalAmount The total amount to be vested - * @param totalDuration The total duration of the vestingß - * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) - */ - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) external returns (bytes memory newCtx); - - /** - * @dev See IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - */ - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external; - /** * @dev Event emitted on update of a vesting schedule * @param superToken The superToken to be vested @@ -307,4 +203,4 @@ interface IVestingScheduler { external view returns (VestingSchedule memory); -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol new file mode 100644 index 0000000000..0840d8f819 --- /dev/null +++ b/packages/automation-contracts/scheduler/contracts/interface/IVestingSchedulerV2.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity ^0.8.0; + +import { + ISuperToken +} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; + +interface IVestingSchedulerV2 { + error TimeWindowInvalid(); + error AccountInvalid(); + error ZeroAddress(); + error HostInvalid(); + error FlowRateInvalid(); + error CliffInvalid(); + error ScheduleAlreadyExists(); + error ScheduleDoesNotExist(); + error ScheduleNotFlowing(); + + /** + * @dev Vesting configuration provided by user. + * @param cliffAndFlowDate Date of flow start and cliff execution (if a cliff was specified) + * @param endDate End date of the vesting + * @param flowRate For the stream + * @param cliffAmount Amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" + */ + struct VestingSchedule { + uint32 cliffAndFlowDate; + uint32 endDate; + int96 flowRate; + uint256 cliffAmount; + uint256 remainderAmount; // TODO: consider packing + } + + /** + * @dev Event emitted on creation of a new vesting schedule + * @param superToken SuperToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting starts + * @param cliffDate Timestamp of the cliff + * @param flowRate The flowRate for the stream + * @param endDate The timestamp when the stream should stop + * @param cliffAmount The amount to be transferred at the cliff + * @param remainderAmount Amount transferred during early end to achieve an accurate "total vested amount" + */ + event VestingScheduleCreated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint32 endDate, + uint256 cliffAmount, + uint256 remainderAmount + ); + + /** + * @dev Creates a new vesting schedule + * @dev If a non-zero cliffDate is set, the startDate has no effect other than being logged in an event. + * @dev If cliffDate is set to zero, the startDate becomes the cliff (transfer cliffAmount and start stream). + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param startDate Timestamp when the vesting should start + * @param cliffDate Timestamp of cliff exectution - if 0, startDate acts as cliff + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount to be transferred at the cliff + * @param endDate The timestamp when the stream should stop. + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createVestingSchedule overload for more details. + */ + function createVestingSchedule( + ISuperToken superToken, + address receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) external; + + /** + * @dev Creates a new vesting schedule + * @dev The function makes it more intuitive to create a vesting schedule compared to the original function. + * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param totalAmount The total amount to be vested + * @param totalDuration The total duration of the vestingß + * @param cliffPeriod The cliff period of the vesting + * @param startDate Timestamp when the vesting should start + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod + ) external; + + /** + * @dev See IVestingScheduler.createVestingScheduleFromAmountAndDuration overload for more details. + * The startDate is set to current block timestamp. + * Cliff period is not applied. + */ + function createVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + + /** + * @dev Creates a new vesting schedule + * @dev The function calculates the endDate, cliffDate, cliffAmount, flowRate, etc, based on the input arguments. + * @dev The function creates the vesting schedule with start date set to current timestamp, + * @dev and executes the start (i.e. creation of the flow) immediately. + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param totalAmount The total amount to be vested + * @param totalDuration The total duration of the vestingß + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) external returns (bytes memory newCtx); + + /** + * @dev See IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + */ + function createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration + ) external; + + /** + * @dev Event emitted on update of a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param oldEndDate Old timestamp when the stream should stop + * @param endDate New timestamp when the stream should stop + */ + event VestingScheduleUpdated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 oldEndDate, + uint32 endDate + ); + + /** + * @dev Updates the end date for a vesting schedule which already reached the cliff + * @notice When updating, there's no restriction to the end date other than not being in the past + * @param superToken SuperToken to be vested + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function updateVestingSchedule(ISuperToken superToken, address receiver, uint32 endDate, bytes memory ctx) + external + returns (bytes memory newCtx); + + /** + * @dev Event emitted on deletion of a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + event VestingScheduleDeleted(ISuperToken indexed superToken, address indexed sender, address indexed receiver); + + /** + * @dev Event emitted on end of a vesting that failed because there was no running stream + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + */ + event VestingEndFailed( + ISuperToken indexed superToken, address indexed sender, address indexed receiver, uint32 endDate + ); + + /** + * @dev Deletes a vesting schedule + * @param superToken The superToken to be vested + * @param receiver Vesting receiver + * @param ctx Superfluid context used when batching operations. (or bytes(0) if not SF batching) + */ + function deleteVestingSchedule(ISuperToken superToken, address receiver, bytes memory ctx) + external + returns (bytes memory newCtx); + + /** + * @dev Emitted when the cliff of a scheduled vesting is executed + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param cliffAndFlowDate The timestamp when the stream should start + * @param flowRate The flowRate for the stream + * @param cliffAmount The amount you would like to transfer at the startDate when you start streaming + * @param flowDelayCompensation Adjusted amount transferred to receiver. (elapse time from config and tx timestamp) + */ + event VestingCliffAndFlowExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 cliffAndFlowDate, + int96 flowRate, + uint256 cliffAmount, + uint256 flowDelayCompensation + ); + + /** + * @dev Executes a cliff (transfer and stream start) + * @notice Intended to be invoked by a backend service + * @param superToken SuperToken to be streamed + * @param sender Account who will be send the stream + * @param receiver Account who will be receiving the stream + */ + function executeCliffAndFlow(ISuperToken superToken, address sender, address receiver) + external + returns (bool success); + + /** + * @dev Emitted when the end of a scheduled vesting is executed + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + * @param endDate The timestamp when the stream should stop + * @param earlyEndCompensation adjusted close amount transferred to receiver. + * @param didCompensationFail adjusted close amount transfer fail. + */ + event VestingEndExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate, + uint256 earlyEndCompensation, + bool didCompensationFail + ); + + /** + * @dev Executes the end of a vesting (stop stream) + * @notice Intended to be invoked by a backend service + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + function executeEndVesting(ISuperToken superToken, address sender, address receiver) + external + returns (bool success); + + /** + * @dev Gets data currently stored for a vesting schedule + * @param superToken The superToken to be vested + * @param sender Vesting sender + * @param receiver Vesting receiver + */ + function getVestingSchedule(address superToken, address sender, address receiver) + external + view + returns (VestingSchedule memory); +} diff --git a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol index 673e7233d9..e65f5d0310 100644 --- a/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingScheduler.t.sol @@ -7,9 +7,6 @@ import { IVestingScheduler } from "./../contracts/interface/IVestingScheduler.so import { VestingScheduler } from "./../contracts/VestingScheduler.sol"; import { FoundrySuperfluidTester } from "@superfluid-finance/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol"; import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; -import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import "forge-std/console.sol"; /// @title VestingSchedulerTests /// @notice Look at me , I am the captain now - Elvijs @@ -24,8 +21,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { uint32 cliffDate, int96 flowRate, uint32 endDate, - uint256 cliffAmount, - uint256 remainderAmount + uint256 cliffAmount ); event VestingScheduleUpdated( @@ -71,15 +67,14 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { event Transfer(address indexed from, address indexed to, uint256 value); /// @dev This is required by solidity for using the SuperTokenV1Library in the tester - VestingScheduler public vestingScheduler; + VestingScheduler internal vestingScheduler; /// @dev Constants for Testing - uint256 immutable BLOCK_TIMESTAMP = 100; - uint32 immutable START_DATE = uint32(BLOCK_TIMESTAMP + 1); - uint32 immutable CLIFF_DATE = uint32(BLOCK_TIMESTAMP + 10 days); + uint32 immutable START_DATE = uint32(block.timestamp + 1); + uint32 immutable CLIFF_DATE = uint32(block.timestamp + 10 days); int96 constant FLOW_RATE = 1000000000; uint256 constant CLIFF_TRANSFER_AMOUNT = 1 ether; - uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); + uint32 immutable END_DATE = uint32(block.timestamp + 20 days); bytes constant EMPTY_CTX = ""; uint256 internal _expectedTotalSupply = 0; @@ -90,7 +85,6 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { /// SETUP AND HELPERS function setUp() override public virtual { super.setUp(); - vm.warp(BLOCK_TIMESTAMP); } function _setACL_AUTHORIZE_FULL_CONTROL(address user, int96 flowRate) private { @@ -112,16 +106,6 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vm.stopPrank(); } - function _arrangeAllowances(address sender, int96 flowRate) private { - // ## Superfluid ACL allowance and permissions - _setACL_AUTHORIZE_FULL_CONTROL(sender, flowRate); - - // ## ERC-20 allowance for cliff and compensation transfers - vm.startPrank(sender); - superToken.approve(address(vestingScheduler), type(uint256).max); - vm.stopPrank(); - } - function _createVestingScheduleWithDefaultData(address sender, address receiver) private { vm.startPrank(sender); vestingScheduler.createVestingSchedule( @@ -142,7 +126,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { function testCreateVestingSchedule() public { vm.expectEmit(true, true, true, true); emit VestingScheduleCreated( - superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT ); _createVestingScheduleWithDefaultData(alice, bob); vm.startPrank(alice); @@ -208,7 +192,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { EMPTY_CTX ); - // revert with cliffDate = 0 but cliffAmount != 0 + // revert with startDate && cliffDate = 0 vm.expectRevert(IVestingScheduler.CliffInvalid.selector); vestingScheduler.createVestingSchedule( superToken, @@ -221,12 +205,12 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { EMPTY_CTX ); - // revert with startDate < block.timestamp && cliffDate = 0 + // revert with startDate && cliffDate = 0 vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); vestingScheduler.createVestingSchedule( superToken, bob, - uint32(block.timestamp - 1), + 0, 0, FLOW_RATE, 0, @@ -252,8 +236,8 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vestingScheduler.createVestingSchedule( superToken, bob, - 0, uint32(block.timestamp) - 1, + 0, FLOW_RATE, 0, END_DATE, @@ -324,7 +308,7 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); vm.expectEmit(true, true, true, true); emit VestingScheduleCreated( - superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT ); _createVestingScheduleWithDefaultData(alice, bob); vm.prank(alice); @@ -514,18 +498,13 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { superToken.transferAll(eve); vm.stopPrank(); vm.startPrank(admin); - uint256 earlyEndTimestamp = block.timestamp + 10 days - 3600; - vm.warp(earlyEndTimestamp); - - vm.expectRevert(); - vestingScheduler.executeEndVesting(superToken, alice, bob); - - uint256 finalTimestamp = END_DATE + 1; + uint256 finalTimestamp = block.timestamp + 10 days - 3600; vm.warp(finalTimestamp); - + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); vm.expectEmit(true, true, true, true); emit VestingEndExecuted( - superToken, alice, bob, END_DATE, 0, true + superToken, alice, bob, END_DATE, adjustedAmountClosing, true ); success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); @@ -559,17 +538,13 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { vm.startPrank(admin); uint256 initialTimestamp = block.timestamp + 10 days + 1800; vm.warp(initialTimestamp); - uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); - vm.expectEmit(true, true, true, true); emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); - vm.expectEmit(true, true, true, true); emit VestingCliffAndFlowExecuted( superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation ); - bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); assertTrue(success, "executeVesting should return true"); vm.stopPrank(); @@ -586,426 +561,4 @@ contract VestingSchedulerTests is FoundrySuperfluidTester { success = vestingScheduler.executeEndVesting(superToken, alice, bob); assertTrue(success, "executeCloseVesting should return true"); } - - // # Vesting Scheduler 1.2 tests - - function testCreateAndExecuteImmediately() public { - uint256 aliceInitialBalance = superToken.balanceOf(alice); - uint256 bobInitialBalance = superToken.balanceOf(bob); - - // # Create schedule - _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); - - vm.startPrank(alice); - superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - - uint32 startAndCliffDate = uint32(block.timestamp); - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); - - vestingScheduler.createVestingSchedule( - superToken, - bob, - startAndCliffDate, - startAndCliffDate, - FLOW_RATE, - CLIFF_TRANSFER_AMOUNT, - END_DATE, - EMPTY_CTX - ); - // --- - - // # Execute start - vm.expectEmit(); - emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT); - - vm.expectEmit(); - emit VestingCliffAndFlowExecuted( - superToken, alice, bob, startAndCliffDate, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, uint256(0) - ); - vm.stopPrank(); - - vm.startPrank(admin); - bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); - vm.stopPrank(); - - assertTrue(success, "executeVesting should return true"); - // --- - - // # Execute end - uint256 finalTimestamp = END_DATE - 3600; - vm.warp(finalTimestamp); - - uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; - uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); - - vm.expectEmit(); - emit Transfer(alice, bob, adjustedAmountClosing); - - vm.expectEmit(); - emit VestingEndExecuted( - superToken, alice, bob, END_DATE, adjustedAmountClosing, false - ); - vm.startPrank(admin); - success = vestingScheduler.executeEndVesting(superToken, alice, bob); - vm.stopPrank(); - assertTrue(success, "executeCloseVesting should return true"); - - uint256 aliceFinalBalance = superToken.balanceOf(alice); - uint256 bobFinalBalance = superToken.balanceOf(bob); - uint256 aliceShouldStream = (END_DATE - startAndCliffDate) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT; - assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); - assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); - // --- - } - - function test_createScheduleFromAmountAndDuration_reverts() public { - _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); - superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - - vm.expectRevert(); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 0, - 1209600, - 604800, - uint32(block.timestamp), - EMPTY_CTX - ); - - console.log("Revert with cliff and start in history."); - vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - 1209600, - 0, - uint32(block.timestamp - 1), - EMPTY_CTX - ); - - console.log("Revert with overflow."); - vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - type(uint256).max, - 1209600, - 0, - uint32(block.timestamp), - EMPTY_CTX - ); - - console.log("Revert with underflow/overflow."); - vm.expectRevert(); // todo: the right error - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - type(uint32).max, - 0, - uint32(block.timestamp), - EMPTY_CTX - ); - - console.log("Revert with start date in history."); - vm.expectRevert(IVestingScheduler.TimeWindowInvalid.selector); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - 1 ether, - 1209600, - 604800, - uint32(block.timestamp - 1), - EMPTY_CTX - ); - } - - function testNewFunctionScheduleCreationWithoutCliff(uint8 randomizer) public { - _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); - - vm.startPrank(alice); - superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - vm.stopPrank(); - - uint32 startDate = uint32(block.timestamp); - uint256 totalVestedAmount = 105_840_000; // a value perfectly divisible by a week - uint32 vestingDuration = 1 weeks; - int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration - uint32 expectedEndDate = startDate + vestingDuration; - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); - - vm.startPrank(alice); - bool useCtx = randomizer % 2 == 0; - if (useCtx) { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - 0, - startDate, - EMPTY_CTX - ); - } else { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - 0, - startDate - ); - } - vm.stopPrank(); - } - - function testNewFunctionScheduleCreationWithCliff(uint8 randomizer) public { - _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); - - vm.startPrank(alice); - superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); - vm.stopPrank(); - - uint32 startDate = uint32(block.timestamp); - uint256 totalVestedAmount = 103_680_000; // a value perfectly divisible - uint32 vestingDuration = 1 weeks + 1 days; - uint32 cliffPeriod = 1 days; - - int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) - uint256 expectedCliffAmount = 12960000; - uint32 expectedCliffDate = startDate + cliffPeriod; - uint32 expectedEndDate = startDate + vestingDuration; - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); - - vm.startPrank(alice); - bool useCtx = randomizer % 2 == 0; - if (useCtx) { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - cliffPeriod, - startDate, - EMPTY_CTX - ); - } else { - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalVestedAmount, - vestingDuration, - cliffPeriod, - startDate - ); - } - vm.stopPrank(); - } - - function test_createScheduleFromAmountAndDuration_executeCliffAndFlow_executeEndVesting( - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate, - uint8 randomizer - ) public { - // Assume - vm.assume(randomizer != 0); - - vm.assume(startDate == 0 || startDate >= block.timestamp); - vm.assume(startDate < 2524600800 /* year 2050 */); - - vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); - vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); - vm.assume(totalDuration < 18250 days /* 50 years */); - - uint256 beforeSenderBalance = superToken.balanceOf(alice); - uint256 beforeReceiverBalance = superToken.balanceOf(bob); - - vm.assume(totalAmount > 1); - vm.assume(totalAmount >= totalDuration); - vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); - vm.assume(totalAmount <= beforeSenderBalance); - - IVestingScheduler.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); - - // Arrange - IVestingScheduler.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( - totalAmount, - totalDuration, - cliffPeriod, - startDate - ); - uint32 expectedCliffDate = cliffPeriod == 0 ? 0 : expectedSchedule.cliffAndFlowDate; - uint32 expectedStartDate = startDate == 0 ? uint32(block.timestamp) : startDate; - - // Assume we're not getting liquidated at the end: - vm.assume(totalAmount <= (beforeSenderBalance - vestingScheduler.END_DATE_VALID_BEFORE() * SafeCast.toUint256(expectedSchedule.flowRate))); - - console.log("Total amount: %s", totalAmount); - console.log("Total duration: %s", totalDuration); - console.log("Cliff period: %s", cliffPeriod); - console.log("Start date: %s", startDate); - console.log("Randomizer: %s", randomizer); - console.log("Expected start date: %s", expectedStartDate); - console.log("Expected cliff date: %s", expectedCliffDate); - console.log("Expected cliff & flow date: %s", expectedSchedule.cliffAndFlowDate); - console.log("Expected end date: %s", expectedSchedule.endDate); - console.log("Expected flow rate: %s", SafeCast.toUint256(expectedSchedule.flowRate)); - console.log("Expected cliff amount: %s", expectedSchedule.cliffAmount); - console.log("Expected remainder amount: %s", expectedSchedule.remainderAmount); - console.log("Sender balance: %s", beforeSenderBalance); - - _arrangeAllowances(alice, expectedSchedule.flowRate); - - vm.expectEmit(); - emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, expectedSchedule.remainderAmount); - - // Act - vm.startPrank(alice); - if (startDate == 0 && randomizer % 2 == 0) { - console.log("Using the overload without start date."); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalAmount, - totalDuration, - cliffPeriod - ); - } else { - if (randomizer % 3 == 0) { - console.log("Using the overload without superfluid context."); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalAmount, - totalDuration, - cliffPeriod, - startDate - ); - } else { - console.log("Using the overload with superfluid context."); - vestingScheduler.createVestingScheduleFromAmountAndDuration( - superToken, - bob, - totalAmount, - totalDuration, - cliffPeriod, - startDate, - EMPTY_CTX - ); - } - } - vm.stopPrank(); - - // Assert - IVestingScheduler.VestingSchedule memory actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertEq(actualSchedule.cliffAndFlowDate, expectedSchedule.cliffAndFlowDate, "schedule created: cliffAndFlowDate not expected"); - assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule created: flowRate not expected"); - assertEq(actualSchedule.cliffAmount, expectedSchedule.cliffAmount, "schedule created: cliffAmount not expected"); - assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule created: endDate not expected"); - assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); - - // Act - vm.warp(expectedSchedule.cliffAndFlowDate + (vestingScheduler.START_DATE_VALID_AFTER() - (vestingScheduler.START_DATE_VALID_AFTER() / randomizer))); - assertTrue(vestingScheduler.executeCliffAndFlow(superToken, alice, bob)); - - // Assert - actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule started: cliffAndFlowDate not expected"); - assertEq(actualSchedule.cliffAmount, 0, "schedule started: cliffAmount not expected"); - assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule started: flowRate not expected"); - assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule started: endDate not expected"); - assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule started: remainderAmount not expected"); - - // Act - vm.warp(expectedSchedule.endDate - (vestingScheduler.END_DATE_VALID_BEFORE() - (vestingScheduler.END_DATE_VALID_BEFORE() / randomizer))); - assertTrue(vestingScheduler.executeEndVesting(superToken, alice, bob)); - - actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); - assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule ended: cliffAndFlowDate not expected"); - assertEq(actualSchedule.cliffAmount, 0, "schedule ended: cliffAmount not expected"); - assertEq(actualSchedule.flowRate, 0, "schedule ended: flowRate not expected"); - assertEq(actualSchedule.endDate, 0, "schedule ended: endDate not expected"); - assertEq(actualSchedule.remainderAmount, 0, "schedule ended: remainderAmount not expected"); - - // Assert - uint256 afterSenderBalance = superToken.balanceOf(alice); - uint256 afterReceiverBalance = superToken.balanceOf(bob); - - assertEq(afterSenderBalance, beforeSenderBalance - totalAmount, "Sender balance should decrease by totalAmount"); - assertEq(afterReceiverBalance, beforeReceiverBalance + totalAmount, "Receiver balance should increase by totalAmount"); - - vm.warp(type(uint32).max); - assertEq(afterSenderBalance, superToken.balanceOf(alice), "After the schedule has ended, the sender's balance should never change."); - } - - function _getExpectedSchedule( - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate - ) public view returns (IVestingScheduler.VestingSchedule memory expectedSchedule) { - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - - uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - - expectedSchedule = IVestingScheduler.VestingSchedule({ - cliffAndFlowDate: cliffAndFlowDate, - flowRate: flowRate, - cliffAmount: cliffAmount, - endDate: endDate, - remainderAmount: 0 - }); - } - - function _getExpectedScheduleFromAmountAndDuration( - uint256 totalAmount, - uint32 totalDuration, - uint32 cliffPeriod, - uint32 startDate - ) public view returns (IVestingScheduler.VestingSchedule memory expectedSchedule) { - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - - int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); - - uint32 cliffDate; - uint32 cliffAndFlowDate; - uint256 cliffAmount; - if (cliffPeriod > 0) { - cliffDate = startDate + cliffPeriod; - cliffAmount = cliffPeriod * SafeCast.toUint256(flowRate); - cliffAndFlowDate = cliffDate; - } else { - cliffDate = 0; - cliffAmount = 0; - cliffAndFlowDate = startDate; - } - - uint32 endDate = startDate + totalDuration; - - uint256 remainderAmount = totalAmount - SafeCast.toUint256(flowRate) * totalDuration; - - expectedSchedule = IVestingScheduler.VestingSchedule({ - cliffAndFlowDate: cliffAndFlowDate, - flowRate: flowRate, - cliffAmount: cliffAmount, - endDate: endDate, - remainderAmount: remainderAmount - }); - } -} +} \ No newline at end of file diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol new file mode 100644 index 0000000000..846742c1fe --- /dev/null +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -0,0 +1,1011 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; +import { FlowOperatorDefinitions } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import { IVestingSchedulerV2 } from "./../contracts/interface/IVestingSchedulerV2.sol"; +import { VestingSchedulerV2 } from "./../contracts/VestingSchedulerV2.sol"; +import { FoundrySuperfluidTester } from "@superfluid-finance/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol"; +import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "forge-std/console.sol"; + +/// @title VestingSchedulerTests +/// @notice Look at me , I am the captain now - Elvijs +contract VestingSchedulerV2Tests is FoundrySuperfluidTester { + using SuperTokenV1Library for ISuperToken; + + event VestingScheduleCreated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint32 endDate, + uint256 cliffAmount, + uint256 remainderAmount + ); + + event VestingScheduleUpdated( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 oldEndDate, + uint32 endDate + ); + + event VestingScheduleDeleted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver + ); + + event VestingCliffAndFlowExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 cliffAndFlowDate, + int96 flowRate, + uint256 cliffAmount, + uint256 flowDelayCompensation + ); + + event VestingEndExecuted( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate, + uint256 earlyEndCompensation, + bool didCompensationFail + ); + + event VestingEndFailed( + ISuperToken indexed superToken, + address indexed sender, + address indexed receiver, + uint32 endDate + ); + + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev This is required by solidity for using the SuperTokenV1Library in the tester + VestingSchedulerV2 public vestingScheduler; + + /// @dev Constants for Testing + uint256 immutable BLOCK_TIMESTAMP = 100; + uint32 immutable START_DATE = uint32(BLOCK_TIMESTAMP + 1); + uint32 immutable CLIFF_DATE = uint32(BLOCK_TIMESTAMP + 10 days); + int96 constant FLOW_RATE = 1000000000; + uint256 constant CLIFF_TRANSFER_AMOUNT = 1 ether; + uint32 immutable END_DATE = uint32(BLOCK_TIMESTAMP + 20 days); + bytes constant EMPTY_CTX = ""; + uint256 internal _expectedTotalSupply = 0; + + constructor() FoundrySuperfluidTester(3) { + vestingScheduler = new VestingSchedulerV2(sf.host, ""); + } + + /// SETUP AND HELPERS + function setUp() override public virtual { + super.setUp(); + vm.warp(BLOCK_TIMESTAMP); + } + + function _setACL_AUTHORIZE_FULL_CONTROL(address user, int96 flowRate) private { + vm.startPrank(user); + sf.host.callAgreement( + sf.cfa, + abi.encodeCall( + sf.cfa.updateFlowOperatorPermissions, + ( + superToken, + address(vestingScheduler), + FlowOperatorDefinitions.AUTHORIZE_FULL_CONTROL, + flowRate, + new bytes(0) + ) + ), + new bytes(0) + ); + vm.stopPrank(); + } + + function _arrangeAllowances(address sender, int96 flowRate) private { + // ## Superfluid ACL allowance and permissions + _setACL_AUTHORIZE_FULL_CONTROL(sender, flowRate); + + // ## ERC-20 allowance for cliff and compensation transfers + vm.startPrank(sender); + superToken.approve(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + } + + function _createVestingScheduleWithDefaultData(address sender, address receiver) private { + vm.startPrank(sender); + vestingScheduler.createVestingSchedule( + superToken, + receiver, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + vm.stopPrank(); + } + + /// TESTS + + function testCreateVestingSchedule() public { + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 + ); + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + //assert storage data + VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == CLIFF_DATE , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE , "schedule.endDate"); + assertTrue(schedule.flowRate == FLOW_RATE , "schedule.flowRate"); + assertTrue(schedule.cliffAmount == CLIFF_TRANSFER_AMOUNT , "schedule.cliffAmount"); + } + + function testCannotCreateVestingScheduleWithWrongData() public { + vm.startPrank(alice); + // revert with superToken = 0 + vm.expectRevert(IVestingSchedulerV2.ZeroAddress.selector); + vestingScheduler.createVestingSchedule( + ISuperToken(address(0)), + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = sender + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + alice, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with receivers = address(0) + vm.expectRevert(IVestingSchedulerV2.AccountInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + address(0), + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with flowRate = 0 + vm.expectRevert(IVestingSchedulerV2.FlowRateInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + 0, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffDate = 0 but cliffAmount != 0 + vm.expectRevert(IVestingSchedulerV2.CliffInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + 0, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + // revert with startDate < block.timestamp && cliffDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + uint32(block.timestamp - 1), + 0, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with endDate = 0 + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + 0, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate < block.timestamp + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + 0, + uint32(block.timestamp) - 1, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate >= endDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with cliffAndFlowDate + startDateValidFor >= endDate - endDateValidBefore + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE, + EMPTY_CTX + ); + + // revert with startDate > cliffDate + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + CLIFF_DATE + 1, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + + + // revert with vesting duration < 7 days + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + CLIFF_DATE + 2 days, + EMPTY_CTX + ); + } + + function testCannotCreateVestingScheduleIfDataExist() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.expectRevert(IVestingSchedulerV2.ScheduleAlreadyExists.selector); + _createVestingScheduleWithDefaultData(alice, bob); + } + + function testUpdateVestingSchedule() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit VestingScheduleCreated( + superToken, alice, bob, START_DATE, CLIFF_DATE, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0 + ); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + vm.startPrank(alice); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE + 1000, EMPTY_CTX); + //assert storage data + IVestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(schedule.cliffAndFlowDate == 0 , "schedule.cliffAndFlowDate"); + assertTrue(schedule.endDate == END_DATE + 1000 , "schedule.endDate"); + } + + function testCannotUpdateVestingScheduleIfNotRunning() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleNotFlowing.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE, EMPTY_CTX); + } + + function testCannotUpdateVestingScheduleIfDataDontExist() public { + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleNotFlowing.selector); + vestingScheduler.updateVestingSchedule(superToken, bob, END_DATE, EMPTY_CTX); + } + + function testDeleteVestingSchedule() public { + _createVestingScheduleWithDefaultData(alice, bob); + vm.startPrank(alice); + vm.expectEmit(true, true, true, true); + emit VestingScheduleDeleted(superToken, alice, bob); + vestingScheduler.deleteVestingSchedule(superToken, bob, EMPTY_CTX); + } + + function testCannotDeleteVestingScheduleIfDataDontExist() public { + vm.startPrank(alice); + vm.expectRevert(IVestingSchedulerV2.ScheduleDoesNotExist.selector); + vestingScheduler.deleteVestingSchedule( + superToken, + bob, + EMPTY_CTX + ); + } + + function testExecuteCliffAndFlowWithCliffAmount() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + emit Transfer(alice, bob, adjustedAmountClosing); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE-CLIFF_DATE) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT ; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowWithoutCliffAmountOrAdjustment() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + vm.startPrank(alice); + vestingScheduler.createVestingSchedule( + superToken, + bob, + START_DATE, + CLIFF_DATE, + FLOW_RATE, + 0, + END_DATE, + EMPTY_CTX + ); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + vm.startPrank(admin); + vm.warp(CLIFF_DATE); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, 0, 0 + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.warp(END_DATE); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted(superToken, alice, bob, END_DATE, 0, false); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE-CLIFF_DATE) * uint96(FLOW_RATE); + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowWithUpdatedEndDate() public { + uint32 NEW_END_DATE = END_DATE - 1000; + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit VestingScheduleUpdated(superToken, alice, bob, END_DATE, NEW_END_DATE); + vestingScheduler.updateVestingSchedule(superToken, bob, NEW_END_DATE, EMPTY_CTX); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + uint256 timeDiffToEndDate = NEW_END_DATE > block.timestamp ? NEW_END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + emit Transfer(alice, bob, adjustedAmountClosing); + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, NEW_END_DATE, adjustedAmountClosing, false + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (NEW_END_DATE-CLIFF_DATE) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT ; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + } + + function testExecuteCliffAndFlowRevertClosingTransfer() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.startPrank(alice); + superToken.transferAll(eve); + vm.stopPrank(); + vm.startPrank(admin); + uint256 earlyEndTimestamp = block.timestamp + 10 days - 3600; + vm.warp(earlyEndTimestamp); + + vm.expectRevert(); + vestingScheduler.executeEndVesting(superToken, alice, bob); + + uint256 finalTimestamp = END_DATE + 1; + vm.warp(finalTimestamp); + + vm.expectEmit(true, true, true, true); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, 0, true + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + } + + function testCannotExecuteEndVestingBeforeTime() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.executeEndVesting(superToken, alice, bob); + } + + function testCannotExecuteCliffAndFlowBeforeTime() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + } + + function testCannotExecuteEndWithoutStreamRunning() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + _createVestingScheduleWithDefaultData(alice, bob); + vm.prank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.startPrank(admin); + uint256 initialTimestamp = block.timestamp + 10 days + 1800; + vm.warp(initialTimestamp); + + uint256 flowDelayCompensation = (block.timestamp - CLIFF_DATE) * uint96(FLOW_RATE); + + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT + flowDelayCompensation); + + vm.expectEmit(true, true, true, true); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, CLIFF_DATE, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, flowDelayCompensation + ); + + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + assertTrue(success, "executeVesting should return true"); + vm.stopPrank(); + vm.startPrank(alice); + superToken.deleteFlow(alice, bob); + vm.stopPrank(); + vm.startPrank(admin); + uint256 finalTimestamp = block.timestamp + 10 days - 3600; + vm.warp(finalTimestamp); + vm.expectEmit(true, true, true, true); + emit VestingEndFailed( + superToken, alice, bob, END_DATE + ); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + assertTrue(success, "executeCloseVesting should return true"); + } + + // # Vesting Scheduler 1.2 tests + + function testCreateAndExecuteImmediately() public { + uint256 aliceInitialBalance = superToken.balanceOf(alice); + uint256 bobInitialBalance = superToken.balanceOf(bob); + + // # Create schedule + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + uint32 startAndCliffDate = uint32(block.timestamp); + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startAndCliffDate, startAndCliffDate, FLOW_RATE, END_DATE, CLIFF_TRANSFER_AMOUNT, 0); + + vestingScheduler.createVestingSchedule( + superToken, + bob, + startAndCliffDate, + startAndCliffDate, + FLOW_RATE, + CLIFF_TRANSFER_AMOUNT, + END_DATE, + EMPTY_CTX + ); + // --- + + // # Execute start + vm.expectEmit(); + emit Transfer(alice, bob, CLIFF_TRANSFER_AMOUNT); + + vm.expectEmit(); + emit VestingCliffAndFlowExecuted( + superToken, alice, bob, startAndCliffDate, FLOW_RATE, CLIFF_TRANSFER_AMOUNT, uint256(0) + ); + vm.stopPrank(); + + vm.startPrank(admin); + bool success = vestingScheduler.executeCliffAndFlow(superToken, alice, bob); + vm.stopPrank(); + + assertTrue(success, "executeVesting should return true"); + // --- + + // # Execute end + uint256 finalTimestamp = END_DATE - 3600; + vm.warp(finalTimestamp); + + uint256 timeDiffToEndDate = END_DATE > block.timestamp ? END_DATE - block.timestamp : 0; + uint256 adjustedAmountClosing = timeDiffToEndDate * uint96(FLOW_RATE); + + vm.expectEmit(); + emit Transfer(alice, bob, adjustedAmountClosing); + + vm.expectEmit(); + emit VestingEndExecuted( + superToken, alice, bob, END_DATE, adjustedAmountClosing, false + ); + vm.startPrank(admin); + success = vestingScheduler.executeEndVesting(superToken, alice, bob); + vm.stopPrank(); + assertTrue(success, "executeCloseVesting should return true"); + + uint256 aliceFinalBalance = superToken.balanceOf(alice); + uint256 bobFinalBalance = superToken.balanceOf(bob); + uint256 aliceShouldStream = (END_DATE - startAndCliffDate) * uint96(FLOW_RATE) + CLIFF_TRANSFER_AMOUNT; + assertEq(aliceInitialBalance - aliceFinalBalance, aliceShouldStream, "(sender) wrong final balance"); + assertEq(bobFinalBalance, bobInitialBalance + aliceShouldStream, "(receiver) wrong final balance"); + // --- + } + + function test_createScheduleFromAmountAndDuration_reverts() public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + + vm.expectRevert(); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 0, + 1209600, + 604800, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with cliff and start in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 0, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + + console.log("Revert with overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + type(uint256).max, + 1209600, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with underflow/overflow."); + vm.expectRevert(); // todo: the right error + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + type(uint32).max, + 0, + uint32(block.timestamp), + EMPTY_CTX + ); + + console.log("Revert with start date in history."); + vm.expectRevert(IVestingSchedulerV2.TimeWindowInvalid.selector); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + 1 ether, + 1209600, + 604800, + uint32(block.timestamp - 1), + EMPTY_CTX + ); + } + + function testNewFunctionScheduleCreationWithoutCliff(uint8 randomizer) public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint32 startDate = uint32(block.timestamp); + uint256 totalVestedAmount = 105_840_000; // a value perfectly divisible by a week + uint32 vestingDuration = 1 weeks; + int96 expectedFlowRate = 175; // totalVestedAmount / vestingDuration + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, 0, expectedFlowRate, expectedEndDate, 0, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + 0, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + 0, + startDate + ); + } + vm.stopPrank(); + } + + function testNewFunctionScheduleCreationWithCliff(uint8 randomizer) public { + _setACL_AUTHORIZE_FULL_CONTROL(alice, FLOW_RATE); + + vm.startPrank(alice); + superToken.increaseAllowance(address(vestingScheduler), type(uint256).max); + vm.stopPrank(); + + uint32 startDate = uint32(block.timestamp); + uint256 totalVestedAmount = 103_680_000; // a value perfectly divisible + uint32 vestingDuration = 1 weeks + 1 days; + uint32 cliffPeriod = 1 days; + + int96 expectedFlowRate = 150; // (totalVestedAmount - cliffAmount) / (vestingDuration - cliffPeriod) + uint256 expectedCliffAmount = 12960000; + uint32 expectedCliffDate = startDate + cliffPeriod; + uint32 expectedEndDate = startDate + vestingDuration; + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, startDate, expectedCliffDate, expectedFlowRate, expectedEndDate, expectedCliffAmount, 0); + + vm.startPrank(alice); + bool useCtx = randomizer % 2 == 0; + if (useCtx) { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } else { + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalVestedAmount, + vestingDuration, + cliffPeriod, + startDate + ); + } + vm.stopPrank(); + } + + function test_createScheduleFromAmountAndDuration_executeCliffAndFlow_executeEndVesting( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate, + uint8 randomizer + ) public { + // Assume + vm.assume(randomizer != 0); + + vm.assume(startDate == 0 || startDate >= block.timestamp); + vm.assume(startDate < 2524600800 /* year 2050 */); + + vm.assume(totalDuration > vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(cliffPeriod <= totalDuration - vestingScheduler.MIN_VESTING_DURATION()); + vm.assume(totalDuration < 18250 days /* 50 years */); + + uint256 beforeSenderBalance = superToken.balanceOf(alice); + uint256 beforeReceiverBalance = superToken.balanceOf(bob); + + vm.assume(totalAmount > 1); + vm.assume(totalAmount >= totalDuration); + vm.assume(totalAmount / totalDuration <= SafeCast.toUint256(type(int96).max)); + vm.assume(totalAmount <= beforeSenderBalance); + + IVestingSchedulerV2.VestingSchedule memory nullSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertTrue(nullSchedule.endDate == 0, "Schedule should not exist"); + + // Arrange + IVestingSchedulerV2.VestingSchedule memory expectedSchedule = _getExpectedScheduleFromAmountAndDuration( + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + uint32 expectedCliffDate = cliffPeriod == 0 ? 0 : expectedSchedule.cliffAndFlowDate; + uint32 expectedStartDate = startDate == 0 ? uint32(block.timestamp) : startDate; + + // Assume we're not getting liquidated at the end: + vm.assume(totalAmount <= (beforeSenderBalance - vestingScheduler.END_DATE_VALID_BEFORE() * SafeCast.toUint256(expectedSchedule.flowRate))); + + console.log("Total amount: %s", totalAmount); + console.log("Total duration: %s", totalDuration); + console.log("Cliff period: %s", cliffPeriod); + console.log("Start date: %s", startDate); + console.log("Randomizer: %s", randomizer); + console.log("Expected start date: %s", expectedStartDate); + console.log("Expected cliff date: %s", expectedCliffDate); + console.log("Expected cliff & flow date: %s", expectedSchedule.cliffAndFlowDate); + console.log("Expected end date: %s", expectedSchedule.endDate); + console.log("Expected flow rate: %s", SafeCast.toUint256(expectedSchedule.flowRate)); + console.log("Expected cliff amount: %s", expectedSchedule.cliffAmount); + console.log("Expected remainder amount: %s", expectedSchedule.remainderAmount); + console.log("Sender balance: %s", beforeSenderBalance); + + _arrangeAllowances(alice, expectedSchedule.flowRate); + + vm.expectEmit(); + emit VestingScheduleCreated(superToken, alice, bob, expectedStartDate, expectedCliffDate, expectedSchedule.flowRate, expectedSchedule.endDate, expectedSchedule.cliffAmount, expectedSchedule.remainderAmount); + + // Act + vm.startPrank(alice); + if (startDate == 0 && randomizer % 2 == 0) { + console.log("Using the overload without start date."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod + ); + } else { + if (randomizer % 3 == 0) { + console.log("Using the overload without superfluid context."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate + ); + } else { + console.log("Using the overload with superfluid context."); + vestingScheduler.createVestingScheduleFromAmountAndDuration( + superToken, + bob, + totalAmount, + totalDuration, + cliffPeriod, + startDate, + EMPTY_CTX + ); + } + } + vm.stopPrank(); + + // Assert + IVestingSchedulerV2.VestingSchedule memory actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, expectedSchedule.cliffAndFlowDate, "schedule created: cliffAndFlowDate not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule created: flowRate not expected"); + assertEq(actualSchedule.cliffAmount, expectedSchedule.cliffAmount, "schedule created: cliffAmount not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule created: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule created: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.cliffAndFlowDate + (vestingScheduler.START_DATE_VALID_AFTER() - (vestingScheduler.START_DATE_VALID_AFTER() / randomizer))); + assertTrue(vestingScheduler.executeCliffAndFlow(superToken, alice, bob)); + + // Assert + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule started: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule started: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, expectedSchedule.flowRate, "schedule started: flowRate not expected"); + assertEq(actualSchedule.endDate, expectedSchedule.endDate, "schedule started: endDate not expected"); + assertEq(actualSchedule.remainderAmount, expectedSchedule.remainderAmount, "schedule started: remainderAmount not expected"); + + // Act + vm.warp(expectedSchedule.endDate - (vestingScheduler.END_DATE_VALID_BEFORE() - (vestingScheduler.END_DATE_VALID_BEFORE() / randomizer))); + assertTrue(vestingScheduler.executeEndVesting(superToken, alice, bob)); + + actualSchedule = vestingScheduler.getVestingSchedule(address(superToken), alice, bob); + assertEq(actualSchedule.cliffAndFlowDate, 0, "schedule ended: cliffAndFlowDate not expected"); + assertEq(actualSchedule.cliffAmount, 0, "schedule ended: cliffAmount not expected"); + assertEq(actualSchedule.flowRate, 0, "schedule ended: flowRate not expected"); + assertEq(actualSchedule.endDate, 0, "schedule ended: endDate not expected"); + assertEq(actualSchedule.remainderAmount, 0, "schedule ended: remainderAmount not expected"); + + // Assert + uint256 afterSenderBalance = superToken.balanceOf(alice); + uint256 afterReceiverBalance = superToken.balanceOf(bob); + + assertEq(afterSenderBalance, beforeSenderBalance - totalAmount, "Sender balance should decrease by totalAmount"); + assertEq(afterReceiverBalance, beforeReceiverBalance + totalAmount, "Receiver balance should increase by totalAmount"); + + vm.warp(type(uint32).max); + assertEq(afterSenderBalance, superToken.balanceOf(alice), "After the schedule has ended, the sender's balance should never change."); + } + + function _getExpectedSchedule( + uint32 startDate, + uint32 cliffDate, + int96 flowRate, + uint256 cliffAmount, + uint32 endDate + ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: 0 + }); + } + + function _getExpectedScheduleFromAmountAndDuration( + uint256 totalAmount, + uint32 totalDuration, + uint32 cliffPeriod, + uint32 startDate + ) public view returns (IVestingSchedulerV2.VestingSchedule memory expectedSchedule) { + if (startDate == 0) { + startDate = uint32(block.timestamp); + } + + int96 flowRate = SafeCast.toInt96(SafeCast.toInt256(totalAmount / totalDuration)); + + uint32 cliffDate; + uint32 cliffAndFlowDate; + uint256 cliffAmount; + if (cliffPeriod > 0) { + cliffDate = startDate + cliffPeriod; + cliffAmount = cliffPeriod * SafeCast.toUint256(flowRate); + cliffAndFlowDate = cliffDate; + } else { + cliffDate = 0; + cliffAmount = 0; + cliffAndFlowDate = startDate; + } + + uint32 endDate = startDate + totalDuration; + + uint256 remainderAmount = totalAmount - SafeCast.toUint256(flowRate) * totalDuration; + + expectedSchedule = IVestingSchedulerV2.VestingSchedule({ + cliffAndFlowDate: cliffAndFlowDate, + flowRate: flowRate, + cliffAmount: cliffAmount, + endDate: endDate, + remainderAmount: remainderAmount + }); + } +} From 5c75550479e10cc2e0e4a51eef6b20d81dd3b196 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Wed, 10 Apr 2024 16:57:02 +0300 Subject: [PATCH 16/20] update deploy script for v2 --- .../scheduler/.env-example | 8 ++- .../scheduler/deploy/deploy.js | 72 ++++++++++++++----- .../scheduler/hardhat.config.js | 18 ++++- .../scheduler/package.json | 1 + 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/packages/automation-contracts/scheduler/.env-example b/packages/automation-contracts/scheduler/.env-example index 3e29df7dad..7e55793109 100644 --- a/packages/automation-contracts/scheduler/.env-example +++ b/packages/automation-contracts/scheduler/.env-example @@ -3,9 +3,15 @@ MUMBAI_PRIVATE_KEY= POLYGON_PRIVATE_KEY= BSC_PRIVATE_KEY= +OPSEPOLIA_PRIVATE_KEY= MUMBAI_URL= POLYGON_URL= BSC_URL= +OPSEPOLIA_URL= -ETHERSCAN_API_KEY = +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/deploy/deploy.js b/packages/automation-contracts/scheduler/deploy/deploy.js index 718fd771d9..b9ee36347d 100644 --- a/packages/automation-contracts/scheduler/deploy/deploy.js +++ b/packages/automation-contracts/scheduler/deploy/deploy.js @@ -24,41 +24,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, registrationKey], + 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], contract: "contracts/FlowScheduler.sol:FlowScheduler", }); + } + + if (deployVestingScheduler) { + console.log("Deploying VestingScheduler..."); + const VestingScheduler = await deploy("VestingScheduler", { + from: deployer, + args: [host, registrationKey], + 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], 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, registrationKey], + 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, registrationKey], + 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 5711a7e622..3c86819248 100644 --- a/packages/automation-contracts/scheduler/hardhat.config.js +++ b/packages/automation-contracts/scheduler/hardhat.config.js @@ -40,8 +40,14 @@ module.exports = { accounts: process.env.BSC_PRIVATE_KEY !== undefined ? [process.env.BSC_PRIVATE_KEY] : [], }, + opsepolia: { + url: process.env.OPSEPOLIA_URL || "", + accounts: + process.env.OPSEPOLIA_PRIVATE_KEY !== undefined + ? [process.env.OPSEPOLIA_PRIVATE_KEY] + : [], + }, }, - namedAccounts: { deployer: { default: 0, @@ -49,5 +55,15 @@ 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/", + }, + }, + ], }, }; diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index 9136ddd415..af83916949 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/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", From bfbefa1d31b86334c8b0c03a35d4b5940975d566 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Tue, 30 Apr 2024 00:13:04 +0300 Subject: [PATCH 17/20] unify deploy scripts --- .../automation-contracts/autowrap/.env-example | 2 ++ .../autowrap/hardhat.config.js | 18 +++++++++++++++++- .../automation-contracts/autowrap/package.json | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/automation-contracts/autowrap/.env-example b/packages/automation-contracts/autowrap/.env-example index 3e29df7dad..39e1cff801 100644 --- a/packages/automation-contracts/autowrap/.env-example +++ b/packages/automation-contracts/autowrap/.env-example @@ -3,9 +3,11 @@ MUMBAI_PRIVATE_KEY= POLYGON_PRIVATE_KEY= BSC_PRIVATE_KEY= +OPSEPOLIA_PRIVATE_KEY= MUMBAI_URL= POLYGON_URL= BSC_URL= +OPSEPOLIA_URL= ETHERSCAN_API_KEY = diff --git a/packages/automation-contracts/autowrap/hardhat.config.js b/packages/automation-contracts/autowrap/hardhat.config.js index 4350f455fe..fef8551b7e 100644 --- a/packages/automation-contracts/autowrap/hardhat.config.js +++ b/packages/automation-contracts/autowrap/hardhat.config.js @@ -41,8 +41,14 @@ module.exports = { accounts: process.env.BSC_PRIVATE_KEY !== undefined ? [process.env.BSC_PRIVATE_KEY] : [], }, + opsepolia: { + url: process.env.OPSEPOLIA_URL || "", + accounts: + process.env.OPSEPOLIA_PRIVATE_KEY !== undefined + ? [process.env.OPSEPOLIA_PRIVATE_KEY] + : [], + }, }, - namedAccounts: { deployer: { default: 0, @@ -50,5 +56,15 @@ 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/", + }, + }, + ], }, }; 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", From 2bdd112a2676bd2c25dc8ea7af03851dd72d5fc2 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Tue, 30 Apr 2024 17:11:41 +0300 Subject: [PATCH 18/20] use newer host.registerApp & unify deploy scripts - add base-mainnet option --- .../autowrap/.env-example | 13 ------- .../autowrap/.env.example | 11 ++++++ .../autowrap/hardhat.config.js | 36 +++++++++++++++---- .../scheduler/{.env-example => .env.example} | 6 ++-- .../scheduler/contracts/FlowScheduler.sol | 4 +-- .../scheduler/contracts/VestingScheduler.sol | 4 +-- .../contracts/VestingSchedulerV2.sol | 4 +-- .../scheduler/deploy/deploy.js | 17 +++++---- .../scheduler/hardhat.config.js | 36 +++++++++++++++---- .../scheduler/test/FlowScheduler.t.sol | 2 +- .../test/FlowSchedulerResolver.t.sol | 2 +- .../scheduler/test/VestingScheduler.t.sol | 2 +- .../scheduler/test/VestingSchedulerV2.t.sol | 2 +- 13 files changed, 89 insertions(+), 50 deletions(-) delete mode 100644 packages/automation-contracts/autowrap/.env-example create mode 100644 packages/automation-contracts/autowrap/.env.example rename packages/automation-contracts/scheduler/{.env-example => .env.example} (66%) diff --git a/packages/automation-contracts/autowrap/.env-example b/packages/automation-contracts/autowrap/.env-example deleted file mode 100644 index 39e1cff801..0000000000 --- a/packages/automation-contracts/autowrap/.env-example +++ /dev/null @@ -1,13 +0,0 @@ -# .env-example - -MUMBAI_PRIVATE_KEY= -POLYGON_PRIVATE_KEY= -BSC_PRIVATE_KEY= -OPSEPOLIA_PRIVATE_KEY= - -MUMBAI_URL= -POLYGON_URL= -BSC_URL= -OPSEPOLIA_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 fef8551b7e..db27b84bc7 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,24 +29,38 @@ module.exports = { mumbai: { url: process.env.MUMBAI_URL || "", accounts: - process.env.MUMBAI_PRIVATE_KEY !== undefined ? [process.env.MUMBAI_PRIVATE_KEY] : [], + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], }, 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.OPSEPOLIA_PRIVATE_KEY !== undefined - ? [process.env.OPSEPOLIA_PRIVATE_KEY] + 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: { @@ -65,6 +79,14 @@ module.exports = { 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/.env-example b/packages/automation-contracts/scheduler/.env.example similarity index 66% rename from packages/automation-contracts/scheduler/.env-example rename to packages/automation-contracts/scheduler/.env.example index 7e55793109..8b21c653d7 100644 --- a/packages/automation-contracts/scheduler/.env-example +++ b/packages/automation-contracts/scheduler/.env.example @@ -1,14 +1,12 @@ # .env-example -MUMBAI_PRIVATE_KEY= -POLYGON_PRIVATE_KEY= -BSC_PRIVATE_KEY= -OPSEPOLIA_PRIVATE_KEY= +PRIVATE_KEY= MUMBAI_URL= POLYGON_URL= BSC_URL= OPSEPOLIA_URL= +BASE_URL=https://mainnet.base.org ETHERSCAN_API_KEY= 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 031e0d4ab2..9f6fd0c6c9 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -18,7 +18,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 public constant START_DATE_VALID_AFTER = 3 days; uint32 public constant END_DATE_VALID_BEFORE = 1 days; - constructor(ISuperfluid host, string memory registrationKey) { + constructor(ISuperfluid host) { cfaV1 = CFAv1Library.InitData( host, IConstantFlowAgreementV1( @@ -37,7 +37,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; - host.registerAppWithKey(configWord, registrationKey); + host.registerApp(configWord); } /// @dev IVestingScheduler.createVestingSchedule implementation. diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol index b231add6da..e4f0115d2b 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -20,7 +20,7 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { uint32 public constant START_DATE_VALID_AFTER = 3 days; uint32 public constant END_DATE_VALID_BEFORE = 1 days; - constructor(ISuperfluid host, string memory registrationKey) { + constructor(ISuperfluid host) { cfaV1 = CFAv1Library.InitData( host, IConstantFlowAgreementV1( @@ -39,7 +39,7 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; - host.registerAppWithKey(configWord, registrationKey); + host.registerApp(configWord); } /// @dev IVestingScheduler.createVestingSchedule implementation. diff --git a/packages/automation-contracts/scheduler/deploy/deploy.js b/packages/automation-contracts/scheduler/deploy/deploy.js index b9ee36347d..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; @@ -42,9 +41,9 @@ module.exports = async function ({ deployments, getNamedAccounts }) { console.log("Deploying FlowScheduler..."); const FlowScheduler = await deploy("FlowScheduler", { from: deployer, - args: [host, registrationKey], + args: [host], log: true, - skipIfAlreadyDeployed: false, + skipIfAlreadyDeployed: false }); // wait for 15 seconds to allow etherscan to indexed the contracts @@ -53,7 +52,7 @@ module.exports = async function ({ deployments, getNamedAccounts }) { console.log("Verifying FlowScheduler..."); await hre.run("verify:verify", { address: FlowScheduler.address, - constructorArguments: [host, registrationKey], + constructorArguments: [host], contract: "contracts/FlowScheduler.sol:FlowScheduler", }); } @@ -62,9 +61,9 @@ module.exports = async function ({ deployments, getNamedAccounts }) { console.log("Deploying VestingScheduler..."); const VestingScheduler = await deploy("VestingScheduler", { from: deployer, - args: [host, registrationKey], + args: [host], log: true, - skipIfAlreadyDeployed: false, + skipIfAlreadyDeployed: false }); // wait for 15 seconds to allow etherscan to indexed the contracts @@ -73,7 +72,7 @@ module.exports = async function ({ deployments, getNamedAccounts }) { console.log("Verifying VestingScheduler..."); await hre.run("verify:verify", { address: VestingScheduler.address, - constructorArguments: [host, registrationKey], + constructorArguments: [host], contract: "contracts/VestingScheduler.sol:VestingScheduler", }); } @@ -82,7 +81,7 @@ module.exports = async function ({ deployments, getNamedAccounts }) { console.log("Deploying VestingSchedulerV2..."); const VestingSchedulerV2 = await deploy("VestingSchedulerV2", { from: deployer, - args: [host, registrationKey], + args: [host], log: true, skipIfAlreadyDeployed: false, }); @@ -93,7 +92,7 @@ module.exports = async function ({ deployments, getNamedAccounts }) { console.log("Verifying VestingSchedulerV2..."); await hre.run("verify:verify", { address: VestingSchedulerV2.address, - constructorArguments: [host, registrationKey], + constructorArguments: [host], contract: "contracts/VestingSchedulerV2.sol:VestingSchedulerV2", }); } diff --git a/packages/automation-contracts/scheduler/hardhat.config.js b/packages/automation-contracts/scheduler/hardhat.config.js index 3c86819248..2d4e4e0741 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,24 +28,38 @@ module.exports = { mumbai: { url: process.env.MUMBAI_URL || "", accounts: - process.env.MUMBAI_PRIVATE_KEY !== undefined ? [process.env.MUMBAI_PRIVATE_KEY] : [], + process.env.PRIVATE_KEY !== undefined + ? [process.env.PRIVATE_KEY] + : [], }, 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.OPSEPOLIA_PRIVATE_KEY !== undefined - ? [process.env.OPSEPOLIA_PRIVATE_KEY] + 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: { @@ -64,6 +78,14 @@ module.exports = { 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/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 e65f5d0310..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 diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol index 846742c1fe..695479cd6c 100644 --- a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -84,7 +84,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { uint256 internal _expectedTotalSupply = 0; constructor() FoundrySuperfluidTester(3) { - vestingScheduler = new VestingSchedulerV2(sf.host, ""); + vestingScheduler = new VestingSchedulerV2(sf.host); } /// SETUP AND HELPERS From cb84d338cf6ec922f1f04dfcdf16f36e67400f1e Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Thu, 16 May 2024 16:24:40 +0300 Subject: [PATCH 19/20] clean-up --- .../contracts/VestingSchedulerV2.sol | 184 +++++++++--------- 1 file changed, 95 insertions(+), 89 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol index e4f0115d2b..072cbe15be 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -89,7 +89,67 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { ); } - /// @dev IVestingScheduler.createVestingScheduleFromAmountAndDuration implementation. + 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, @@ -184,44 +244,6 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { ); } - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration - ) external { - _createAndExecuteVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - bytes("") - ); - } - - /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. - function _createAndExecuteVestingScheduleFromAmountAndDuration( - ISuperToken superToken, - address receiver, - uint256 totalAmount, - uint32 totalDuration, - bytes memory ctx - ) private returns (bytes memory newCtx) { - newCtx = _createVestingScheduleFromAmountAndDuration( - superToken, - receiver, - totalAmount, - totalDuration, - 0, // cliffPeriod - 0, // startDate - ctx - ); - - address sender = _getSender(ctx); - assert(_executeCliffAndFlow(superToken, sender, receiver)); - } - function _createVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, @@ -231,6 +253,7 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { 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); } @@ -253,7 +276,7 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { ); } else { uint32 cliffDate = startDate + cliffPeriod; - uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); // cliffPeriod * flowRate + uint256 cliffAmount = SafeMath.mul(cliffPeriod, SafeCast.toUint256(flowRate)); newCtx = _createVestingSchedule( superToken, receiver, @@ -268,59 +291,42 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { } } - function _createVestingSchedule( + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function createAndExecuteVestingScheduleFromAmountAndDuration( ISuperToken superToken, address receiver, - uint32 startDate, - uint32 cliffDate, - int96 flowRate, - uint256 cliffAmount, - uint32 endDate, - uint256 remainderAmount, - bytes memory ctx - ) private returns (bytes memory newCtx) { - newCtx = ctx; - address sender = _getSender(ctx); - - if (startDate == 0) { - startDate = uint32(block.timestamp); - } - if (startDate < block.timestamp) revert TimeWindowInvalid(); - - if (receiver == address(0) || receiver == sender) revert AccountInvalid(); - if (address(superToken) == address(0)) revert ZeroAddress(); - if (flowRate <= 0) revert FlowRateInvalid(); - if (cliffDate != 0 && startDate > cliffDate) revert TimeWindowInvalid(); - if (cliffDate == 0 && cliffAmount != 0) revert CliffInvalid(); - - uint32 cliffAndFlowDate = cliffDate == 0 ? startDate : cliffDate; - if (cliffAndFlowDate < block.timestamp || - cliffAndFlowDate >= endDate || - cliffAndFlowDate + START_DATE_VALID_AFTER >= endDate - END_DATE_VALID_BEFORE || - endDate - cliffAndFlowDate < MIN_VESTING_DURATION - ) revert TimeWindowInvalid(); - - bytes32 hashConfig = keccak256(abi.encodePacked(superToken, sender, receiver)); - if (vestingSchedules[hashConfig].endDate != 0) revert ScheduleAlreadyExists(); - vestingSchedules[hashConfig] = VestingSchedule( - cliffAndFlowDate, - endDate, - flowRate, - cliffAmount, - remainderAmount + uint256 totalAmount, + uint32 totalDuration + ) external { + _createAndExecuteVestingScheduleFromAmountAndDuration( + superToken, + receiver, + totalAmount, + totalDuration, + bytes("") ); + } - emit VestingScheduleCreated( + /// @dev IVestingScheduler.createAndExecuteVestingScheduleFromAmountAndDuration. + function _createAndExecuteVestingScheduleFromAmountAndDuration( + ISuperToken superToken, + address receiver, + uint256 totalAmount, + uint32 totalDuration, + bytes memory ctx + ) private returns (bytes memory newCtx) { + newCtx = _createVestingScheduleFromAmountAndDuration( superToken, - sender, receiver, - startDate, - cliffDate, - flowRate, - endDate, - cliffAmount, - remainderAmount + totalAmount, + totalDuration, + 0, // cliffPeriod + 0, // startDate + ctx ); + + address sender = _getSender(ctx); + assert(_executeCliffAndFlow(superToken, sender, receiver)); } function updateVestingSchedule( @@ -340,8 +346,8 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { // Only allow an update if 1. vesting exists 2. executeCliffAndFlow() has been called if (schedule.cliffAndFlowDate != 0 || schedule.endDate == 0) revert ScheduleNotFlowing(); vestingSchedules[configHash].endDate = endDate; + // Note: Nullify the remainder amount when complexity of updates is introduced. vestingSchedules[configHash].remainderAmount = 0; - // Note: Nullify the remainder amount if complexity of updates is introduced. emit VestingScheduleUpdated( superToken, @@ -424,7 +430,6 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { return true; } - /// @dev IVestingScheduler.executeEndVesting implementation. function executeEndVesting( ISuperToken superToken, @@ -447,10 +452,11 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { ? (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)); - // TODO: Assert? Revert? SafeERC20? } emit VestingEndExecuted( From c1f97dc1108b7c237bb6613a86d60af981ae0b47 Mon Sep 17 00:00:00 2001 From: Kaspar Kallas Date: Fri, 17 May 2024 17:17:52 +0300 Subject: [PATCH 20/20] add diff generation script & completely revert VestingScheduler.sol --- .../automation-contracts/scheduler/audit/generate_diffs.sh | 4 ++++ .../scheduler/contracts/VestingScheduler.sol | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100755 packages/automation-contracts/scheduler/audit/generate_diffs.sh 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/VestingScheduler.sol b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol index 9f6fd0c6c9..031e0d4ab2 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingScheduler.sol @@ -18,7 +18,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { uint32 public constant START_DATE_VALID_AFTER = 3 days; uint32 public constant END_DATE_VALID_BEFORE = 1 days; - constructor(ISuperfluid host) { + constructor(ISuperfluid host, string memory registrationKey) { cfaV1 = CFAv1Library.InitData( host, IConstantFlowAgreementV1( @@ -37,7 +37,7 @@ contract VestingScheduler is IVestingScheduler, SuperAppBase { SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; - host.registerApp(configWord); + host.registerAppWithKey(configWord, registrationKey); } /// @dev IVestingScheduler.createVestingSchedule implementation.