diff --git a/.solhint.json b/.solhint.json index 7ac084cfe..0d4c50cbc 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,7 +2,7 @@ "extends": "solhint:recommended", "rules": { "avoid-low-level-calls": "off", - "code-complexity": ["error", 9], + "code-complexity": ["error", 10], "compiler-version": ["error", ">=0.8.22"], "contract-name-camelcase": "off", "const-name-snakecase": "off", diff --git a/bun.lockb b/bun.lockb index dae3e9c4e..b55c04098 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index b0d9bd3a6..a4b7e3d8a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@openzeppelin/contracts": "5.0.2", - "@prb/math": "4.0.3" + "@prb/math": "4.1.0" }, "devDependencies": { "forge-std": "github:foundry-rs/forge-std#v1.8.2", diff --git a/script/core/Init.s.sol b/script/core/Init.s.sol index 1365fb9ba..ee7363358 100644 --- a/script/core/Init.s.sol +++ b/script/core/Init.s.sol @@ -4,7 +4,6 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud2x18 } from "@prb/math/src/UD2x18.sol"; import { ud60x18 } from "@prb/math/src/UD60x18.sol"; - import { Solarray } from "solarray/src/Solarray.sol"; import { ISablierLockup } from "../../src/core/interfaces/ISablierLockup.sol"; @@ -54,6 +53,7 @@ contract Init is BaseScript { transferable: true, broker: Broker(address(0), ud60x18(0)) }), + LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }), LockupLinear.Durations({ cliff: cliffDurations[i], total: totalDurations[i] }) ); } diff --git a/script/periphery/CreateMerkleLL.s.sol b/script/periphery/CreateMerkleLL.s.sol index 38567cda3..4d95c916d 100644 --- a/script/periphery/CreateMerkleLL.s.sol +++ b/script/periphery/CreateMerkleLL.s.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierLockup } from "../../src/core/interfaces/ISablierLockup.sol"; +import { LockupLinear } from "../../src/core/types/DataTypes.sol"; import { ISablierMerkleFactory } from "../../src/periphery/interfaces/ISablierMerkleFactory.sol"; import { ISablierMerkleLL } from "../../src/periphery/interfaces/ISablierMerkleLL.sol"; import { MerkleBase, MerkleLL } from "../../src/periphery/types/DataTypes.sol"; @@ -37,6 +38,7 @@ contract CreateMerkleLL is BaseScript { cliffDuration: 30 days, totalDuration: 90 days }), + unlockAmounts: LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }), aggregateAmount: 10_000e18, recipientCount: 100 }); diff --git a/src/core/SablierLockup.sol b/src/core/SablierLockup.sol index 2c0fdefc9..f0e184e58 100644 --- a/src/core/SablierLockup.sol +++ b/src/core/SablierLockup.sol @@ -45,6 +45,9 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { /// @dev Stream tranches mapped by stream IDs. This is used in Lockup Tranched models. mapping(uint256 streamId => LockupTranched.Tranche[] tranches) internal _tranches; + /// @dev Unlock amounts mapped by stream IDs. This is used in Lockup Linear models. + mapping(uint256 streamId => LockupLinear.UnlockAmounts unlockAmounts) internal _unlockAmounts; + /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ @@ -108,6 +111,17 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { tranches = _tranches[streamId]; } + /// @inheritdoc ISablierLockup + function getUnlockAmounts(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupLinear.UnlockAmounts memory unlockAmounts) + { + unlockAmounts = _unlockAmounts[streamId]; + } + /*////////////////////////////////////////////////////////////////////////// USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -148,6 +162,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { /// @inheritdoc ISablierLockup function createWithDurationsLL( Lockup.CreateWithDurations calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, LockupLinear.Durations calldata durations ) external @@ -172,17 +187,20 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { // Checks, Effects and Interactions: create the stream. streamId = _createLL( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - totalAmount: params.totalAmount, - asset: params.asset, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: timestamps, - broker: params.broker - }), - cliffTime + CreateLLParams( + Lockup.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + asset: params.asset, + cancelable: params.cancelable, + transferable: params.transferable, + timestamps: timestamps, + broker: params.broker + }), + unlockAmounts, + cliffTime + ) ); } @@ -236,6 +254,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { /// @inheritdoc ISablierLockup function createWithTimestampsLL( Lockup.CreateWithTimestamps calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, uint40 cliffTime ) external @@ -244,7 +263,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { returns (uint256 streamId) { // Checks, Effects and Interactions: create the stream. - streamId = _createLL(params, cliffTime); + streamId = _createLL(CreateLLParams(params, unlockAmounts, cliffTime)); } /// @inheritdoc ISablierLockup @@ -267,17 +286,18 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { /// @inheritdoc SablierLockupBase function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { + Lockup.Timestamps memory timestamps = + Lockup.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); + // If the start time is in the future, return zero. uint40 blockTimestamp = uint40(block.timestamp); - uint40 startTime = _streams[streamId].startTime; - if (startTime >= blockTimestamp) { + if (timestamps.start >= blockTimestamp) { return 0; } // If the end time is not in the future, return the deposited amount. - uint40 endTime = _streams[streamId].endTime; uint128 depositedAmount = _streams[streamId].amounts.deposited; - if (endTime <= blockTimestamp) { + if (timestamps.end <= blockTimestamp) { return depositedAmount; } @@ -288,7 +308,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({ segments: _segments[streamId], - startTime: startTime, + startTime: timestamps.start, withdrawnAmount: _streams[streamId].amounts.withdrawn }); } @@ -296,9 +316,10 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { streamedAmount = VestingMath.calculateLockupLinearStreamedAmount({ depositedAmount: depositedAmount, - startTime: startTime, + startTime: timestamps.start, cliffTime: _cliffs[streamId], - endTime: endTime, + endTime: timestamps.end, + unlockAmounts: _unlockAmounts[streamId], withdrawnAmount: _streams[streamId].amounts.withdrawn }); } @@ -408,36 +429,47 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { }); } + struct CreateLLParams { + Lockup.CreateWithTimestamps createWithTimestamps; + LockupLinear.UnlockAmounts unlockAmounts; + uint40 cliffTime; + } + /// @dev See the documentation for the user-facing functions that call this internal function. - function _createLL( - Lockup.CreateWithTimestamps memory params, - uint40 cliffTime - ) - internal - returns (uint256 streamId) - { + function _createLL(CreateLLParams memory params) internal returns (uint256 streamId) { // Check: validate the user-provided parameters and cliff time. Lockup.CreateAmounts memory createAmounts = Helpers.checkCreateLockupLinear({ - sender: params.sender, - timestamps: params.timestamps, - cliffTime: cliffTime, - totalAmount: params.totalAmount, - brokerFee: params.broker.fee, + sender: params.createWithTimestamps.sender, + timestamps: params.createWithTimestamps.timestamps, + cliffTime: params.cliffTime, + totalAmount: params.createWithTimestamps.totalAmount, + unlockAmounts: params.unlockAmounts, + brokerFee: params.createWithTimestamps.broker.fee, maxBrokerFee: MAX_BROKER_FEE }); // Load the stream ID in a variable. streamId = nextStreamId; + // Effect: set the start unlock amount if its non-zero. + if (params.unlockAmounts.start > 0) { + _unlockAmounts[streamId].start = params.unlockAmounts.start; + } + // Effect: update cliff time if its non-zero. - if (cliffTime > 0) { - _cliffs[streamId] = cliffTime; + if (params.cliffTime > 0) { + _cliffs[streamId] = params.cliffTime; + + // Effect: set the cliff unlock amount if its non-zero. + if (params.unlockAmounts.cliff > 0) { + _unlockAmounts[streamId].cliff = params.unlockAmounts.cliff; + } } // Effect: create the stream, mint the NFT and transfer the deposit amount. _create({ streamId: streamId, - params: params, + params: params.createWithTimestamps, createAmounts: createAmounts, lockupModel: Lockup.Model.LOCKUP_LINEAR }); @@ -446,15 +478,16 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { emit ISablierLockup.CreateLockupLinearStream({ streamId: streamId, funder: msg.sender, - sender: params.sender, - recipient: params.recipient, + sender: params.createWithTimestamps.sender, + recipient: params.createWithTimestamps.recipient, amounts: createAmounts, - asset: params.asset, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: params.timestamps, - cliffTime: cliffTime, - broker: params.broker.account + asset: params.createWithTimestamps.asset, + cancelable: params.createWithTimestamps.cancelable, + transferable: params.createWithTimestamps.transferable, + timestamps: params.createWithTimestamps.timestamps, + cliffTime: params.cliffTime, + unlockAmounts: params.unlockAmounts, + broker: params.createWithTimestamps.broker.account }); } diff --git a/src/core/interfaces/ISablierLockup.sol b/src/core/interfaces/ISablierLockup.sol index 80e100f1c..b54a79eb1 100644 --- a/src/core/interfaces/ISablierLockup.sol +++ b/src/core/interfaces/ISablierLockup.sol @@ -52,6 +52,8 @@ interface ISablierLockup is ISablierLockupBase { /// @param transferable Boolean indicating whether the stream NFT is transferable or not. /// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, all as Unix timestamps. /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. + /// @param unlockAmounts Struct encapsulating (i) the amount unlocked at the start time and (ii) the amount unlocked + /// at the cliff time. /// @param broker The address of the broker who has helped create the stream, e.g. a front-end website. event CreateLockupLinearStream( uint256 streamId, @@ -64,6 +66,7 @@ interface ISablierLockup is ISablierLockupBase { bool transferable, Lockup.Timestamps timestamps, uint40 cliffTime, + LockupLinear.UnlockAmounts unlockAmounts, address broker ); @@ -110,13 +113,24 @@ interface ISablierLockup is ISablierLockupBase { /// @notice Retrieves the segments used to compose the dynamic distribution function. /// @dev Reverts if `streamId` references a null stream or a non Lockup Dynamic stream. /// @param streamId The stream ID for the query. + /// @return segments See the documentation in {DataTypes}. function getSegments(uint256 streamId) external view returns (LockupDynamic.Segment[] memory segments); /// @notice Retrieves the tranches used to compose the tranched distribution function. /// @dev Reverts if `streamId` references a null stream or a non Lockup Tranched stream. /// @param streamId The stream ID for the query. + /// @return tranches See the documentation in {DataTypes}. function getTranches(uint256 streamId) external view returns (LockupTranched.Tranche[] memory tranches); + /// @notice Retrieves the unlock amounts used to compose the linear distribution function. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + /// @return unlockAmounts See the documentation in {DataTypes}. + function getUnlockAmounts(uint256 streamId) + external + view + returns (LockupLinear.UnlockAmounts memory unlockAmounts); + /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -152,9 +166,12 @@ interface ISablierLockup is ISablierLockupBase { /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. /// @param durations Struct encapsulating (i) cliff period duration and (ii) total stream duration, both in seconds. + /// @param unlockAmounts Struct encapsulating (i) the amount unlocked at the start time and (ii) the amount unlocked + /// at the cliff time. /// @return streamId The ID of the newly created stream. function createWithDurationsLL( Lockup.CreateWithDurations calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, LockupLinear.Durations calldata durations ) external @@ -230,13 +247,19 @@ interface ISablierLockup is ISablierLockupBase { /// `params.timestamps.end`. /// - `params.recipient` must not be the zero address. /// - `params.sender` must not be the zero address. + /// - The sum of `params.unlockAmounts.start` and `params.unlockAmounts.cliff` must be less than or equal to + /// deposit amount. + /// - If `params.timestamps.cliff` not set, the `params.unlockAmounts.cliff` must be zero. /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` assets. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. /// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. + /// @param unlockAmounts Struct encapsulating (i) the amount unlocked at the start time and (ii) the amount unlocked + /// at the cliff time. /// @return streamId The ID of the newly created stream. function createWithTimestampsLL( Lockup.CreateWithTimestamps calldata params, + LockupLinear.UnlockAmounts calldata unlockAmounts, uint40 cliffTime ) external diff --git a/src/core/libraries/Errors.sol b/src/core/libraries/Errors.sol index 2dbe9d376..935b72609 100644 --- a/src/core/libraries/Errors.sol +++ b/src/core/libraries/Errors.sol @@ -39,6 +39,9 @@ library Errors { /// @notice Thrown when trying to create a linear stream with a cliff time not strictly less than the end time. error SablierHelpers_CliffTimeNotLessThanEndTime(uint40 cliffTime, uint40 endTime); + /// @notice Thrown when trying to create a stream with a non zero cliff unlock amount when the cliff time is zero. + error SablierHelpers_CliffTimeZeroUnlockAmountNotZero(uint128 cliffUnlockAmount); + /// @notice Thrown when trying to create a dynamic stream with a deposit amount not equal to the sum of the segment /// amounts. error SablierHelpers_DepositAmountNotEqualToSegmentAmountsSum(uint128 depositAmount, uint128 segmentAmountsSum); @@ -95,6 +98,11 @@ library Errors { /// @notice Thrown when trying to create a tranched stream with unordered tranche timestamps. error SablierHelpers_TrancheTimestampsNotOrdered(uint256 index, uint40 previousTimestamp, uint40 currentTimestamp); + /// @notice Thrown when trying to create a stream with the sum of the unlock amounts greater than deposit amount. + error SablierHelpers_UnlockAmountsSumTooHigh( + uint128 depositAmount, uint128 startUnlockAmount, uint128 cliffUnlockAmount + ); + /*////////////////////////////////////////////////////////////////////////// SABLIER-LOCKUP-BASE //////////////////////////////////////////////////////////////////////////*/ diff --git a/src/core/libraries/Helpers.sol b/src/core/libraries/Helpers.sol index e9af67208..6f88aef3a 100644 --- a/src/core/libraries/Helpers.sol +++ b/src/core/libraries/Helpers.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.22; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { Lockup, LockupDynamic, LockupTranched } from "./../types/DataTypes.sol"; +import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "./../types/DataTypes.sol"; import { Errors } from "./Errors.sol"; /// @title Helpers @@ -106,6 +106,7 @@ library Helpers { Lockup.Timestamps memory timestamps, uint40 cliffTime, uint128 totalAmount, + LockupLinear.UnlockAmounts memory unlockAmounts, UD60x18 brokerFee, UD60x18 maxBrokerFee ) @@ -120,7 +121,7 @@ library Helpers { _checkCreateStream(sender, createAmounts.deposit, timestamps.start); // Check: validate the user-provided cliff and end times. - _checkCliffAndEndTime(timestamps, cliffTime); + _checkTimestampsAndUnlockAmounts(createAmounts.deposit, timestamps, cliffTime, unlockAmounts); } /// @dev Checks the parameters of the {SablierLockup-_createLT} function. @@ -187,8 +188,16 @@ library Helpers { amounts.deposit = totalAmount - amounts.brokerFee; } - /// @dev Checks the user-provided cliff and end times of a lockup linear stream. - function _checkCliffAndEndTime(Lockup.Timestamps memory timestamps, uint40 cliffTime) private pure { + /// @dev Checks the user-provided cliff, end times and unlock amounts of a lockup linear stream. + function _checkTimestampsAndUnlockAmounts( + uint128 depositAmount, + Lockup.Timestamps memory timestamps, + uint40 cliffTime, + LockupLinear.UnlockAmounts memory unlockAmounts + ) + private + pure + { // Since a cliff time of zero means there is no cliff, the following checks are performed only if it's not zero. if (cliffTime > 0) { // Check: the start time is strictly less than the cliff time. @@ -201,11 +210,22 @@ library Helpers { revert Errors.SablierHelpers_CliffTimeNotLessThanEndTime(cliffTime, timestamps.end); } } + // Check: the cliff unlock amount is zero when the cliff time is zero. + else if (unlockAmounts.cliff > 0) { + revert Errors.SablierHelpers_CliffTimeZeroUnlockAmountNotZero(unlockAmounts.cliff); + } // Check: the start time is strictly less than the end time. if (timestamps.start >= timestamps.end) { revert Errors.SablierHelpers_StartTimeNotLessThanEndTime(timestamps.start, timestamps.end); } + + // Check: the sum of the start and cliff unlock amounts is not greater than deposit amount. + if (unlockAmounts.start + unlockAmounts.cliff > depositAmount) { + revert Errors.SablierHelpers_UnlockAmountsSumTooHigh( + depositAmount, unlockAmounts.start, unlockAmounts.cliff + ); + } } /// @dev Checks the user-provided common parameters across lockup streams. diff --git a/src/core/libraries/VestingMath.sol b/src/core/libraries/VestingMath.sol index f45f7ea08..68634882d 100644 --- a/src/core/libraries/VestingMath.sol +++ b/src/core/libraries/VestingMath.sol @@ -5,7 +5,7 @@ import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/U import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uint40.sol"; import { SD59x18 } from "@prb/math/src/SD59x18.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { LockupDynamic, LockupTranched } from "./../types/DataTypes.sol"; +import { LockupDynamic, LockupLinear, LockupTranched } from "./../types/DataTypes.sol"; /// @title VestingMath /// @notice Library with functions needed to calculate vested amount across lockup streams. @@ -99,19 +99,21 @@ library VestingMath { /// @dev Lockup linear model uses the following distribution function: /// /// $$ - /// f(x) = x * d + c + /// f(x) = x * sa + s + c /// $$ /// /// Where: /// - /// - $x$ is the elapsed time divided by the stream's total duration. - /// - $d$ is the deposited amount. - /// - $c$ is the cliff amount. + /// - $x$ is the elapsed time in the streamable range divided by the streamable total duration. + /// - $sa$ is the streamable amount, i.e. deposited amount minus unlock amounts' sum. + /// - $s$ is the start unlock amount. + /// - $c$ is the cliff unlock amount. function calculateLockupLinearStreamedAmount( uint128 depositedAmount, uint40 startTime, uint40 cliffTime, uint40 endTime, + LockupLinear.UnlockAmounts memory unlockAmounts, uint128 withdrawnAmount ) public @@ -120,36 +122,48 @@ library VestingMath { { uint256 blockTimestamp = block.timestamp; - // If the cliff time is in the future, return zero. + // If the cliff time is in the future, return the start unlock amount. if (cliffTime > blockTimestamp) { - return 0; + return unlockAmounts.start; } - // In all other cases, calculate the amount streamed so far. Normalization to 18 decimals is not needed - // because there is no mix of amounts with different decimals. unchecked { - // Calculate how much time has passed since the stream started, and the stream's total duration. - UD60x18 elapsedTime = ud(blockTimestamp - startTime); - UD60x18 totalDuration = ud(endTime - startTime); + uint128 unlockAmountsSum = unlockAmounts.start + unlockAmounts.cliff; + + // If the sum of the unlock amounts is greater than or equal to the deposited amount, return the deposited + // amount. The ">=" operator is used as a safety measure in case of a bug, as the sum of the unlock amounts + // should never exceed the deposited amount. + if (unlockAmountsSum >= depositedAmount) { + return depositedAmount; + } - // Divide the elapsed time by the stream's total duration. - UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration); + UD60x18 elapsedTime; + UD60x18 streamableDuration; + + // Calculate the streamable range. + if (cliffTime == 0) { + elapsedTime = ud(blockTimestamp - startTime); + streamableDuration = ud(endTime - startTime); + } else { + elapsedTime = ud(blockTimestamp - cliffTime); + streamableDuration = ud(endTime - cliffTime); + } - // Cast the deposited amount to UD60x18. - UD60x18 depositedAmountUD60x18 = ud(depositedAmount); + UD60x18 elapsedTimePercentage = elapsedTime.div(streamableDuration); + UD60x18 streamableAmount = ud(depositedAmount - unlockAmountsSum); - // Calculate the streamed amount by multiplying the elapsed time percentage by the deposited amount. - UD60x18 streamedAmount = elapsedTimePercentage.mul(depositedAmountUD60x18); + // The streamed amount is the sum of the unlock amounts plus the product of elapsed time percentage and + // streamable amount. + uint128 streamedAmount = unlockAmountsSum + (elapsedTimePercentage.mul(streamableAmount)).intoUint128(); // Although the streamed amount should never exceed the deposited amount, this condition is checked // without asserting to avoid locking assets in case of a bug. If this situation occurs, the withdrawn // amount is considered to be the streamed amount, and the stream is effectively frozen. - if (streamedAmount.gt(depositedAmountUD60x18)) { + if (streamedAmount > depositedAmount) { return withdrawnAmount; } - // Cast the streamed amount to uint128. This is safe due to the check above. - return uint128(streamedAmount.intoUint256()); + return streamedAmount; } } diff --git a/src/core/types/DataTypes.sol b/src/core/types/DataTypes.sol index 8808aa4d2..8e80e5ca2 100644 --- a/src/core/types/DataTypes.sol +++ b/src/core/types/DataTypes.sol @@ -195,6 +195,16 @@ library LockupLinear { uint40 cliff; uint40 total; } + + /// @notice Struct encapsulating the unlock amounts for the stream. + /// @dev The sum of `start` and `cliff` must be less than or equal to deposit amount. Both amounts can be zero. + /// @param start The amount to be unlocked at the start time. + /// @param cliff The amount to be unlocked at the cliff time. + struct UnlockAmounts { + // slot 0 + uint128 start; + uint128 cliff; + } } /// @notice Namespace for the structs used only in Lockup Tranched model. diff --git a/src/periphery/SablierBatchLockup.sol b/src/periphery/SablierBatchLockup.sol index 2797839f5..274e0d2f3 100644 --- a/src/periphery/SablierBatchLockup.sol +++ b/src/periphery/SablierBatchLockup.sol @@ -171,6 +171,7 @@ contract SablierBatchLockup is ISablierBatchLockup { transferable: batch[i].transferable, broker: batch[i].broker }), + batch[i].unlockAmounts, batch[i].durations ); } @@ -220,6 +221,7 @@ contract SablierBatchLockup is ISablierBatchLockup { timestamps: batch[i].timestamps, broker: batch[i].broker }), + batch[i].unlockAmounts, batch[i].cliffTime ); } diff --git a/src/periphery/SablierMerkleFactory.sol b/src/periphery/SablierMerkleFactory.sol index 3cd80bff1..3adcb4539 100644 --- a/src/periphery/SablierMerkleFactory.sol +++ b/src/periphery/SablierMerkleFactory.sol @@ -5,6 +5,7 @@ import { uUNIT } from "@prb/math/src/UD2x18.sol"; import { Adminable } from "../core/abstracts/Adminable.sol"; import { ISablierLockup } from "../core/interfaces/ISablierLockup.sol"; +import { LockupLinear } from "../core/types/DataTypes.sol"; import { ISablierMerkleBase } from "./interfaces/ISablierMerkleBase.sol"; import { ISablierMerkleFactory } from "./interfaces/ISablierMerkleFactory.sol"; @@ -160,6 +161,7 @@ contract SablierMerkleFactory is bool cancelable, bool transferable, MerkleLL.Schedule memory schedule, + LockupLinear.UnlockAmounts memory unlockAmounts, uint256 aggregateAmount, uint256 recipientCount ) @@ -180,7 +182,8 @@ contract SablierMerkleFactory is lockup, cancelable, transferable, - abi.encode(schedule) + abi.encode(schedule), + abi.encode(unlockAmounts) ) ); @@ -188,7 +191,9 @@ contract SablierMerkleFactory is uint256 sablierFee = _computeSablierFeeForUser(msg.sender); // Deploy the MerkleLL contract with CREATE2. - merkleLL = new SablierMerkleLL{ salt: salt }(baseParams, lockup, cancelable, transferable, schedule, sablierFee); + merkleLL = new SablierMerkleLL{ salt: salt }( + baseParams, lockup, cancelable, transferable, schedule, unlockAmounts, sablierFee + ); // Log the creation of the MerkleLL contract, including some metadata that is not stored on-chain. emit CreateMerkleLL( @@ -198,6 +203,7 @@ contract SablierMerkleFactory is cancelable, transferable, schedule, + unlockAmounts, aggregateAmount, recipientCount, sablierFee diff --git a/src/periphery/SablierMerkleLL.sol b/src/periphery/SablierMerkleLL.sol index 92a27b914..5959736d2 100644 --- a/src/periphery/SablierMerkleLL.sol +++ b/src/periphery/SablierMerkleLL.sol @@ -6,7 +6,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { ud } from "@prb/math/src/UD60x18.sol"; import { ISablierLockup } from "../core/interfaces/ISablierLockup.sol"; -import { Broker, Lockup } from "../core/types/DataTypes.sol"; +import { Broker, Lockup, LockupLinear } from "../core/types/DataTypes.sol"; import { SablierMerkleBase } from "./abstracts/SablierMerkleBase.sol"; import { ISablierMerkleLL } from "./interfaces/ISablierMerkleLL.sol"; @@ -36,6 +36,9 @@ contract SablierMerkleLL is /// @inheritdoc ISablierMerkleLL MerkleLL.Schedule public override schedule; + /// @inheritdoc ISablierMerkleLL + LockupLinear.UnlockAmounts public override unlockAmounts; + /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ @@ -48,6 +51,7 @@ contract SablierMerkleLL is bool cancelable, bool transferable, MerkleLL.Schedule memory schedule_, + LockupLinear.UnlockAmounts memory unlockAmounts_, uint256 sablierFee ) SablierMerkleBase(baseParams, sablierFee) @@ -56,6 +60,7 @@ contract SablierMerkleLL is LOCKUP = lockup; TRANSFERABLE = transferable; schedule = schedule_; + unlockAmounts = unlockAmounts_; // Max approve the Lockup contract to spend funds from the MerkleLL contract. ASSET.forceApprove(address(LOCKUP), type(uint256).max); @@ -98,6 +103,7 @@ contract SablierMerkleLL is timestamps: timestamps, broker: Broker({ account: address(0), fee: ud(0) }) }), + unlockAmounts, cliffTime ); diff --git a/src/periphery/interfaces/ISablierMerkleFactory.sol b/src/periphery/interfaces/ISablierMerkleFactory.sol index 47fb2ed24..6024f9484 100644 --- a/src/periphery/interfaces/ISablierMerkleFactory.sol +++ b/src/periphery/interfaces/ISablierMerkleFactory.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.22; import { IAdminable } from "../../core/interfaces/IAdminable.sol"; import { ISablierLockup } from "../../core/interfaces/ISablierLockup.sol"; +import { LockupLinear } from "../../core/types/DataTypes.sol"; import { ISablierMerkleBase } from "../interfaces/ISablierMerkleBase.sol"; import { MerkleBase, MerkleFactory, MerkleLL, MerkleLT } from "../types/DataTypes.sol"; @@ -39,6 +40,7 @@ interface ISablierMerkleFactory is IAdminable { bool cancelable, bool transferable, MerkleLL.Schedule schedule, + LockupLinear.UnlockAmounts unlockAmounts, uint256 aggregateAmount, uint256 recipientCount, uint256 sablierFee @@ -142,6 +144,7 @@ interface ISablierMerkleFactory is IAdminable { bool cancelable, bool transferable, MerkleLL.Schedule memory schedule, + LockupLinear.UnlockAmounts memory unlockAmounts, uint256 aggregateAmount, uint256 recipientCount ) diff --git a/src/periphery/interfaces/ISablierMerkleLL.sol b/src/periphery/interfaces/ISablierMerkleLL.sol index 2390bf022..0cfcffd55 100644 --- a/src/periphery/interfaces/ISablierMerkleLL.sol +++ b/src/periphery/interfaces/ISablierMerkleLL.sol @@ -34,4 +34,8 @@ interface ISablierMerkleLL is ISablierMerkleBase { /// `Lockup.CreateWithTimestampsLL`. /// @dev A start time value of zero will be considered as `block.timestamp`. function schedule() external view returns (uint40 startTime, uint40 cliffDuration, uint40 endDuration); + + /// @notice The unlock aomunts used to calculate the streamed amount in + /// {VestingMath.calculateLockupLinearStreamedAmount}. + function unlockAmounts() external view returns (uint128 startUnlockAmount, uint128 cliffUnlockAmount); } diff --git a/src/periphery/types/DataTypes.sol b/src/periphery/types/DataTypes.sol index 4670bac9b..c74084a2e 100644 --- a/src/periphery/types/DataTypes.sol +++ b/src/periphery/types/DataTypes.sol @@ -26,6 +26,7 @@ library BatchLockup { bool cancelable; bool transferable; LockupLinear.Durations durations; + LockupLinear.UnlockAmounts unlockAmounts; Broker broker; } @@ -61,6 +62,7 @@ library BatchLockup { bool transferable; Lockup.Timestamps timestamps; uint40 cliffTime; + LockupLinear.UnlockAmounts unlockAmounts; Broker broker; } diff --git a/test/core/fork/LockupLinear.t.sol b/test/core/fork/LockupLinear.t.sol index f4521b0ab..75abf8861 100644 --- a/test/core/fork/LockupLinear.t.sol +++ b/test/core/fork/LockupLinear.t.sol @@ -7,7 +7,7 @@ import { ud } from "@prb/math/src/UD60x18.sol"; import { Solarray } from "solarray/src/Solarray.sol"; import { ISablierLockup } from "src/core/interfaces/ISablierLockup.sol"; import { ISablierLockupBase } from "src/core/interfaces/ISablierLockupBase.sol"; -import { Broker, Lockup } from "src/core/types/DataTypes.sol"; +import { Broker, Lockup, LockupLinear } from "src/core/types/DataTypes.sol"; import { Fork_Test } from "./Fork.t.sol"; abstract contract Lockup_Linear_Fork_Test is Fork_Test { @@ -41,6 +41,7 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { uint128 withdrawAmount; uint40 warpTimestamp; Lockup.Timestamps timestamps; + LockupLinear.UnlockAmounts unlockAmounts; uint40 cliffTime; Broker broker; } @@ -67,6 +68,7 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { bool isDepleted; bool isSettled; uint256 streamId; + uint128 streamedAmount; // Create vars uint256 actualBrokerBalance; uint256 actualNextStreamId; @@ -126,15 +128,25 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { // The cliff time must be either zero or greater than the start time. vars.hasCliff = params.cliffTime > 0; - if (vars.hasCliff) { - params.cliffTime = - boundUint40(params.cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks); - } + params.cliffTime = vars.hasCliff + ? boundUint40(params.cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks) + : 0; + // Bound the end time so that it is always greater than the start time, and the cliff time. vars.endTimeLowerBound = maxOfTwo(params.timestamps.start, params.cliffTime); params.timestamps.end = boundUint40(params.timestamps.end, vars.endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); + // Calculate the broker fee amount and the deposit amount. + vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); + vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; + + // Bound the unlock amounts. + params.unlockAmounts.start = boundUint128(params.unlockAmounts.start, 0, vars.createAmounts.deposit); + params.unlockAmounts.cliff = vars.hasCliff + ? boundUint128(params.unlockAmounts.cliff, 0, vars.createAmounts.deposit - params.unlockAmounts.start) + : 0; + // Make the holder the caller. resetPrank(FORK_ASSET_HOLDER); @@ -148,13 +160,9 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { vars.initialLockupBalance = vars.balances[0]; vars.initialBrokerBalance = vars.balances[1]; - // Calculate the broker fee amount and the deposit amount. - vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); - vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; - vars.streamId = lockup.nextStreamId(); - // Expect the relevant events to be emitted. + // // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); vm.expectEmit({ emitter: address(lockup) }); @@ -169,6 +177,7 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { transferable: true, timestamps: params.timestamps, cliffTime: params.cliffTime, + unlockAmounts: params.unlockAmounts, broker: params.broker.account }); @@ -184,14 +193,29 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { timestamps: params.timestamps, broker: params.broker }), + params.unlockAmounts, params.cliffTime ); + vars.streamedAmount = calculateLockupLinearStreamedAmount( + params.timestamps.start, + params.cliffTime, + params.timestamps.end, + vars.createAmounts.deposit, + params.unlockAmounts + ); + // Check if the stream is settled. It is possible for a Lockup Linear stream to settle at the time of creation - // in case end time is in the past. - vars.isSettled = params.timestamps.end <= vars.blockTimestamp; + // in case 1. the start unlock amount equals the deposited amount 2. end time is in the past. + if (vars.streamedAmount == vars.createAmounts.deposit) { + vars.isSettled = true; + } else { + vars.isSettled = false; + } vars.isCancelable = vars.isSettled ? false : true; + lockup.statusOf(vars.streamId); + // Assert that the stream has been created. assertEq(lockup.getDepositedAmount(vars.streamId), vars.createAmounts.deposit, "depositedAmount"); assertEq(lockup.getAsset(vars.streamId), FORK_ASSET, "asset"); @@ -205,6 +229,8 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); assertEq(lockup.getStartTime(vars.streamId), params.timestamps.start, "startTime"); assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); + assertEq(lockup.getUnlockAmounts(vars.streamId).start, params.unlockAmounts.start, "unlockAmounts.start"); + assertEq(lockup.getUnlockAmounts(vars.streamId).cliff, params.unlockAmounts.cliff, "unlockAmounts.cliff"); // Assert that the stream's status is correct. vars.actualStatus = lockup.statusOf(vars.streamId); diff --git a/test/core/fork/NFTDescriptor.t.sol b/test/core/fork/NFTDescriptor.t.sol index ca4370425..4beca5108 100644 --- a/test/core/fork/NFTDescriptor.t.sol +++ b/test/core/fork/NFTDescriptor.t.sol @@ -56,7 +56,7 @@ contract NFTDescriptor_Fork_Test is Fork_Test { // TODO: Add the deployment addresses for Lockup v1.3.0. // Deploy some streams temporarity for the test resetPrank({ msgSender: users.sender }); - lockup.createWithDurationsLL(defaults.createWithDurations(), defaults.durations()); + lockup.createWithDurationsLL(defaults.createWithDurations(), defaults.unlockAmounts(), defaults.durations()); _; } diff --git a/test/core/integration/Integration.t.sol b/test/core/integration/Integration.t.sol index ef8143667..b5551064d 100644 --- a/test/core/integration/Integration.t.sol +++ b/test/core/integration/Integration.t.sol @@ -47,6 +47,7 @@ abstract contract Integration_Test is Base_Test { Lockup.CreateWithTimestamps createWithTimestamps; Lockup.CreateWithDurations createWithDurations; uint40 cliffTime; + LockupLinear.UnlockAmounts unlockAmounts; LockupLinear.Durations durations; LockupDynamic.Segment[] segments; LockupDynamic.SegmentWithDuration[] segmentsWithDurations; @@ -88,6 +89,7 @@ abstract contract Integration_Test is Base_Test { _defaultParams.createWithDurations = defaults.createWithDurations(); _defaultParams.cliffTime = defaults.CLIFF_TIME(); _defaultParams.durations = defaults.durations(); + _defaultParams.unlockAmounts = defaults.unlockAmounts(); // See https://github.com/ethereum/solidity/issues/12783 LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations = defaults.segmentsWithDurations(); @@ -129,7 +131,7 @@ abstract contract Integration_Test is Base_Test { if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { streamId = lockup.createWithTimestampsLD(params, _defaultParams.segments); } else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { - streamId = lockup.createWithTimestampsLL(params, _defaultParams.cliffTime); + streamId = lockup.createWithTimestampsLL(params, _defaultParams.unlockAmounts, _defaultParams.cliffTime); } else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { streamId = lockup.createWithTimestampsLT(params, _defaultParams.tranches); } @@ -156,7 +158,9 @@ abstract contract Integration_Test is Base_Test { streamId = lockup.createWithDurationsLD(_defaultParams.createWithDurations, _defaultParams.segmentsWithDurations); } else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { - streamId = lockup.createWithDurationsLL(_defaultParams.createWithDurations, _defaultParams.durations); + streamId = lockup.createWithDurationsLL( + _defaultParams.createWithDurations, _defaultParams.unlockAmounts, _defaultParams.durations + ); } else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { streamId = lockup.createWithDurationsLT(_defaultParams.createWithDurations, _defaultParams.tranchesWithDurations); @@ -209,7 +213,7 @@ abstract contract Integration_Test is Base_Test { } function expectRevert_CANCELEDStatus(bytes memory callData) internal { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); (bool success, bytes memory returnData) = address(lockup).call(callData); diff --git a/test/core/integration/concrete/lockup-base/burn/burn.t.sol b/test/core/integration/concrete/lockup-base/burn/burn.t.sol index 0a89841e5..2dc0d4357 100644 --- a/test/core/integration/concrete/lockup-base/burn/burn.t.sol +++ b/test/core/integration/concrete/lockup-base/burn/burn.t.sol @@ -35,7 +35,7 @@ contract Burn_Integration_Concrete_Test is Integration_Test { } function test_RevertGiven_CANCELEDStatus() external whenNoDelegateCall givenNotNull givenNotDepletedStream { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); resetPrank({ msgSender: users.sender }); lockup.cancel(defaultStreamId); resetPrank({ msgSender: users.recipient }); diff --git a/test/core/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol b/test/core/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol index cd30622cf..01075b9d1 100644 --- a/test/core/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol +++ b/test/core/integration/concrete/lockup-base/create-with-timestamps/createWithTimestamps.t.sol @@ -56,7 +56,8 @@ abstract contract CreateWithTimestamps_Integration_Concrete_Test is Integration_ ); } else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { callData = abi.encodeCall( - lockup.createWithTimestampsLL, (_defaultParams.createWithTimestamps, _defaultParams.cliffTime) + lockup.createWithTimestampsLL, + (_defaultParams.createWithTimestamps, _defaultParams.unlockAmounts, _defaultParams.cliffTime) ); } else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { callData = abi.encodeCall( diff --git a/test/core/integration/concrete/lockup-base/getters/getters.t.sol b/test/core/integration/concrete/lockup-base/getters/getters.t.sol index d276ad73a..ea1b1315c 100644 --- a/test/core/integration/concrete/lockup-base/getters/getters.t.sol +++ b/test/core/integration/concrete/lockup-base/getters/getters.t.sol @@ -91,7 +91,7 @@ contract Getters_Integration_Concrete_Test is Integration_Test { } function test_GetRefundedAmountGivenCanceledStreamAndCANCELEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Cancel the stream. lockup.cancel(defaultStreamId); @@ -102,7 +102,7 @@ contract Getters_Integration_Concrete_Test is Integration_Test { } function test_GetRefundedAmountGivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Cancel the stream. lockup.cancel(defaultStreamId); @@ -268,7 +268,7 @@ contract Getters_Integration_Concrete_Test is Integration_Test { } function test_IsColdGivenCANCELEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); assertTrue(lockup.isCold(defaultStreamId), "isCold"); } @@ -349,7 +349,7 @@ contract Getters_Integration_Concrete_Test is Integration_Test { } function test_IsWarmGivenCANCELEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); assertFalse(lockup.isWarm(defaultStreamId), "isWarm"); } diff --git a/test/core/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol b/test/core/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol index 9fa46c6e4..2fb5f383c 100644 --- a/test/core/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-base/refundable-amount-of/refundableAmountOf.t.sol @@ -9,14 +9,14 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te } function test_GivenNonCancelableStream() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); uint128 actualRefundableAmount = lockup.refundableAmountOf(notCancelableStreamId); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); } function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull givenCancelableStream { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedRefundableAmount = 0; @@ -24,10 +24,10 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te } function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull givenCancelableStream { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 10 seconds }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); @@ -41,7 +41,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te } function test_GivenSTREAMINGStatus() external givenNotNull givenCancelableStream givenNotCanceledStream { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedReturnableAmount = defaults.REFUND_AMOUNT(); assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); diff --git a/test/core/integration/concrete/lockup-base/status-of/statusOf.t.sol b/test/core/integration/concrete/lockup-base/status-of/statusOf.t.sol index db35185eb..8196874a2 100644 --- a/test/core/integration/concrete/lockup-base/status-of/statusOf.t.sol +++ b/test/core/integration/concrete/lockup-base/status-of/statusOf.t.sol @@ -21,7 +21,7 @@ contract StatusOf_Integration_Concrete_Test is Integration_Test { } function test_GivenCanceledStream() external givenNotNull givenAssetsNotFullyWithdrawn { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); // It should return CANCELED. diff --git a/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol b/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol index a7a9db30b..8e9b2a0e5 100644 --- a/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.t.sol @@ -9,27 +9,27 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test } function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); // It should return the correct streamed amount. uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint256 expectedStreamedAmount = defaults.CLIFF_AMOUNT(); + uint256 expectedStreamedAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } /// @dev This test warps a second time to ensure that {streamedAmountOf} ignores the current time. function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); // Withdraw max to deplete the stream. lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 10 seconds }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); // It should return the correct streamed amount. uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = defaults.CLIFF_AMOUNT(); + uint128 expectedStreamedAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -43,6 +43,15 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test } function test_GivenSTREAMINGStatus() external givenNotNull givenNotCanceledStream { + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // It should return the correct streamed amount. + uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); + uint128 expectedStreamedAmount = defaults.WITHDRAW_AMOUNT(); + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + function test_GivenSETTLEDStatus() external givenNotNull givenNotCanceledStream { vm.warp({ newTimestamp: defaults.END_TIME() }); // It should return the deposited amount. @@ -56,7 +65,7 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is Integration_Test // Withdraw max to deplete the stream. lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - // It should return the correct streamed amount. + // It should return the deposited amount. uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); diff --git a/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree b/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree index 64942faf7..112eb6037 100644 --- a/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree +++ b/test/core/integration/concrete/lockup-base/streamed-amount-of/streamedAmountOf.tree @@ -10,6 +10,8 @@ StreamedAmountOf_Integration_Concrete_Test ├── given PENDING status │ └── it should return zero ├── given STREAMING status + │ └── it should the correct streamed amount + ├── given SETTLED status │ └── it should return the deposited amount └── given DEPLETED status - └── it should return the correct streamed amount + └── it should return the deposited amount diff --git a/test/core/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol b/test/core/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol index 9d5dfdfb3..a3c11bca7 100644 --- a/test/core/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol +++ b/test/core/integration/concrete/lockup-base/withdraw-multiple/withdrawMultiple.t.sol @@ -23,7 +23,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { function setUp() public virtual override { Integration_Test.setUp(); - originalTime = getBlockTimestamp(); + originalTime = defaults.START_TIME(); withdrawMultipleStreamIds = warpAndCreateStreams(defaults.START_TIME()); @@ -40,7 +40,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { // 2. A stream with an early end time // 3. A stream meant to be canceled before the withdrawal is made streamIds[0] = createDefaultStream(); - streamIds[1] = createDefaultStreamWithEndTimeLD(defaults.WARP_26_PERCENT()); + streamIds[1] = createDefaultStreamWithEndTimeLD(defaults.WARP_26_PERCENT() + 1); streamIds[2] = createDefaultStream(); } @@ -76,10 +76,10 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { whenNonZeroArrayLength { uint256[] memory streamIds = - Solarray.uint256s(withdrawMultipleStreamIds[0], withdrawMultipleStreamIds[1], nullStreamId); + Solarray.uint256s(nullStreamId, withdrawMultipleStreamIds[0], withdrawMultipleStreamIds[1]); // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 1 }); // It should revert. vm.expectRevert(abi.encodeWithSelector(Errors.SablierLockupBase_Null.selector, nullStreamId)); @@ -119,7 +119,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { givenNoDEPLETEDStreams { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 1 }); // Run the test. uint128[] memory amounts = Solarray.uint128s(defaults.WITHDRAW_AMOUNT(), 0, 0); @@ -141,7 +141,7 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { whenNoZeroAmounts { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 1 }); // Run the test. uint128 withdrawableAmount = lockup.withdrawableAmountOf(withdrawMultipleStreamIds[2]); @@ -184,9 +184,10 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { givenNoNullStreams givenNoDEPLETEDStreams whenNoZeroAmounts + whenCallerAuthorizedForAllStreams { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 1 }); // Cancel the 3rd stream. resetPrank({ msgSender: users.sender }); diff --git a/test/core/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/core/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol index c15deace5..aadb8491b 100644 --- a/test/core/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-base/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -9,20 +9,20 @@ abstract contract WithdrawableAmountOf_Integration_Concrete_Test is Integration_ } function test_GivenCanceledStreamAndCANCELEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); // It should return the correct withdrawable amount. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); - uint256 expectedWithdrawableAmount = defaults.CLIFF_AMOUNT(); + uint256 expectedWithdrawableAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenCanceledStreamAndDEPLETEDStatus() external givenNotNull { - vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); lockup.cancel(defaultStreamId); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 10 seconds }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 10 seconds }); // It should return zero. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); diff --git a/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol b/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol index 529cbb24b..a2b1c8b9d 100644 --- a/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol @@ -11,19 +11,6 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test is StreamedAm Integration_Test.setUp(); } - function test_GivenStartTimeInPresent() external givenSTREAMINGStatus { - vm.warp({ newTimestamp: defaults.START_TIME() }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - assertEq(actualStreamedAmount, 0, "streamedAmount"); - } - - function test_GivenEndTimeNotInFuture() external givenSTREAMINGStatus givenStartTimeInPast { - vm.warp({ newTimestamp: defaults.END_TIME() + 1 }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); - assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); - } - function test_GivenSingleSegment() external givenSTREAMINGStatus givenStartTimeInPast givenEndTimeInFuture { // Simulate the passage of time. vm.warp({ newTimestamp: defaults.START_TIME() + 2000 seconds }); @@ -46,12 +33,12 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test is StreamedAm } function test_GivenMultipleSegments() external givenSTREAMINGStatus givenStartTimeInPast givenEndTimeInFuture { - // Simulate the passage of time. 750 seconds is ~10% of the way in the second segment. - vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 750 seconds }); + // Simulate the passage of time. 740 seconds is ~10% of the way in the second segment. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 740 seconds }); // It should return the correct streamed amount. uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = defaults.segments()[0].amount + 2371.708245126284505e18; // ~7,500*0.1^{0.5} + uint128 expectedStreamedAmount = defaults.segments()[0].amount + 2340.0854685246007116e18; // ~7,400*0.1^{0.5} assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } } diff --git a/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree b/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree index 2d31b7ac5..bdf568c00 100644 --- a/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree +++ b/test/core/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree @@ -1,12 +1,6 @@ StreamedAmountOf_Lockup_Dynamic_Integration_Concrete_Test └── given STREAMING status - ├── given start time in present - │ └── it should return zero - └── given start time in past - ├── given end time not in future - │ └── it should return the deposited amount - └── given end time in future - ├── given single segment - │ └── it should return the correct streamed amount - └── given multiple segments - └── it should return the correct streamed amount + ├── given single segment + │ └── it should return the correct streamed amount + └── given multiple segments + └── it should return the correct streamed amount diff --git a/test/core/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/core/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol index 34ea60863..a20f60f08 100644 --- a/test/core/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -21,18 +21,18 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is function test_GivenNoPreviousWithdrawals() external givenSTREAMINGStatus givenStartTimeInPast { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 3750 seconds }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 3750 seconds }); // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); - // The second term is 7,500*0.5^{0.5} - uint128 expectedWithdrawableAmount = defaults.segments()[0].amount + 5303.30085889910643e18; + // The second term is 7,400*0.5^{0.5} + uint128 expectedWithdrawableAmount = defaults.segments()[0].amount + 5267.8268764263694426e18; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_GivenPreviousWithdrawal() external givenSTREAMINGStatus givenStartTimeInPast { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 3750 seconds }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() + 3750 seconds }); // Make the withdrawal. lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); @@ -42,7 +42,7 @@ contract WithdrawableAmountOf_Lockup_Dynamic_Integration_Concrete_Test is // The second term is 7,500*0.5^{0.5} uint128 expectedWithdrawableAmount = - defaults.segments()[0].amount + 5303.30085889910643e18 - defaults.WITHDRAW_AMOUNT(); + defaults.segments()[0].amount + 5267.8268764263694426e18 - defaults.WITHDRAW_AMOUNT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } } diff --git a/test/core/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol b/test/core/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol index 72ae65a01..d2fbb2473 100644 --- a/test/core/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol +++ b/test/core/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol @@ -13,7 +13,8 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr function test_RevertWhen_DelegateCall() external { expectRevert_DelegateCall({ callData: abi.encodeCall( - lockup.createWithDurationsLL, (_defaultParams.createWithDurations, _defaultParams.durations) + lockup.createWithDurationsLL, + (_defaultParams.createWithDurations, _defaultParams.unlockAmounts, _defaultParams.durations) ) }); } @@ -36,13 +37,13 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr } function test_WhenCliffTimeCalculationNotOverflow() external whenNoDelegateCall whenCliffDurationNotZero { - LockupLinear.Durations memory durations = defaults.durations(); - _test_CreateWithDurations(durations); + _test_CreateWithDurations(_defaultParams.durations); } function test_RevertWhen_EndTimeCalculationOverflows() external whenNoDelegateCall whenCliffDurationZero { uint40 startTime = getBlockTimestamp(); _defaultParams.durations = LockupLinear.Durations({ cliff: 0, total: MAX_UINT40 - startTime + 1 seconds }); + _defaultParams.unlockAmounts.cliff = 0; // Calculate the end time. Needs to be "unchecked" to allow an overflow. uint40 endTime; @@ -59,9 +60,8 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr } function test_WhenEndTimeCalculationNotOverflow() external whenNoDelegateCall whenCliffDurationZero { - LockupLinear.Durations memory durations = defaults.durations(); - durations.cliff = 0; - _test_CreateWithDurations(durations); + _defaultParams.durations.cliff = 0; + _test_CreateWithDurations(_defaultParams.durations); } function _test_CreateWithDurations(LockupLinear.Durations memory durations) private { @@ -77,6 +77,8 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr uint40 cliffTime; if (durations.cliff > 0) { cliffTime = blockTimestamp + durations.cliff; + } else { + _defaultParams.unlockAmounts.cliff = 0; } // It should perform the ERC-20 transfers. @@ -100,6 +102,7 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr transferable: true, timestamps: timestamps, cliffTime: cliffTime, + unlockAmounts: _defaultParams.unlockAmounts, broker: users.broker }); @@ -119,6 +122,8 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); assertEq(lockup.getCliffTime(streamId), cliffTime, "cliffTime"); + assertEq(lockup.getUnlockAmounts(streamId).start, _defaultParams.unlockAmounts.start, "unlockAmounts.start"); + assertEq(lockup.getUnlockAmounts(streamId).cliff, _defaultParams.unlockAmounts.cliff, "unlockAmounts.cliff"); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_LINEAR); // Assert that the stream's status is "STREAMING". diff --git a/test/core/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol b/test/core/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol index cfa7a9bf1..df5a626e6 100644 --- a/test/core/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol +++ b/test/core/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol @@ -31,15 +31,16 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp whenCliffTimeZero { uint40 startTime = defaults.END_TIME(); - uint40 cliffTime = 0; uint40 endTime = defaults.START_TIME(); _defaultParams.createWithTimestamps.timestamps.start = startTime; _defaultParams.createWithTimestamps.timestamps.end = endTime; + _defaultParams.cliffTime = 0; + _defaultParams.unlockAmounts.cliff = 0; vm.expectRevert( abi.encodeWithSelector(Errors.SablierHelpers_StartTimeNotLessThanEndTime.selector, startTime, endTime) ); - lockup.createWithTimestampsLL(_defaultParams.createWithTimestamps, cliffTime); + createDefaultStream(); } function test_WhenStartTimeLessThanEndTime() @@ -74,11 +75,12 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp _defaultParams.createWithTimestamps.timestamps.start = startTime; _defaultParams.createWithTimestamps.timestamps.end = endTime; + _defaultParams.cliffTime = cliffTime; vm.expectRevert( abi.encodeWithSelector(Errors.SablierHelpers_StartTimeNotLessThanCliffTime.selector, startTime, cliffTime) ); - lockup.createWithTimestampsLL(_defaultParams.createWithTimestamps, cliffTime); + createDefaultStream(); } function test_RevertWhen_CliffTimeNotLessThanEndTime() @@ -144,6 +146,11 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp address funder = users.sender; uint256 expectedStreamId = lockup.nextStreamId(); + // Set the default parameters. + _defaultParams.createWithTimestamps.asset = IERC20(asset); + _defaultParams.unlockAmounts.cliff = cliffTime == 0 ? 0 : _defaultParams.unlockAmounts.cliff; + _defaultParams.cliffTime = cliffTime; + // It should perform the ERC-20 transfers. expectCallToTransferFrom({ asset: IERC20(asset), @@ -175,12 +182,11 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp transferable: true, timestamps: defaults.lockupTimestamps(), cliffTime: cliffTime, + unlockAmounts: _defaultParams.unlockAmounts, broker: users.broker }); // Create the stream. - _defaultParams.createWithTimestamps.asset = IERC20(asset); - _defaultParams.cliffTime = cliffTime; uint256 streamId = createDefaultStream(); // It should create the stream. @@ -188,5 +194,7 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp assertEq(lockup.getAsset(streamId), IERC20(asset), "asset"); assertEq(lockup.getCliffTime(streamId), cliffTime, "cliffTime"); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_LINEAR); + assertEq(lockup.getUnlockAmounts(streamId).start, _defaultParams.unlockAmounts.start, "unlockAmounts.start"); + assertEq(lockup.getUnlockAmounts(streamId).cliff, _defaultParams.unlockAmounts.cliff, "unlockAmounts.cliff"); } } diff --git a/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol b/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol index 00e3e51d2..cf9cdb269 100644 --- a/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol @@ -13,26 +13,16 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test is Lockup_Linear_Integration_Concrete_Test.setUp(); } - function test_GivenCliffTimeZero() external givenPENDINGStatus { - uint40 cliffTime = 0; - uint256 streamId = lockup.createWithTimestampsLL(_defaultParams.createWithTimestamps, cliffTime); - - vm.warp({ newTimestamp: defaults.START_TIME() - 1 }); - + function test_GivenCliffTimeInFuture() external givenSTREAMINGStatus { + _defaultParams.unlockAmounts.start = 1; + uint256 streamId = createDefaultStream(); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() - 1 }); uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = 0; - assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); - } - - function test_GivenCliffTimeNotZero() external givenPENDINGStatus { - vm.warp({ newTimestamp: defaults.START_TIME() - 1 }); - - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = 0; + uint128 expectedStreamedAmount = 1; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } - function test_GivenCliffTimeInFuture() external givenSTREAMINGStatus { + function test_GivenCliffTimeInFuture_Zero() external givenSTREAMINGStatus { vm.warp({ newTimestamp: defaults.CLIFF_TIME() - 1 }); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = 0; @@ -46,16 +36,36 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test is assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } - function test_GivenEndTimeNotInFuture() external givenSTREAMINGStatus givenCliffTimeInPast { - vm.warp({ newTimestamp: defaults.END_TIME() + 1 }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); + function test_GivenStartAmount() external givenSTREAMINGStatus givenCliffTimeInPast { + _defaultParams.unlockAmounts.start = 1; + uint256 streamId = createDefaultStream(); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); + uint128 expectedStreamedAmount = defaults.WITHDRAW_AMOUNT() + 1; + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + function test_GivenNoCliffAmount() external givenSTREAMINGStatus givenCliffTimeInPast givenNoStartAmount { + _defaultParams.unlockAmounts.cliff = 0; + uint256 streamId = createDefaultStream(); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); + uint128 expectedStreamedAmount = calculateLockupLinearStreamedAmount( + _defaultParams.createWithTimestamps.timestamps.start, + _defaultParams.cliffTime, + _defaultParams.createWithTimestamps.timestamps.end, + defaults.DEPOSIT_AMOUNT(), + _defaultParams.unlockAmounts + ); + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } - function test_GivenEndTimeInFuture() external givenSTREAMINGStatus givenCliffTimeInPast { + function test_GivenCliffAmount() external givenSTREAMINGStatus givenCliffTimeInPast givenNoStartAmount { + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = 2600e18; + uint128 expectedStreamedAmount = defaults.WITHDRAW_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } } diff --git a/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree b/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree index 994e2d043..faae60fac 100644 --- a/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree +++ b/test/core/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree @@ -1,16 +1,14 @@ StreamedAmountOf_Lockup_Linear_Integration_Concrete_Test -├── given PENDING status -│ ├── given cliff time zero -│ │ └── it should return zero -│ └── given cliff time not zero -│ └── it should return zero └── given STREAMING status ├── given cliff time in future - │ └── it should return zero + │ └── it should return start amount ├── given cliff time in present │ └── it should return the correct streamed amount └── given cliff time in past - ├── given end time not in future - │ └── it should return the deposited amount - └── given end time in future - └── it should return the correct streamed amount + ├── given start amount + │ └── it should return correct streamed amount + └── given no start amount + ├── given no cliff amount + │ └── it should return correct streamed amount + └── given cliff amount + └── it should return correct streamed amount \ No newline at end of file diff --git a/test/core/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol b/test/core/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol index 756c3aae1..4bcfbf4de 100644 --- a/test/core/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol +++ b/test/core/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol @@ -51,14 +51,14 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte { uint40 startTime = getBlockTimestamp(); LockupTranched.TrancheWithDuration[] memory tranches = defaults.tranchesWithDurations(); - tranches[2].duration = 0; - uint256 index = 2; + uint256 index = 1; + tranches[index].duration = 0; vm.expectRevert( abi.encodeWithSelector( Errors.SablierHelpers_TrancheTimestampsNotOrdered.selector, index, - startTime + tranches[0].duration + tranches[1].duration, - startTime + tranches[0].duration + tranches[1].duration + startTime + tranches[0].duration, + startTime + tranches[0].duration ) ); createDefaultStreamWithDurations(tranches); @@ -137,7 +137,6 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte LockupTranched.Tranche[] memory tranches = defaults.tranches(); tranches[0].timestamp = timestamps.start + tranchesWithDurations[0].duration; tranches[1].timestamp = tranches[0].timestamp + tranchesWithDurations[1].duration; - tranches[2].timestamp = tranches[1].timestamp + tranchesWithDurations[2].duration; // It should perform the ERC-20 transfers. expectCallToTransferFrom({ from: funder, to: address(lockup), value: defaults.DEPOSIT_AMOUNT() }); diff --git a/test/core/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol b/test/core/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol index cb8e494d6..69c3a6b4a 100644 --- a/test/core/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol +++ b/test/core/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol @@ -140,6 +140,7 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp whenStartTimeLessThanFirstTimestamp { // Swap the tranche timestamps. + // LockupTranched.Tranche[] memory tranches = defaults.tranches(); (_defaultParams.tranches[0].timestamp, _defaultParams.tranches[1].timestamp) = (_defaultParams.tranches[1].timestamp, _defaultParams.tranches[0].timestamp); @@ -153,6 +154,7 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp _defaultParams.tranches[1].timestamp ) ); + _defaultParams.createWithTimestamps.timestamps.end = _defaultParams.tranches[1].timestamp; createDefaultStream(); } diff --git a/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol b/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol index 829369963..cfe4064cd 100644 --- a/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol @@ -53,7 +53,7 @@ contract StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test is // It should return the correct streamed amount. uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = defaults.tranches()[0].amount + defaults.tranches()[1].amount; + uint128 expectedStreamedAmount = defaults.tranches()[0].amount; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } } diff --git a/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree b/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree index cae8b9a89..f8c857e3c 100644 --- a/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree +++ b/test/core/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree @@ -3,10 +3,7 @@ StreamedAmountOf_Lockup_Tranched_Integration_Concrete_Test ├── given start time in present │ └── it should return zero └── given start time in past - ├── given end time not in future - │ └── it should return the deposited amount - └── given end time in future - ├── given first tranche timestamp in future - │ └── it should return 0 - └── given first tranche timestamp not in future - └── it should return the correct streamed amount + ├── given first tranche timestamp in future + │ └── it should return 0 + └── given first tranche timestamp not in future + └── it should return the correct streamed amount diff --git a/test/core/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/core/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol index 8c717a5fb..7a14bfb45 100644 --- a/test/core/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/test/core/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -22,7 +22,7 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is function test_GivenNoPreviousWithdrawals() external givenSTREAMINGStatus givenStartTimeInPast { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); @@ -32,15 +32,15 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Concrete_Test is function test_GivenPreviousWithdrawal() external givenSTREAMINGStatus givenStartTimeInPast { // Simulate the passage of time. - vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.CLIFF_AMOUNT() }); + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); - uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount - defaults.CLIFF_AMOUNT(); + uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount - defaults.WITHDRAW_AMOUNT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } } diff --git a/test/core/integration/fuzz/lockup-base/cancel.t.sol b/test/core/integration/fuzz/lockup-base/cancel.t.sol index 853866c6f..3fe7626c8 100644 --- a/test/core/integration/fuzz/lockup-base/cancel.t.sol +++ b/test/core/integration/fuzz/lockup-base/cancel.t.sol @@ -56,7 +56,7 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test { whenRecipientReturnsValidSelector whenRecipientNotReentrant { - timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); + timeJump = _bound(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); // Allow the recipient to hook. resetPrank({ msgSender: users.admin }); diff --git a/test/core/integration/fuzz/lockup-base/withdraw.t.sol b/test/core/integration/fuzz/lockup-base/withdraw.t.sol index 1d2b1958b..f64a8beb5 100644 --- a/test/core/integration/fuzz/lockup-base/withdraw.t.sol +++ b/test/core/integration/fuzz/lockup-base/withdraw.t.sol @@ -94,7 +94,7 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { whenWithdrawAmountNotOverdraw whenCallerRecipient { - timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); + timeJump = _bound(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); vm.assume(to != address(0)); // Simulate the passage of time. @@ -162,7 +162,7 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test { whenWithdrawAmountNotOverdraw givenNotCanceledStream { - timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + timeJump = _bound(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); vm.assume(to != address(0)); // Simulate the passage of time. diff --git a/test/core/integration/fuzz/lockup-base/withdrawMax.t.sol b/test/core/integration/fuzz/lockup-base/withdrawMax.t.sol index c8ebf6c9c..ef3631ef4 100644 --- a/test/core/integration/fuzz/lockup-base/withdrawMax.t.sol +++ b/test/core/integration/fuzz/lockup-base/withdrawMax.t.sol @@ -49,7 +49,7 @@ contract WithdrawMax_Integration_Fuzz_Test is Integration_Test { } function testFuzz_WithdrawMax(uint256 timeJump) external givenEndTimeInFuture { - timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); + timeJump = _bound(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); diff --git a/test/core/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol b/test/core/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol index 9304cd93b..b6545e33a 100644 --- a/test/core/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol +++ b/test/core/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol @@ -24,8 +24,8 @@ contract StreamedAmountOf_Lockup_Dynamic_Integration_Fuzz_Test is Integration_Te givenStartTimeInPast { vm.assume(segment.amount != 0); - segment.timestamp = boundUint40(segment.timestamp, defaults.CLIFF_TIME(), defaults.END_TIME()); - timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + segment.timestamp = boundUint40(segment.timestamp, defaults.WARP_26_PERCENT(), defaults.END_TIME()); + timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); // Create the single-segment array. LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](1); diff --git a/test/core/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol b/test/core/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol index 59e2e1493..da609d9cb 100644 --- a/test/core/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol +++ b/test/core/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol @@ -57,11 +57,9 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio // Create the timestamps struct by calculating the start time, cliff time and the end time. Lockup.Timestamps memory timestamps = Lockup.Timestamps({ start: getBlockTimestamp(), end: getBlockTimestamp() + durations.total }); - - uint40 cliffTime; - if (durations.cliff > 0) { - cliffTime = getBlockTimestamp() + durations.cliff; - } + uint40 cliffTime = durations.cliff == 0 ? 0 : getBlockTimestamp() + durations.cliff; + LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmounts(); + unlockAmounts.cliff = durations.cliff > 0 ? unlockAmounts.cliff : 0; // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -76,11 +74,13 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio transferable: true, timestamps: timestamps, cliffTime: cliffTime, + unlockAmounts: unlockAmounts, broker: users.broker }); // Create the stream. _defaultParams.durations = durations; + _defaultParams.unlockAmounts = unlockAmounts; uint256 streamId = createDefaultStreamWithDurations(); // It should create the stream. @@ -97,6 +97,8 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); assertEq(lockup.getCliffTime(streamId), cliffTime, "cliffTime"); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_LINEAR); + assertEq(lockup.getUnlockAmounts(streamId).start, unlockAmounts.start, "unlockAmounts.start"); + assertEq(lockup.getUnlockAmounts(streamId).cliff, unlockAmounts.cliff, "unlockAmounts.cliff"); // Assert that the stream's status is "STREAMING". Lockup.Status actualStatus = lockup.statusOf(streamId); diff --git a/test/core/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol b/test/core/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol index efe64ed05..ecb5facd0 100644 --- a/test/core/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol +++ b/test/core/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol @@ -5,7 +5,7 @@ import { MAX_UD60x18, ud } from "@prb/math/src/UD60x18.sol"; import { ISablierLockup } from "src/core/interfaces/ISablierLockup.sol"; import { Errors } from "src/core/libraries/Errors.sol"; -import { Broker, Lockup } from "src/core/types/DataTypes.sol"; +import { Broker, Lockup, LockupLinear } from "src/core/types/DataTypes.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; @@ -24,7 +24,7 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati ); _defaultParams.createWithTimestamps.broker = broker; - lockup.createWithTimestampsLL(_defaultParams.createWithTimestamps, _defaultParams.cliffTime); + createDefaultStream(); } function testFuzz_RevertWhen_StartTimeNotLessThanCliffTime(uint40 startTime) @@ -42,7 +42,7 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati Errors.SablierHelpers_StartTimeNotLessThanCliffTime.selector, startTime, defaults.CLIFF_TIME() ) ); - lockup.createWithTimestampsLL(_defaultParams.createWithTimestamps, _defaultParams.cliffTime); + createDefaultStream(); } function testFuzz_RevertWhen_CliffTimeNotLessThanEndTime( @@ -61,12 +61,12 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati _defaultParams.createWithTimestamps.timestamps.start = startTime; _defaultParams.createWithTimestamps.timestamps.end = endTime; + _defaultParams.cliffTime = cliffTime; vm.expectRevert( abi.encodeWithSelector(Errors.SablierHelpers_CliffTimeNotLessThanEndTime.selector, cliffTime, endTime) ); - - lockup.createWithTimestampsLL(_defaultParams.createWithTimestamps, cliffTime); + createDefaultStream(); } struct Vars { @@ -92,10 +92,12 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati /// - Start time lower than and equal to cliff time /// - Cliff time zero and not zero /// - Multiple values for the cliff time and the end time + /// - Multiple values for start unlock amount and cliff unlock amount /// - Multiple values for the broker fee, including zero function testFuzz_CreateWithTimestampsLL( address funder, Lockup.CreateWithTimestamps memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, uint40 cliffTime ) external @@ -134,6 +136,10 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, vars.createAmounts.deposit); + unlockAmounts.cliff = + cliffTime > 0 ? boundUint128(unlockAmounts.cliff, 0, vars.createAmounts.deposit - unlockAmounts.start) : 0; + // Make the fuzzed funder the caller in this test. resetPrank(funder); vars.expectedStreamId = lockup.nextStreamId(); @@ -165,13 +171,14 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati transferable: params.transferable, timestamps: Lockup.Timestamps({ start: params.timestamps.start, end: params.timestamps.end }), cliffTime: cliffTime, + unlockAmounts: unlockAmounts, broker: params.broker.account }); params.asset = dai; // Create the stream. - vars.actualStreamId = lockup.createWithTimestampsLL(params, cliffTime); + vars.actualStreamId = lockup.createWithTimestampsLL(params, unlockAmounts, cliffTime); // It should create the stream. assertEq(lockup.getDepositedAmount(vars.actualStreamId), vars.createAmounts.deposit, "depositedAmount"); @@ -185,7 +192,9 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati assertEq(lockup.getSender(vars.actualStreamId), params.sender, "sender"); assertEq(lockup.getStartTime(vars.actualStreamId), params.timestamps.start, "startTime"); assertFalse(lockup.wasCanceled(vars.actualStreamId), "wasCanceled"); - assertEq(lockup.getCliffTime(vars.actualStreamId), cliffTime, "cliff"); + assertEq(lockup.getUnlockAmounts(vars.actualStreamId).start, unlockAmounts.start, "unlockAmounts.start"); + assertEq(lockup.getUnlockAmounts(vars.actualStreamId).cliff, unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getCliffTime(vars.actualStreamId), cliffTime, "cliffTime"); assertEq(lockup.getLockupModel(vars.actualStreamId), Lockup.Model.LOCKUP_LINEAR); // Assert that the stream's status is correct. diff --git a/test/core/integration/fuzz/lockup-linear/streamedAmountOf.t.sol b/test/core/integration/fuzz/lockup-linear/streamedAmountOf.t.sol index 5dea8e436..8fe1d231d 100644 --- a/test/core/integration/fuzz/lockup-linear/streamedAmountOf.t.sol +++ b/test/core/integration/fuzz/lockup-linear/streamedAmountOf.t.sol @@ -1,20 +1,42 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/core/types/DataTypes.sol"; +import { Lockup, LockupLinear } from "src/core/types/DataTypes.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_Integration_Fuzz_Test { - function testFuzz_StreamedAmountOf_CliffTimeInFuture(uint40 timeJump) + function testFuzz_StreamedAmountOf_CliffTimeInFuture( + uint40 timeJump, + uint128 depositAmount, + LockupLinear.UnlockAmounts memory unlockAmounts + ) external givenNotNull givenNotCanceledStream { - timeJump = boundUint40(timeJump, 0, defaults.CLIFF_DURATION() - 1); + vm.assume(depositAmount != 0); + timeJump = boundUint40(timeJump, 1, defaults.CLIFF_DURATION() - 1); + + // Bound the unlock amounts. + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); + unlockAmounts.cliff = boundUint128(unlockAmounts.start, 0, depositAmount - unlockAmounts.start); + + // Mint enough assets to the Sender. + deal({ token: address(dai), to: users.sender, give: depositAmount }); + + // Approve the lockup contract to transfer the deposit amount. + dai.approve(address(lockup), depositAmount); + + // Create the stream with the fuzzed deposit amount. + _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.unlockAmounts = unlockAmounts; + uint256 streamId = createDefaultStream(); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); - uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); - uint128 expectedStreamedAmount = 0; + uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); + uint128 expectedStreamedAmount = unlockAmounts.start > 0 ? unlockAmounts.start : 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -28,7 +50,8 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I /// - Status settled function testFuzz_StreamedAmountOf_Calculation( uint40 timeJump, - uint128 depositAmount + uint128 depositAmount, + LockupLinear.UnlockAmounts memory unlockAmounts ) external givenNotNull @@ -38,21 +61,30 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I vm.assume(depositAmount != 0); timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + // Bound the unlock amounts. + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); + unlockAmounts.cliff = boundUint128(unlockAmounts.start, 0, depositAmount - unlockAmounts.start); + // Mint enough assets to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); + // Approve the lockup contract to transfer the deposit amount. + dai.approve(address(lockup), depositAmount); + // Create the stream with the fuzzed deposit amount. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = depositAmount; - uint256 streamId = lockup.createWithTimestampsLL(params, defaults.CLIFF_TIME()); + _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + _defaultParams.unlockAmounts = unlockAmounts; + uint256 streamId = createDefaultStream(); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Run the test. uint128 actualStreamedAmount = lockup.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = - calculateLockupLinearStreamedAmount(defaults.START_TIME(), defaults.END_TIME(), depositAmount); + uint128 expectedStreamedAmount = calculateLockupLinearStreamedAmount( + defaults.START_TIME(), defaults.CLIFF_TIME(), defaults.END_TIME(), depositAmount, unlockAmounts + ); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -60,7 +92,8 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I function testFuzz_StreamedAmountOf_Monotonicity( uint40 timeWarp0, uint40 timeWarp1, - uint128 depositAmount + uint128 depositAmount, + LockupLinear.UnlockAmounts memory unlockAmounts ) external givenNotNull @@ -71,13 +104,21 @@ contract StreamedAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Linear_I timeWarp0 = boundUint40(timeWarp0, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); timeWarp1 = boundUint40(timeWarp1, timeWarp0, defaults.TOTAL_DURATION()); + // Bound the unlock amounts. + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); + unlockAmounts.cliff = boundUint128(unlockAmounts.start, 0, depositAmount - unlockAmounts.start); + // Mint enough assets to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); + // Approve the lockup contract to transfer the deposit amount. + dai.approve(address(lockup), depositAmount); + // Create the stream with the fuzzed deposit amount. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = depositAmount; - uint256 streamId = lockup.createWithTimestampsLL(params, defaults.CLIFF_TIME()); + _defaultParams.unlockAmounts = unlockAmounts; + _defaultParams.createWithTimestamps.totalAmount = depositAmount; + _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + uint256 streamId = createDefaultStream(); // Warp to the future for the first time. vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp0 }); diff --git a/test/core/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol b/test/core/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol index 600905f1d..e261d9cc2 100644 --- a/test/core/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol +++ b/test/core/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; -import { Lockup } from "src/core/types/DataTypes.sol"; +import { Lockup, LockupLinear } from "src/core/types/DataTypes.sol"; import { Lockup_Linear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; @@ -35,23 +35,29 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line givenCliffTimeNotInFuture { vm.assume(depositAmount != 0); - timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); // Mint enough assets to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream. The broker fee is disabled so that it doesn't interfere with the calculations. - Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); - params.totalAmount = depositAmount; - uint256 streamId = lockup.createWithTimestampsLL(params, defaults.CLIFF_TIME()); + _defaultParams.createWithTimestamps.broker = defaults.brokerNull(); + _defaultParams.unlockAmounts = defaults.unlockAmountsZero(); + _defaultParams.createWithTimestamps.totalAmount = depositAmount; + uint256 streamId = createDefaultStream(); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Run the test. uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(streamId); - uint128 expectedWithdrawableAmount = - calculateLockupLinearStreamedAmount(defaults.START_TIME(), defaults.END_TIME(), depositAmount); + uint128 expectedWithdrawableAmount = calculateLockupLinearStreamedAmount( + defaults.START_TIME(), + defaults.CLIFF_TIME(), + defaults.END_TIME(), + depositAmount, + _defaultParams.unlockAmounts + ); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -85,16 +91,18 @@ contract WithdrawableAmountOf_Lockup_Linear_Integration_Fuzz_Test is Lockup_Line // Create the stream. The broker fee is disabled so that it doesn't interfere with the calculations. Lockup.CreateWithTimestamps memory params = defaults.createWithTimestampsBrokerNull(); params.totalAmount = depositAmount; - uint256 streamId = lockup.createWithTimestampsLL(params, defaults.CLIFF_TIME()); + LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmountsZero(); + uint256 streamId = lockup.createWithTimestampsLL(params, unlockAmounts, defaults.CLIFF_TIME()); - timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. - uint128 streamedAmount = - calculateLockupLinearStreamedAmount(defaults.START_TIME(), defaults.END_TIME(), depositAmount); + uint128 streamedAmount = calculateLockupLinearStreamedAmount( + defaults.START_TIME(), defaults.CLIFF_TIME(), defaults.END_TIME(), depositAmount, unlockAmounts + ); withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); // Make the withdrawal. diff --git a/test/core/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol b/test/core/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol index 3b07d52b2..9b0c88604 100644 --- a/test/core/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol +++ b/test/core/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol @@ -15,7 +15,7 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tr /// - Status streaming /// - Status settled function testFuzz_WithdrawableAmountOf_NoPreviousWithdrawals(uint40 timeJump) external givenStartTimeInPast { - timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with // the calculations. @@ -60,7 +60,7 @@ contract WithdrawableAmountOf_Lockup_Tranched_Integration_Fuzz_Test is Lockup_Tr params.totalAmount = defaults.DEPOSIT_AMOUNT(); uint256 streamId = lockup.createWithTimestampsLT(params, defaults.tranches()); - timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + timeJump = boundUint40(timeJump, defaults.WARP_26_PERCENT_DURATION(), defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); diff --git a/test/core/invariant/handlers/LockupCreateHandler.sol b/test/core/invariant/handlers/LockupCreateHandler.sol index 8c4ba38b2..7c06a1da3 100644 --- a/test/core/invariant/handlers/LockupCreateHandler.sol +++ b/test/core/invariant/handlers/LockupCreateHandler.sol @@ -6,11 +6,12 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierLockup } from "src/core/interfaces/ISablierLockup.sol"; import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "src/core/types/DataTypes.sol"; +import { Calculations } from "test/utils/Calculations.sol"; import { LockupStore } from "../stores/LockupStore.sol"; import { BaseHandler } from "./BaseHandler.sol"; /// @dev This contract is a complement of {LockupHandler}. -contract LockupCreateHandler is BaseHandler { +contract LockupCreateHandler is BaseHandler, Calculations { /*////////////////////////////////////////////////////////////////////////// TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ @@ -73,6 +74,7 @@ contract LockupCreateHandler is BaseHandler { function createWithDurationsLL( uint256 timeJumpSeed, Lockup.CreateWithDurations memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, LockupLinear.Durations memory durations ) public @@ -84,11 +86,7 @@ contract LockupCreateHandler is BaseHandler { // We don't want to create more than a certain number of streams. vm.assume(lockupStore.lastStreamId() <= MAX_STREAM_COUNT); - // Bound the stream parameters. - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - durations.cliff = boundUint40(durations.cliff, 1 seconds, 2500 seconds); - durations.total = boundUint40(durations.total, durations.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); - params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); + (params, unlockAmounts, durations) = _boundCreateWithDurationsLLParams(params, unlockAmounts, durations); // Mint enough assets to the Sender. deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); @@ -98,7 +96,7 @@ contract LockupCreateHandler is BaseHandler { // Create the stream. params.asset = asset; - uint256 streamId = lockup.createWithDurationsLL(params, durations); + uint256 streamId = lockup.createWithDurationsLL(params, unlockAmounts, durations); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -193,6 +191,7 @@ contract LockupCreateHandler is BaseHandler { function createWithTimestampsLL( uint256 timeJumpSeed, Lockup.CreateWithTimestamps memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, uint40 cliffTime ) public @@ -204,18 +203,7 @@ contract LockupCreateHandler is BaseHandler { // We don't want to create more than a certain number of streams. vm.assume(lockupStore.lastStreamId() <= MAX_STREAM_COUNT); - params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - params.timestamps.start = boundUint40(params.timestamps.start, 1 seconds, getBlockTimestamp()); - params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); - - // The cliff time must be either zero or greater than the start time. - if (cliffTime > 0) { - cliffTime = boundUint40(cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks); - } - - // Bound the end time so that it is always greater than the start time, and the cliff time. - uint40 endTimeLowerBound = maxOfTwo(params.timestamps.start, cliffTime); - params.timestamps.end = boundUint40(params.timestamps.end, endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); + (params, unlockAmounts, cliffTime) = _boundCreateWithTimestampsLLParams(params, unlockAmounts, cliffTime); // Mint enough assets to the Sender. deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); @@ -225,7 +213,7 @@ contract LockupCreateHandler is BaseHandler { // Create the stream. params.asset = asset; - uint256 streamId = lockup.createWithTimestampsLL(params, cliffTime); + uint256 streamId = lockup.createWithTimestampsLL(params, unlockAmounts, cliffTime); // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); @@ -275,4 +263,70 @@ contract LockupCreateHandler is BaseHandler { // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to bound the params of the `createWithDurationsLL` function so that all the requirements are + /// respected. + /// @dev Function needed to prevent "Stack too deep error". + function _boundCreateWithDurationsLLParams( + Lockup.CreateWithDurations memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, + LockupLinear.Durations memory durations + ) + private + pure + returns (Lockup.CreateWithDurations memory, LockupLinear.UnlockAmounts memory, LockupLinear.Durations memory) + { + // Bound the stream parameters. + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + durations.cliff = boundUint40(durations.cliff, 1 seconds, 2500 seconds); + durations.total = boundUint40(durations.total, durations.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); + params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); + uint128 depositAmount = calculateDepositAmount(params.totalAmount, params.broker.fee); + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); + unlockAmounts.cliff = depositAmount == unlockAmounts.start + ? 0 + : boundUint128(unlockAmounts.cliff, 0, depositAmount - unlockAmounts.start); + + return (params, unlockAmounts, durations); + } + + /// @notice Function to bound the params of the `createWithTimestampsLL` function so that all the requirements are + /// respected. + /// @dev Function needed to prevent "Stack too deep error". + function _boundCreateWithTimestampsLLParams( + Lockup.CreateWithTimestamps memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, + uint40 cliffTime + ) + private + view + returns (Lockup.CreateWithTimestamps memory, LockupLinear.UnlockAmounts memory, uint40) + { + uint40 blockTimestamp = getBlockTimestamp(); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.timestamps.start = boundUint40(params.timestamps.start, 1 seconds, blockTimestamp); + params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); + uint128 depositAmount = calculateDepositAmount(params.totalAmount, params.broker.fee); + unlockAmounts.start = boundUint128(unlockAmounts.start, 0, depositAmount); + unlockAmounts.cliff = 0; + + // The cliff time must be either zero or greater than the start time. + if (cliffTime > 0) { + cliffTime = boundUint40(cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks); + + unlockAmounts.cliff = depositAmount == unlockAmounts.start + ? 0 + : boundUint128(unlockAmounts.cliff, 0, depositAmount - unlockAmounts.start); + } + + // Bound the end time so that it is always greater than the start time, and the cliff time. + uint40 endTimeLowerBound = maxOfTwo(params.timestamps.start, cliffTime); + params.timestamps.end = boundUint40(params.timestamps.end, endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); + + return (params, unlockAmounts, cliffTime); + } } diff --git a/test/periphery/Periphery.t.sol b/test/periphery/Periphery.t.sol index c2eecde78..12fe839ce 100644 --- a/test/periphery/Periphery.t.sol +++ b/test/periphery/Periphery.t.sol @@ -58,6 +58,7 @@ contract Periphery_Test is Base_Test { function expectMultipleCallsToCreateWithDurationsLL( uint64 count, Lockup.CreateWithDurations memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, LockupLinear.Durations memory durations ) internal @@ -65,7 +66,7 @@ contract Periphery_Test is Base_Test { vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithDurationsLL, (params, durations)) + data: abi.encodeCall(ISablierLockup.createWithDurationsLL, (params, unlockAmounts, durations)) }); } @@ -104,14 +105,15 @@ contract Periphery_Test is Base_Test { function expectMultipleCallsToCreateWithTimestampsLL( uint64 count, Lockup.CreateWithTimestamps memory params, - uint40 cliff + LockupLinear.UnlockAmounts memory unlockAmounts, + uint40 cliffTime ) internal { vm.expectCall({ callee: address(lockup), count: count, - data: abi.encodeCall(ISablierLockup.createWithTimestampsLL, (params, cliff)) + data: abi.encodeCall(ISablierLockup.createWithTimestampsLL, (params, unlockAmounts, cliffTime)) }); } @@ -205,7 +207,8 @@ contract Periphery_Test is Base_Test { IERC20 asset_, bytes32 merkleRoot, uint40 expiration, - uint256 sablierFee + uint256 sablierFee, + LockupLinear.UnlockAmounts memory unlockAmounts ) internal view @@ -223,11 +226,12 @@ contract Periphery_Test is Base_Test { lockup, defaults.CANCELABLE(), defaults.TRANSFERABLE(), - abi.encode(defaults.schedule()) + abi.encode(defaults.schedule()), + abi.encode(unlockAmounts) ) ); bytes32 creationBytecodeHash = - keccak256(getMerkleLLBytecode(campaignOwner, asset_, merkleRoot, expiration, sablierFee)); + keccak256(getMerkleLLBytecode(campaignOwner, asset_, merkleRoot, expiration, sablierFee, unlockAmounts)); return vm.computeCreate2Address({ salt: salt, initCodeHash: creationBytecodeHash, @@ -299,7 +303,8 @@ contract Periphery_Test is Base_Test { IERC20 asset_, bytes32 merkleRoot, uint40 expiration, - uint256 sablierFee + uint256 sablierFee, + LockupLinear.UnlockAmounts memory unlockAmounts ) internal view @@ -311,6 +316,7 @@ contract Periphery_Test is Base_Test { defaults.CANCELABLE(), defaults.TRANSFERABLE(), defaults.schedule(), + unlockAmounts, sablierFee ); if (!isTestOptimizedProfile()) { diff --git a/test/periphery/fork/batch-lockup/createWithTimestampsLL.t.sol b/test/periphery/fork/batch-lockup/createWithTimestampsLL.t.sol index 4772dcb3a..5798b7360 100644 --- a/test/periphery/fork/batch-lockup/createWithTimestampsLL.t.sol +++ b/test/periphery/fork/batch-lockup/createWithTimestampsLL.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Lockup } from "src/core/types/DataTypes.sol"; +import { Lockup, LockupLinear } from "src/core/types/DataTypes.sol"; import { BatchLockup } from "src/periphery/types/DataTypes.sol"; import { ArrayBuilder } from "../../../utils/ArrayBuilder.sol"; @@ -18,6 +18,7 @@ abstract contract CreateWithTimestampsLL_BatchLockup_Fork_Test is Fork_Test { uint128 batchSize; Lockup.Timestamps timestamps; uint40 cliffTime; + LockupLinear.UnlockAmounts unlockAmounts; address sender; address recipient; uint128 perStreamAmount; @@ -32,6 +33,12 @@ abstract contract CreateWithTimestampsLL_BatchLockup_Fork_Test is Fork_Test { boundUint40(params.cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks); params.timestamps.end = boundUint40(params.timestamps.end, params.cliffTime + 1 seconds, MAX_UNIX_TIMESTAMP); + // Bound the unlock amounts. + params.unlockAmounts.start = boundUint128(params.unlockAmounts.start, 0, params.perStreamAmount); + params.unlockAmounts.cliff = params.cliffTime > 0 + ? boundUint128(params.unlockAmounts.cliff, 0, params.perStreamAmount - params.unlockAmounts.start) + : 0; + checkUsers(params.sender, params.recipient); uint256 firstStreamId = lockup.nextStreamId(); @@ -51,7 +58,7 @@ abstract contract CreateWithTimestampsLL_BatchLockup_Fork_Test is Fork_Test { broker: defaults.brokerNull() }); BatchLockup.CreateWithTimestampsLL[] memory batchParams = - BatchLockupBuilder.fillBatch(createParams, params.cliffTime, params.batchSize); + BatchLockupBuilder.fillBatch(createParams, params.unlockAmounts, params.cliffTime, params.batchSize); // Asset flow: sender → batch → Sablier expectCallToTransferFrom({ @@ -63,7 +70,8 @@ abstract contract CreateWithTimestampsLL_BatchLockup_Fork_Test is Fork_Test { expectMultipleCallsToCreateWithTimestampsLL({ count: uint64(params.batchSize), params: createParams, - cliff: params.cliffTime + unlockAmounts: params.unlockAmounts, + cliffTime: params.cliffTime }); expectMultipleCallsToTransferFrom({ asset: FORK_ASSET, diff --git a/test/periphery/fork/merkle-campaign/MerkleLL.t.sol b/test/periphery/fork/merkle-campaign/MerkleLL.t.sol index 310ba031b..90d13b595 100644 --- a/test/periphery/fork/merkle-campaign/MerkleLL.t.sol +++ b/test/periphery/fork/merkle-campaign/MerkleLL.t.sol @@ -102,7 +102,13 @@ abstract contract MerkleLL_Fork_Test is Fork_Test { uint256 sablierFee = defaults.DEFAULT_SABLIER_FEE(); vars.expectedLL = computeMerkleLLAddress( - params.campaignOwner, params.campaignOwner, FORK_ASSET, vars.merkleRoot, params.expiration, sablierFee + params.campaignOwner, + params.campaignOwner, + FORK_ASSET, + vars.merkleRoot, + params.expiration, + sablierFee, + defaults.unlockAmountsZero() ); vars.baseParams = defaults.baseParams({ @@ -120,6 +126,7 @@ abstract contract MerkleLL_Fork_Test is Fork_Test { cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: defaults.schedule(), + unlockAmounts: defaults.unlockAmountsZero(), aggregateAmount: vars.aggregateAmount, recipientCount: vars.recipientCount, sablierFee: sablierFee @@ -131,6 +138,7 @@ abstract contract MerkleLL_Fork_Test is Fork_Test { cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: defaults.schedule(), + unlockAmounts: defaults.unlockAmountsZero(), aggregateAmount: vars.aggregateAmount, recipientCount: vars.recipientCount }); @@ -211,6 +219,8 @@ abstract contract MerkleLL_Fork_Test is Fork_Test { assertEq(lockup.getSender(vars.expectedStreamId), params.campaignOwner, "sender"); assertEq(lockup.getStartTime(vars.expectedStreamId), getBlockTimestamp(), "start time"); assertEq(lockup.wasCanceled(vars.expectedStreamId), false, "was canceled"); + assertEq(lockup.getUnlockAmounts(vars.expectedStreamId).start, 0, "unlock amounts start"); + assertEq(lockup.getUnlockAmounts(vars.expectedStreamId).cliff, 0, "unlock amounts cliff"); assertTrue(vars.merkleLL.hasClaimed(vars.indexes[params.posBeforeSort])); diff --git a/test/periphery/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol b/test/periphery/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol index 813502cf1..ef0662570 100644 --- a/test/periphery/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol +++ b/test/periphery/integration/batch-lockup/create-with-durations-ll/createWithDurationsLL.t.sol @@ -30,6 +30,7 @@ contract CreateWithDurationsLL_Integration_Test is Periphery_Test { expectMultipleCallsToCreateWithDurationsLL({ count: defaults.BATCH_SIZE(), params: defaults.createWithDurationsBrokerNull(), + unlockAmounts: defaults.unlockAmounts(), durations: defaults.durations() }); expectMultipleCallsToTransferFrom({ diff --git a/test/periphery/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol b/test/periphery/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol index 8dc414936..377977c28 100644 --- a/test/periphery/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol +++ b/test/periphery/integration/batch-lockup/create-with-timestamps-ll/createWithTimestamps.t.sol @@ -30,7 +30,8 @@ contract CreateWithTimestampsLL_Integration_Test is Periphery_Test { expectMultipleCallsToCreateWithTimestampsLL({ count: defaults.BATCH_SIZE(), params: defaults.createWithTimestampsBrokerNull(), - cliff: defaults.CLIFF_TIME() + unlockAmounts: defaults.unlockAmounts(), + cliffTime: defaults.CLIFF_TIME() }); expectMultipleCallsToTransferFrom({ count: defaults.BATCH_SIZE(), diff --git a/test/periphery/integration/merkle-campaign/MerkleCampaign.t.sol b/test/periphery/integration/merkle-campaign/MerkleCampaign.t.sol index 85dedf95f..ac1a33b79 100644 --- a/test/periphery/integration/merkle-campaign/MerkleCampaign.t.sol +++ b/test/periphery/integration/merkle-campaign/MerkleCampaign.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22; +import { LockupLinear } from "src/core/types/DataTypes.sol"; + import { ISablierMerkleBase } from "src/periphery/interfaces/ISablierMerkleBase.sol"; import { ISablierMerkleInstant } from "src/periphery/interfaces/ISablierMerkleInstant.sol"; import { ISablierMerkleLL } from "src/periphery/interfaces/ISablierMerkleLL.sol"; @@ -179,6 +181,7 @@ abstract contract MerkleCampaign_Integration_Test is Periphery_Test { asset_: dai, merkleRoot: merkleRoot, expiration: expiration, + unlockAmounts: defaults.unlockAmounts(), sablierFee: sablierFee }); } @@ -202,6 +205,7 @@ abstract contract MerkleCampaign_Integration_Test is Periphery_Test { cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: defaults.schedule(), + unlockAmounts: defaults.unlockAmounts(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT() }); diff --git a/test/periphery/integration/merkle-campaign/factory/create-merkle-ll/createMerkleLL.t.sol b/test/periphery/integration/merkle-campaign/factory/create-merkle-ll/createMerkleLL.t.sol index 3ec8b3710..be5e9f803 100644 --- a/test/periphery/integration/merkle-campaign/factory/create-merkle-ll/createMerkleLL.t.sol +++ b/test/periphery/integration/merkle-campaign/factory/create-merkle-ll/createMerkleLL.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { LockupLinear } from "src/core/types/DataTypes.sol"; import { ISablierMerkleFactory } from "src/periphery/interfaces/ISablierMerkleFactory.sol"; import { ISablierMerkleLL } from "src/periphery/interfaces/ISablierMerkleLL.sol"; import { Errors } from "src/periphery/libraries/Errors.sol"; @@ -14,6 +15,7 @@ contract CreateMerkleLL_Integration_Test is MerkleCampaign_Integration_Test { bool cancelable = defaults.CANCELABLE(); bool transferable = defaults.TRANSFERABLE(); MerkleLL.Schedule memory schedule = defaults.schedule(); + LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmounts(); uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); uint256 recipientCount = defaults.RECIPIENT_COUNT(); @@ -31,6 +33,7 @@ contract CreateMerkleLL_Integration_Test is MerkleCampaign_Integration_Test { cancelable: cancelable, transferable: transferable, schedule: schedule, + unlockAmounts: unlockAmounts, aggregateAmount: aggregateAmount, recipientCount: recipientCount }); @@ -42,6 +45,7 @@ contract CreateMerkleLL_Integration_Test is MerkleCampaign_Integration_Test { bool cancelable = defaults.CANCELABLE(); bool transferable = defaults.TRANSFERABLE(); MerkleLL.Schedule memory schedule = defaults.schedule(); + LockupLinear.UnlockAmounts memory unlockAmounts = defaults.unlockAmounts(); uint256 aggregateAmount = defaults.AGGREGATE_AMOUNT(); uint256 recipientCount = defaults.RECIPIENT_COUNT(); @@ -53,6 +57,7 @@ contract CreateMerkleLL_Integration_Test is MerkleCampaign_Integration_Test { cancelable: cancelable, transferable: transferable, schedule: schedule, + unlockAmounts: unlockAmounts, aggregateAmount: aggregateAmount, recipientCount: recipientCount }); @@ -90,6 +95,7 @@ contract CreateMerkleLL_Integration_Test is MerkleCampaign_Integration_Test { cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: defaults.schedule(), + unlockAmounts: defaults.unlockAmounts(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT(), sablierFee: customFee @@ -132,6 +138,7 @@ contract CreateMerkleLL_Integration_Test is MerkleCampaign_Integration_Test { cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: defaults.schedule(), + unlockAmounts: defaults.unlockAmounts(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT(), sablierFee: defaults.DEFAULT_SABLIER_FEE() diff --git a/test/periphery/integration/merkle-campaign/ll/claim/claim.t.sol b/test/periphery/integration/merkle-campaign/ll/claim/claim.t.sol index 0e4ba4c4b..e0f5b07bb 100644 --- a/test/periphery/integration/merkle-campaign/ll/claim/claim.t.sol +++ b/test/periphery/integration/merkle-campaign/ll/claim/claim.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.22 <0.9.0; +import { LockupLinear } from "src/core/types/DataTypes.sol"; import { ISablierMerkleLL } from "src/periphery/interfaces/ISablierMerkleLL.sol"; import { MerkleLL } from "src/periphery/types/DataTypes.sol"; @@ -24,6 +25,7 @@ contract Claim_MerkleLL_Integration_Test is Claim_Integration_Test, MerkleLL_Int cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: schedule, + unlockAmounts: defaults.unlockAmountsZero(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT() }); @@ -48,6 +50,7 @@ contract Claim_MerkleLL_Integration_Test is Claim_Integration_Test, MerkleLL_Int cancelable: defaults.CANCELABLE(), transferable: defaults.TRANSFERABLE(), schedule: schedule, + unlockAmounts: defaults.unlockAmounts(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT() }); @@ -67,6 +70,9 @@ contract Claim_MerkleLL_Integration_Test is Claim_Integration_Test, MerkleLL_Int uint256 expectedStreamId = lockup.nextStreamId(); uint256 previousFeeAccrued = address(merkleLL).balance; + LockupLinear.UnlockAmounts memory unlockAmounts = + cliffTime > 0 ? defaults.unlockAmounts() : defaults.unlockAmountsZero(); + // It should emit a {Claim} event. vm.expectEmit({ emitter: address(merkleLL) }); emit ISablierMerkleLL.Claim(defaults.INDEX1(), users.recipient1, defaults.CLAIM_AMOUNT(), expectedStreamId); @@ -92,9 +98,10 @@ contract Claim_MerkleLL_Integration_Test is Claim_Integration_Test, MerkleLL_Int assertEq(lockup.getSender(expectedStreamId), users.campaignOwner, "sender"); assertEq(lockup.getStartTime(expectedStreamId), startTime, "start time"); assertEq(lockup.wasCanceled(expectedStreamId), false, "was canceled"); + assertEq(lockup.getUnlockAmounts(expectedStreamId).start, unlockAmounts.start, "unlock amount start"); + assertEq(lockup.getUnlockAmounts(expectedStreamId).cliff, unlockAmounts.cliff, "unlock amount cliff"); assertTrue(merkleLL.hasClaimed(defaults.INDEX1()), "not claimed"); - assertEq(address(merkleLL).balance, previousFeeAccrued + defaults.DEFAULT_SABLIER_FEE(), "fee collected"); } } diff --git a/test/periphery/integration/merkle-campaign/ll/constructor.t.sol b/test/periphery/integration/merkle-campaign/ll/constructor.t.sol index f59fc2535..5569b98f2 100644 --- a/test/periphery/integration/merkle-campaign/ll/constructor.t.sol +++ b/test/periphery/integration/merkle-campaign/ll/constructor.t.sol @@ -47,6 +47,7 @@ contract Constructor_MerkleLL_Integration_Test is MerkleCampaign_Integration_Tes defaults.CANCELABLE(), defaults.TRANSFERABLE(), defaults.schedule(), + defaults.unlockAmounts(), defaults.DEFAULT_SABLIER_FEE() ); diff --git a/test/utils/BatchLockupBuilder.sol b/test/utils/BatchLockupBuilder.sol index 1886f11d1..51655ce35 100644 --- a/test/utils/BatchLockupBuilder.sol +++ b/test/utils/BatchLockupBuilder.sol @@ -62,6 +62,7 @@ library BatchLockupBuilder { /// @notice Turns the inputs into an array of {BatchLockup.CreateWithDurationsLL} structs. function fillBatch( Lockup.CreateWithDurations memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, LockupLinear.Durations memory durations, uint256 batchSize ) @@ -77,6 +78,7 @@ library BatchLockupBuilder { cancelable: params.cancelable, transferable: params.transferable, durations: durations, + unlockAmounts: unlockAmounts, broker: params.broker }); batch = fillBatch(batchSingle, batchSize); @@ -177,6 +179,7 @@ library BatchLockupBuilder { /// @notice Turns the inputs into an array of {BatchLockup.CreateWithTimestampsLL} structs. function fillBatch( Lockup.CreateWithTimestamps memory params, + LockupLinear.UnlockAmounts memory unlockAmounts, uint40 cliffTime, uint256 batchSize ) @@ -193,6 +196,7 @@ library BatchLockupBuilder { transferable: params.transferable, timestamps: params.timestamps, cliffTime: cliffTime, + unlockAmounts: unlockAmounts, broker: params.broker }); batch = fillBatch(batchSingle, batchSize); diff --git a/test/utils/Calculations.sol b/test/utils/Calculations.sol index e54ddb137..4b18f72f1 100644 --- a/test/utils/Calculations.sol +++ b/test/utils/Calculations.sol @@ -6,7 +6,7 @@ import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uin import { SD59x18 } from "@prb/math/src/SD59x18.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { LockupDynamic, LockupTranched } from "../../src/core/types/DataTypes.sol"; +import { LockupDynamic, LockupLinear, LockupTranched } from "../../src/core/types/DataTypes.sol"; abstract contract Calculations { using CastingUint128 for uint128; @@ -67,22 +67,41 @@ abstract contract Calculations { /// @dev Helper function that replicates the logic of {VestingMath.calculateLockupLinearStreamedAmount}. function calculateLockupLinearStreamedAmount( uint40 startTime, + uint40 cliffTime, uint40 endTime, - uint128 depositAmount + uint128 depositAmount, + LockupLinear.UnlockAmounts memory unlockAmounts ) internal view returns (uint128) { uint40 blockTimestamp = uint40(block.timestamp); + + if (startTime >= blockTimestamp) { + return 0; + } if (blockTimestamp >= endTime) { return depositAmount; } + if (cliffTime > blockTimestamp) { + return unlockAmounts.start; + } + unchecked { - UD60x18 elapsedTime = ud(blockTimestamp - startTime); - UD60x18 totalDuration = ud(endTime - startTime); - UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration); - return elapsedTimePercentage.mul(ud(depositAmount)).intoUint128(); + UD60x18 unlockAmountsSum = ud(unlockAmounts.start).add(ud(unlockAmounts.cliff)); + + if (unlockAmountsSum.unwrap() >= depositAmount) { + return depositAmount; + } + + UD60x18 elapsedTime = cliffTime > 0 ? ud(blockTimestamp - cliffTime) : ud(blockTimestamp - startTime); + UD60x18 streamableDuration = cliffTime > 0 ? ud(endTime - cliffTime) : ud(endTime - startTime); + UD60x18 elapsedTimePercentage = elapsedTime.div(streamableDuration); + + UD60x18 streamableAmount = ud(depositAmount).sub(unlockAmountsSum); + UD60x18 streamedAmount = elapsedTimePercentage.mul(streamableAmount); + return streamedAmount.add(unlockAmountsSum).intoUint128(); } } diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol index ae7123fd6..fc303220e 100644 --- a/test/utils/Defaults.sol +++ b/test/utils/Defaults.sol @@ -27,22 +27,24 @@ contract Defaults is Constants, Merkle { uint64 public constant BATCH_SIZE = 10; UD60x18 public constant BROKER_FEE = UD60x18.wrap(0.003e18); // 0.3% uint128 public constant BROKER_FEE_AMOUNT = 30.090270812437311935e18; // 0.3% of total amount - uint128 public constant CLIFF_AMOUNT = 2500e18; + uint128 public constant CLIFF_AMOUNT = 2500e18 + 2534; uint40 public immutable CLIFF_TIME; uint40 public constant CLIFF_DURATION = 2500 seconds; uint128 public constant DEPOSIT_AMOUNT = 10_000e18; uint40 public immutable END_TIME; uint256 public constant MAX_COUNT = 10_000; uint40 public immutable MAX_SEGMENT_DURATION; - uint128 public constant REFUND_AMOUNT = DEPOSIT_AMOUNT - CLIFF_AMOUNT; + uint256 public constant MAX_TRANCHE_COUNT = 10_000; + uint128 public constant REFUND_AMOUNT = DEPOSIT_AMOUNT - WITHDRAW_AMOUNT; uint256 public constant SEGMENT_COUNT = 2; uint40 public immutable START_TIME; uint128 public constant TOTAL_AMOUNT = 10_030.090270812437311935e18; // deposit + broker fee uint40 public constant TOTAL_DURATION = 10_000 seconds; - uint256 public constant TRANCHE_COUNT = 3; + uint256 public constant TRANCHE_COUNT = 2; uint128 public constant TOTAL_TRANSFER_AMOUNT = DEPOSIT_AMOUNT * uint128(BATCH_SIZE); uint128 public constant WITHDRAW_AMOUNT = 2600e18; - uint40 public immutable WARP_26_PERCENT; // 26% of the way through the stream + uint40 public immutable WARP_26_PERCENT; + uint40 public immutable WARP_26_PERCENT_DURATION = 2600 seconds; // 26% of the way through the stream /*////////////////////////////////////////////////////////////////////////// MERKLE-LOCKUP @@ -86,7 +88,7 @@ contract Defaults is Constants, Merkle { END_TIME = START_TIME + TOTAL_DURATION; EXPIRATION = JULY_1_2024 + 12 weeks; MAX_SEGMENT_DURATION = TOTAL_DURATION / uint40(MAX_COUNT); - WARP_26_PERCENT = START_TIME + CLIFF_DURATION + 100 seconds; + WARP_26_PERCENT = START_TIME + WARP_26_PERCENT_DURATION; } /*////////////////////////////////////////////////////////////////////////// @@ -143,10 +145,14 @@ contract Defaults is Constants, Merkle { function segments() public view returns (LockupDynamic.Segment[] memory segments_) { segments_ = new LockupDynamic.Segment[](2); segments_[0] = ( - LockupDynamic.Segment({ amount: 2500e18, exponent: ud2x18(3.14e18), timestamp: START_TIME + CLIFF_DURATION }) + LockupDynamic.Segment({ + amount: 2600e18, + exponent: ud2x18(3.14e18), + timestamp: START_TIME + WARP_26_PERCENT_DURATION + }) ); segments_[1] = ( - LockupDynamic.Segment({ amount: 7500e18, exponent: ud2x18(0.5e18), timestamp: START_TIME + TOTAL_DURATION }) + LockupDynamic.Segment({ amount: 7400e18, exponent: ud2x18(0.5e18), timestamp: START_TIME + TOTAL_DURATION }) ); } @@ -161,23 +167,22 @@ contract Defaults is Constants, Merkle { LockupDynamic.SegmentWithDuration({ amount: segments_[0].amount, exponent: segments_[0].exponent, - duration: 2500 seconds + duration: 2600 seconds }) ); segmentsWithDurations_[1] = ( LockupDynamic.SegmentWithDuration({ amount: segments_[1].amount, exponent: segments_[1].exponent, - duration: 7500 seconds + duration: 7400 seconds }) ); } function tranches() public view returns (LockupTranched.Tranche[] memory tranches_) { - tranches_ = new LockupTranched.Tranche[](3); - tranches_[0] = LockupTranched.Tranche({ amount: 2500e18, timestamp: START_TIME + CLIFF_DURATION }); - tranches_[1] = LockupTranched.Tranche({ amount: 100e18, timestamp: WARP_26_PERCENT }); - tranches_[2] = LockupTranched.Tranche({ amount: 7400e18, timestamp: START_TIME + TOTAL_DURATION }); + tranches_ = new LockupTranched.Tranche[](2); + tranches_[0] = LockupTranched.Tranche({ amount: 2600e18, timestamp: WARP_26_PERCENT }); + tranches_[1] = LockupTranched.Tranche({ amount: 7400e18, timestamp: START_TIME + TOTAL_DURATION }); } function tranchesWithDurations() @@ -185,10 +190,17 @@ contract Defaults is Constants, Merkle { pure returns (LockupTranched.TrancheWithDuration[] memory tranchesWithDurations_) { - tranchesWithDurations_ = new LockupTranched.TrancheWithDuration[](3); - tranchesWithDurations_[0] = LockupTranched.TrancheWithDuration({ amount: 2500e18, duration: 2500 seconds }); - tranchesWithDurations_[1] = LockupTranched.TrancheWithDuration({ amount: 100e18, duration: 100 seconds }); - tranchesWithDurations_[2] = LockupTranched.TrancheWithDuration({ amount: 7400e18, duration: 7400 seconds }); + tranchesWithDurations_ = new LockupTranched.TrancheWithDuration[](2); + tranchesWithDurations_[0] = LockupTranched.TrancheWithDuration({ amount: 2600e18, duration: 2600 seconds }); + tranchesWithDurations_[1] = LockupTranched.TrancheWithDuration({ amount: 7400e18, duration: 7400 seconds }); + } + + function unlockAmounts() public pure returns (LockupLinear.UnlockAmounts memory) { + return LockupLinear.UnlockAmounts({ start: 0, cliff: CLIFF_AMOUNT }); + } + + function unlockAmountsZero() public pure returns (LockupLinear.UnlockAmounts memory) { + return LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }); } /*////////////////////////////////////////////////////////////////////////// @@ -247,7 +259,7 @@ contract Defaults is Constants, Merkle { /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLL} parameters. function batchCreateWithDurationsLL() public view returns (BatchLockup.CreateWithDurationsLL[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithDurationsBrokerNull(), durations(), BATCH_SIZE); + batch = BatchLockupBuilder.fillBatch(createWithDurationsBrokerNull(), unlockAmounts(), durations(), BATCH_SIZE); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithDurationsLT} parameters. @@ -280,7 +292,7 @@ contract Defaults is Constants, Merkle { view returns (BatchLockup.CreateWithTimestampsLL[] memory batch) { - batch = BatchLockupBuilder.fillBatch(createWithTimestampsBrokerNull(), CLIFF_TIME, batchSize); + batch = BatchLockupBuilder.fillBatch(createWithTimestampsBrokerNull(), unlockAmounts(), CLIFF_TIME, batchSize); } /// @dev Returns a default-size batch of {BatchLockup.CreateWithTimestampsLT} parameters. @@ -370,10 +382,10 @@ contract Defaults is Constants, Merkle { { tranches_ = new LockupTranched.Tranche[](2); if (streamStartTime == 0) { - tranches_[0].timestamp = uint40(block.timestamp) + CLIFF_DURATION; + tranches_[0].timestamp = uint40(block.timestamp) + WARP_26_PERCENT_DURATION; tranches_[1].timestamp = uint40(block.timestamp) + TOTAL_DURATION; } else { - tranches_[0].timestamp = streamStartTime + CLIFF_DURATION; + tranches_[0].timestamp = streamStartTime + WARP_26_PERCENT_DURATION; tranches_[1].timestamp = streamStartTime + TOTAL_DURATION; } @@ -397,8 +409,8 @@ contract Defaults is Constants, Merkle { { tranchesWithPercentages_ = new MerkleLT.TrancheWithPercentage[](2); tranchesWithPercentages_[0] = - MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.25e18), duration: 2500 seconds }); + MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.26e18), duration: 2600 seconds }); tranchesWithPercentages_[1] = - MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.75e18), duration: 7500 seconds }); + MerkleLT.TrancheWithPercentage({ unlockPercentage: ud2x18(0.74e18), duration: 7400 seconds }); } } diff --git a/test/utils/Modifiers.sol b/test/utils/Modifiers.sol index 5d33a85d0..4836fd660 100644 --- a/test/utils/Modifiers.sol +++ b/test/utils/Modifiers.sol @@ -345,6 +345,10 @@ abstract contract Modifiers is Fuzzers { _; } + modifier whenUnlockAmountsSumNotExceedDepositAmount() { + _; + } + /*////////////////////////////////////////////////////////////////////////// CREATE-MERKLE //////////////////////////////////////////////////////////////////////////*/ @@ -457,6 +461,10 @@ abstract contract Modifiers is Fuzzers { _; } + modifier givenNoStartAmount() { + _; + } + /*////////////////////////////////////////////////////////////////////////// TRANSFER-ADMIN //////////////////////////////////////////////////////////////////////////*/