From 7cdba72369647b885fcf1e74a74b520b79935052 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Sun, 5 Jan 2025 14:31:26 +0200 Subject: [PATCH 1/6] refactor: add blockTimestamp param in VestingMath functions refactor: remove unchecked in calculateStreamedPercentage docs: add assumptions in VestingMath function docs: improve explanatory comments --- src/LockupNFTDescriptor.sol | 5 +--- src/SablierLockup.sol | 11 ++++++-- src/interfaces/ISablierLockup.sol | 3 ++ src/interfaces/ISablierLockupBase.sol | 3 +- src/libraries/VestingMath.sol | 40 +++++++++++++++++++-------- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/LockupNFTDescriptor.sol b/src/LockupNFTDescriptor.sol index bc313fc7c..0f5e35edf 100644 --- a/src/LockupNFTDescriptor.sol +++ b/src/LockupNFTDescriptor.sol @@ -198,10 +198,7 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor { pure returns (uint256) { - // This cannot overflow because both inputs are uint128s, and zero deposit amounts are not allowed in Sablier. - unchecked { - return streamedAmount * 10_000 / depositedAmount; - } + return streamedAmount * 10_000 / depositedAmount; } /// @notice Generates a pseudo-random HSL color by hashing together the `chainid`, the `sablier` address, diff --git a/src/SablierLockup.sol b/src/SablierLockup.sol index b12a632a6..b94376c99 100644 --- a/src/SablierLockup.sol +++ b/src/SablierLockup.sol @@ -184,7 +184,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { // Calculate the cliff time and the end time. It is safe to use unchecked arithmetic because {_createLL} will // nonetheless check that the end time is greater than the cliff time, and also that the cliff time, if set, - // is greater than or equal to the start time. + // is greater than the start time. unchecked { if (durations.cliff > 0) { cliffTime = timestamps.start + durations.cliff; @@ -302,7 +302,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { // If the start time is in the future, return zero. uint40 blockTimestamp = uint40(block.timestamp); - if (timestamps.start >= blockTimestamp) { + if (timestamps.start > blockTimestamp) { return 0; } @@ -319,6 +319,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({ segments: _segments[streamId], + blockTimestamp: blockTimestamp, startTime: timestamps.start, withdrawnAmount: _streams[streamId].amounts.withdrawn }); @@ -327,6 +328,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { streamedAmount = VestingMath.calculateLockupLinearStreamedAmount({ depositedAmount: depositedAmount, + blockTimestamp: blockTimestamp, timestamps: timestamps, cliffTime: _cliffs[streamId], unlockAmounts: _unlockAmounts[streamId], @@ -335,7 +337,10 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { } // Calculate streamed amount for Lockup Tranched model. else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { - streamedAmount = VestingMath.calculateLockupTranchedStreamedAmount({ tranches: _tranches[streamId] }); + streamedAmount = VestingMath.calculateLockupTranchedStreamedAmount({ + blockTimestamp: blockTimestamp, + tranches: _tranches[streamId] + }); } return streamedAmount; diff --git a/src/interfaces/ISablierLockup.sol b/src/interfaces/ISablierLockup.sol index 03e0404b9..158a24157 100644 --- a/src/interfaces/ISablierLockup.sol +++ b/src/interfaces/ISablierLockup.sol @@ -164,6 +164,7 @@ interface ISablierLockup is ISablierLockupBase { /// - `params.recipient` must not be the zero address. /// - `params.sender` must not be the zero address. /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` tokens. + /// - `params.shape.length` must not be greater than 32 characters. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. /// @param segments Segments used to compose the dynamic distribution function. @@ -198,6 +199,7 @@ interface ISablierLockup is ISablierLockupBase { /// 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` tokens. + /// - `params.shape.length` must not be greater than 32 characters. /// /// @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. @@ -234,6 +236,7 @@ interface ISablierLockup is ISablierLockupBase { /// - `params.recipient` must not be the zero address. /// - `params.sender` must not be the zero address. /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` tokens. + /// - `params.shape.length` must not be greater than 32 characters. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. /// @param tranches Tranches used to compose the tranched distribution function. diff --git a/src/interfaces/ISablierLockupBase.sol b/src/interfaces/ISablierLockupBase.sol index ac11b35b5..fe3e82ba2 100644 --- a/src/interfaces/ISablierLockupBase.sol +++ b/src/interfaces/ISablierLockupBase.sol @@ -246,7 +246,8 @@ interface ISablierLockupBase is /// Notes: /// - If there any tokens left for the recipient to withdraw, the stream is marked as canceled. Otherwise, the /// stream is marked as depleted. - /// - This function attempts to invoke a hook on the recipient, if the resolved address is a contract. + /// - This function attempts to call a hook on the recipient of the stream, unless msg.sender is the recipient or + /// the recipient is not on the allowlist. /// /// Requirements: /// - Must not be delegate called. diff --git a/src/libraries/VestingMath.sol b/src/libraries/VestingMath.sol index c4255326c..0fabddf82 100644 --- a/src/libraries/VestingMath.sol +++ b/src/libraries/VestingMath.sol @@ -33,18 +33,23 @@ library VestingMath { /// 2. The stream's start time must be in the past so that the calculations below do not overflow. /// 3. The stream's end time must be in the future so that the loop below does not panic with an "index out of /// bounds" error. + /// + /// Assumptions: + /// 1. The sum of all segment amounts does not overflow uint128. + /// 2. The segment timestamps are ordered chronologically. + /// 3. There are no duplicate segment timestamps. + /// 4. The number of segments does not exceed MAX_COUNT. function calculateLockupDynamicStreamedAmount( LockupDynamic.Segment[] memory segments, + uint40 blockTimestamp, uint40 startTime, uint128 withdrawnAmount ) public - view + pure returns (uint128) { unchecked { - uint40 blockTimestamp = uint40(block.timestamp); - // Sum the amounts in all segments that precede the block timestamp. uint128 previousSegmentAmounts; uint40 currentSegmentTimestamp = segments[0].timestamp; @@ -100,7 +105,9 @@ library VestingMath { /// @dev Lockup linear model uses the following distribution function: /// /// $$ - /// f(x) = x * sa + s + c + /// ( x * sa + s, cliffTime > blockTimestamp + /// f(x) = ( + /// ( x * sa + s + c, cliffTime <= blockTimestamp /// $$ /// /// Where: @@ -109,19 +116,23 @@ library VestingMath { /// - $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. + /// + /// Assumptions: + /// 1. unlockAmounts.start + unlockAmounts.cliff does not overflow uint128 + /// 2. timestamps.start < timestamps.end and timestamps.start <= block.timestamp <= timestamps.end + /// 3. If cliffTime > 0, timestamps.start < cliffTime < timestamps.end function calculateLockupLinearStreamedAmount( uint128 depositedAmount, + uint40 blockTimestamp, Lockup.Timestamps memory timestamps, uint40 cliffTime, LockupLinear.UnlockAmounts memory unlockAmounts, uint128 withdrawnAmount ) public - view + pure returns (uint128) { - uint256 blockTimestamp = block.timestamp; - // If the cliff time is in the future, return the start unlock amount. if (cliffTime > blockTimestamp) { return unlockAmounts.start; @@ -177,13 +188,20 @@ library VestingMath { /// Where: /// /// - $\Sigma(eta)$ is the sum of all vested tranches' amounts. - function calculateLockupTranchedStreamedAmount(LockupTranched.Tranche[] memory tranches) + /// + /// Assumptions: + /// 1. The sum of all tranche amounts does not overflow uint128. + /// 2. The tranche timestamps are ordered chronologically. + /// 3. There are no duplicate tranche timestamps. + /// 4. The number of segments does not exceed MAX_COUNT. + function calculateLockupTranchedStreamedAmount( + uint40 blockTimestamp, + LockupTranched.Tranche[] memory tranches + ) public - view + pure returns (uint128) { - uint256 blockTimestamp = block.timestamp; - // If the first tranche's timestamp is in the future, return zero. if (tranches[0].timestamp > blockTimestamp) { return 0; From 43cf22380645cdcce4e8b2ba53c7d71388df0131 Mon Sep 17 00:00:00 2001 From: Paul Razvan Berg Date: Mon, 6 Jan 2025 13:37:55 +0200 Subject: [PATCH 2/6] docs: fix typos --- benchmark/LockupDynamic.Gas.t.sol | 2 +- benchmark/LockupLinear.Gas.t.sol | 2 +- benchmark/LockupTranched.Gas.t.sol | 2 +- benchmark/results/SablierLockup_Dynamic.md | 2 +- benchmark/results/SablierLockup_Linear.md | 2 +- benchmark/results/SablierLockup_Tranched.md | 2 +- src/SablierLockup.sol | 12 ++++++------ 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/benchmark/LockupDynamic.Gas.t.sol b/benchmark/LockupDynamic.Gas.t.sol index 2d22f290a..fc604b8aa 100644 --- a/benchmark/LockupDynamic.Gas.t.sol +++ b/benchmark/LockupDynamic.Gas.t.sol @@ -38,7 +38,7 @@ contract Lockup_Dynamic_Gas_Test is Benchmark_Test { vm.writeFile({ path: benchmarkResultsFile, data: string.concat( - "# Benchmarks for Lockup Dynamic Model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" + "# Benchmarks for the Lockup Dynamic model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" ) }); diff --git a/benchmark/LockupLinear.Gas.t.sol b/benchmark/LockupLinear.Gas.t.sol index 4cd5c19f1..7b9f7c8f2 100644 --- a/benchmark/LockupLinear.Gas.t.sol +++ b/benchmark/LockupLinear.Gas.t.sol @@ -22,7 +22,7 @@ contract Lockup_Linear_Gas_Test is Benchmark_Test { vm.writeFile({ path: benchmarkResultsFile, data: string.concat( - "# Benchmarks for Lockup Linear Model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" + "# Benchmarks for the Lockup Linear model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" ) }); diff --git a/benchmark/LockupTranched.Gas.t.sol b/benchmark/LockupTranched.Gas.t.sol index faf8cd468..0ef67fe0c 100644 --- a/benchmark/LockupTranched.Gas.t.sol +++ b/benchmark/LockupTranched.Gas.t.sol @@ -29,7 +29,7 @@ contract Lockup_Tranched_Gas_Test is Benchmark_Test { vm.writeFile({ path: benchmarkResultsFile, data: string.concat( - "# Benchmarks for Lockup Tranched Model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" + "# Benchmarks for the Lockup Tranched model\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" ) }); diff --git a/benchmark/results/SablierLockup_Dynamic.md b/benchmark/results/SablierLockup_Dynamic.md index cc2532d30..10769bbe6 100644 --- a/benchmark/results/SablierLockup_Dynamic.md +++ b/benchmark/results/SablierLockup_Dynamic.md @@ -1,4 +1,4 @@ -# Benchmarks for Lockup Dynamic Model +# Benchmarks for the Lockup Dynamic model | Implementation | Gas Usage | | ------------------------------------------------------------ | --------- | diff --git a/benchmark/results/SablierLockup_Linear.md b/benchmark/results/SablierLockup_Linear.md index ecbd9521f..9d9adb00c 100644 --- a/benchmark/results/SablierLockup_Linear.md +++ b/benchmark/results/SablierLockup_Linear.md @@ -1,4 +1,4 @@ -# Benchmarks for Lockup Linear Model +# Benchmarks for the Lockup Linear model | Implementation | Gas Usage | | ------------------------------------------------------------- | --------- | diff --git a/benchmark/results/SablierLockup_Tranched.md b/benchmark/results/SablierLockup_Tranched.md index 9a768d1a0..1db66e9fb 100644 --- a/benchmark/results/SablierLockup_Tranched.md +++ b/benchmark/results/SablierLockup_Tranched.md @@ -1,4 +1,4 @@ -# Benchmarks for Lockup Tranched Model +# Benchmarks for the Lockup Tranched model | Implementation | Gas Usage | | ------------------------------------------------------------ | --------- | diff --git a/src/SablierLockup.sol b/src/SablierLockup.sol index b94376c99..6004247d7 100644 --- a/src/SablierLockup.sol +++ b/src/SablierLockup.sol @@ -315,7 +315,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { uint128 streamedAmount; Lockup.Model lockupModel = _streams[streamId].lockupModel; - // Calculate streamed amount for Lockup Dynamic model. + // Calculate streamed amount for the Lockup Dynamic model. if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({ segments: _segments[streamId], @@ -324,7 +324,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { withdrawnAmount: _streams[streamId].amounts.withdrawn }); } - // Calculate streamed amount for Lockup Linear model. + // Calculate streamed amount for the Lockup Linear model. else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { streamedAmount = VestingMath.calculateLockupLinearStreamedAmount({ depositedAmount: depositedAmount, @@ -335,7 +335,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { withdrawnAmount: _streams[streamId].amounts.withdrawn }); } - // Calculate streamed amount for Lockup Tranched model. + // Calculate streamed amount for the Lockup Tranched model. else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { streamedAmount = VestingMath.calculateLockupTranchedStreamedAmount({ blockTimestamp: blockTimestamp, @@ -476,16 +476,16 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { // Load the stream ID in a variable. streamId = nextStreamId; - // Effect: set the start unlock amount if its non-zero. + // Effect: set the start unlock amount if it is non-zero. if (unlockAmounts.start > 0) { _unlockAmounts[streamId].start = unlockAmounts.start; } - // Effect: update cliff time if its non-zero. + // Effect: update cliff time if it is non-zero. if (cliffTime > 0) { _cliffs[streamId] = cliffTime; - // Effect: set the cliff unlock amount if its non-zero. + // Effect: set the cliff unlock amount if it is non-zero. if (unlockAmounts.cliff > 0) { _unlockAmounts[streamId].cliff = unlockAmounts.cliff; } From e270e09981929c32863ea1a552acd77e65d414d4 Mon Sep 17 00:00:00 2001 From: Paul Razvan Berg Date: Mon, 6 Jan 2025 19:14:21 +0200 Subject: [PATCH 3/6] docs: polish natspec --- src/interfaces/ISablierLockupBase.sol | 8 ++++---- src/libraries/VestingMath.sol | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/interfaces/ISablierLockupBase.sol b/src/interfaces/ISablierLockupBase.sol index fe3e82ba2..ade9d9ef0 100644 --- a/src/interfaces/ISablierLockupBase.sol +++ b/src/interfaces/ISablierLockupBase.sol @@ -246,8 +246,7 @@ interface ISablierLockupBase is /// Notes: /// - If there any tokens left for the recipient to withdraw, the stream is marked as canceled. Otherwise, the /// stream is marked as depleted. - /// - This function attempts to call a hook on the recipient of the stream, unless msg.sender is the recipient or - /// the recipient is not on the allowlist. + /// - If the address is on the allowlist, this function will invoke a hook on the recipient. /// /// Requirements: /// - Must not be delegate called. @@ -275,7 +274,7 @@ interface ISablierLockupBase is /// @dev Emits a {CollectFees} event. /// /// Notes: - /// - If the admin is a contract, it must be able to receive ETH. + /// - If the admin is a contract, it must be able to receive native token payments, e.g., ETH for Ethereum Mainnet. function collectFees() external; /// @notice Removes the right of the stream's sender to cancel the stream. @@ -325,7 +324,8 @@ interface ISablierLockupBase is /// @dev Emits a {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} event. /// /// Notes: - /// - This function attempts to call a hook on the recipient of the stream, unless `msg.sender` is the recipient. + /// - If `msg.sender` is not the recipient and the address is on the allowlist, this function will invoke a hook on + /// the recipient. /// /// Requirements: /// - Must not be delegate called. diff --git a/src/libraries/VestingMath.sol b/src/libraries/VestingMath.sol index 0fabddf82..cb5507242 100644 --- a/src/libraries/VestingMath.sol +++ b/src/libraries/VestingMath.sol @@ -117,6 +117,7 @@ library VestingMath { /// - $s$ is the start unlock amount. /// - $c$ is the cliff unlock amount. /// + /// TODO: edit this /// Assumptions: /// 1. unlockAmounts.start + unlockAmounts.cliff does not overflow uint128 /// 2. timestamps.start < timestamps.end and timestamps.start <= block.timestamp <= timestamps.end From be050b915cf860ba8fb5280ab438aa6553616e75 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Tue, 7 Jan 2025 02:15:06 +0200 Subject: [PATCH 4/6] refactor: move logic in VestingMath chore: remove MAX_COUNT assumption --- src/SablierLockup.sol | 23 +++++++-------------- src/libraries/VestingMath.sol | 39 +++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/SablierLockup.sol b/src/SablierLockup.sol index 6004247d7..bd8300504 100644 --- a/src/SablierLockup.sol +++ b/src/SablierLockup.sol @@ -297,30 +297,21 @@ 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. + // Load the stream's parameters in memory. uint40 blockTimestamp = uint40(block.timestamp); - if (timestamps.start > blockTimestamp) { - return 0; - } - - // If the end time is not in the future, return the deposited amount. uint128 depositedAmount = _streams[streamId].amounts.deposited; - if (timestamps.end <= blockTimestamp) { - return depositedAmount; - } - - uint128 streamedAmount; Lockup.Model lockupModel = _streams[streamId].lockupModel; + uint128 streamedAmount; + Lockup.Timestamps memory timestamps = + Lockup.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); // Calculate streamed amount for the Lockup Dynamic model. if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({ + depositedAmount: depositedAmount, segments: _segments[streamId], blockTimestamp: blockTimestamp, - startTime: timestamps.start, + timestamps: timestamps, withdrawnAmount: _streams[streamId].amounts.withdrawn }); } @@ -338,7 +329,9 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { // Calculate streamed amount for the Lockup Tranched model. else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { streamedAmount = VestingMath.calculateLockupTranchedStreamedAmount({ + depositedAmount: depositedAmount, blockTimestamp: blockTimestamp, + timestamps: timestamps, tranches: _tranches[streamId] }); } diff --git a/src/libraries/VestingMath.sol b/src/libraries/VestingMath.sol index cb5507242..74e0c6c2c 100644 --- a/src/libraries/VestingMath.sol +++ b/src/libraries/VestingMath.sol @@ -38,17 +38,27 @@ library VestingMath { /// 1. The sum of all segment amounts does not overflow uint128. /// 2. The segment timestamps are ordered chronologically. /// 3. There are no duplicate segment timestamps. - /// 4. The number of segments does not exceed MAX_COUNT. function calculateLockupDynamicStreamedAmount( + uint128 depositedAmount, LockupDynamic.Segment[] memory segments, uint40 blockTimestamp, - uint40 startTime, + Lockup.Timestamps memory timestamps, uint128 withdrawnAmount ) public pure returns (uint128) { + // If the start time is in the future, return zero. + if (timestamps.start > blockTimestamp) { + return 0; + } + + // If the end time is not in the future, return the deposited amount. + if (timestamps.end <= blockTimestamp) { + return depositedAmount; + } + unchecked { // Sum the amounts in all segments that precede the block timestamp. uint128 previousSegmentAmounts; @@ -69,7 +79,7 @@ library VestingMath { if (index == 0) { // When the current segment's index is equal to 0, the current segment is the first, so use the start // time as the previous timestamp. - previousTimestamp = startTime; + previousTimestamp = timestamps.start; } else { // Otherwise, when the current segment's index is greater than zero, it means that the segment is not // the first. In this case, use the previous segment's timestamp. @@ -134,6 +144,16 @@ library VestingMath { pure returns (uint128) { + // If the start time is in the future, return zero. + if (timestamps.start > blockTimestamp) { + return 0; + } + + // If the end time is not in the future, return the deposited amount. + if (timestamps.end <= blockTimestamp) { + return depositedAmount; + } + // If the cliff time is in the future, return the start unlock amount. if (cliffTime > blockTimestamp) { return unlockAmounts.start; @@ -194,15 +214,26 @@ library VestingMath { /// 1. The sum of all tranche amounts does not overflow uint128. /// 2. The tranche timestamps are ordered chronologically. /// 3. There are no duplicate tranche timestamps. - /// 4. The number of segments does not exceed MAX_COUNT. function calculateLockupTranchedStreamedAmount( + uint128 depositedAmount, uint40 blockTimestamp, + Lockup.Timestamps memory timestamps, LockupTranched.Tranche[] memory tranches ) public pure returns (uint128) { + // If the start time is in the future, return zero. + if (timestamps.start > blockTimestamp) { + return 0; + } + + // If the end time is not in the future, return the deposited amount. + if (timestamps.end <= blockTimestamp) { + return depositedAmount; + } + // If the first tranche's timestamp is in the future, return zero. if (tranches[0].timestamp > blockTimestamp) { return 0; From 51dfa58cbf32dc3e221054b808731492c2919495 Mon Sep 17 00:00:00 2001 From: Paul Razvan Berg Date: Tue, 7 Jan 2025 21:56:40 +0200 Subject: [PATCH 5/6] docs: polish assumptions --- src/libraries/VestingMath.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libraries/VestingMath.sol b/src/libraries/VestingMath.sol index 74e0c6c2c..be75ac47b 100644 --- a/src/libraries/VestingMath.sol +++ b/src/libraries/VestingMath.sol @@ -127,11 +127,10 @@ library VestingMath { /// - $s$ is the start unlock amount. /// - $c$ is the cliff unlock amount. /// - /// TODO: edit this /// Assumptions: - /// 1. unlockAmounts.start + unlockAmounts.cliff does not overflow uint128 - /// 2. timestamps.start < timestamps.end and timestamps.start <= block.timestamp <= timestamps.end - /// 3. If cliffTime > 0, timestamps.start < cliffTime < timestamps.end + /// 1. The sum of the unlock amounts (start and cliff) does not overflow uint128. + /// 2. The start time is before the end time, and the block timestamp is between the start and end times. + /// 3. If the cliff time is not zero, it is after the start time and before the end time. function calculateLockupLinearStreamedAmount( uint128 depositedAmount, uint40 blockTimestamp, From 2e815ce2df72b57a95401f7a627cf945d24b8853 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu Date: Tue, 7 Jan 2025 23:54:47 +0200 Subject: [PATCH 6/6] refactor: use unchecked and casting in calculateStreamedPercentage docs: last polishes --- src/LockupNFTDescriptor.sol | 5 ++++- src/SablierLockup.sol | 8 ++++---- src/libraries/VestingMath.sol | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/LockupNFTDescriptor.sol b/src/LockupNFTDescriptor.sol index 0f5e35edf..d0885901e 100644 --- a/src/LockupNFTDescriptor.sol +++ b/src/LockupNFTDescriptor.sol @@ -198,7 +198,10 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor { pure returns (uint256) { - return streamedAmount * 10_000 / depositedAmount; + // This cannot overflow because both inputs are uint128s, and zero deposit amounts are not allowed in Sablier. + unchecked { + return uint256(streamedAmount) * 10_000 / depositedAmount; + } } /// @notice Generates a pseudo-random HSL color by hashing together the `chainid`, the `sablier` address, diff --git a/src/SablierLockup.sol b/src/SablierLockup.sol index bd8300504..47389397b 100644 --- a/src/SablierLockup.sol +++ b/src/SablierLockup.sol @@ -297,7 +297,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { /// @inheritdoc SablierLockupBase function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { - // Load the stream's parameters in memory. + // Load in memory the parameters used in {VestingMath}. uint40 blockTimestamp = uint40(block.timestamp); uint128 depositedAmount = _streams[streamId].amounts.deposited; Lockup.Model lockupModel = _streams[streamId].lockupModel; @@ -305,7 +305,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { Lockup.Timestamps memory timestamps = Lockup.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); - // Calculate streamed amount for the Lockup Dynamic model. + // Calculate the streamed amount for the Lockup Dynamic model. if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({ depositedAmount: depositedAmount, @@ -315,7 +315,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { withdrawnAmount: _streams[streamId].amounts.withdrawn }); } - // Calculate streamed amount for the Lockup Linear model. + // Calculate the streamed amount for the Lockup Linear model. else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { streamedAmount = VestingMath.calculateLockupLinearStreamedAmount({ depositedAmount: depositedAmount, @@ -326,7 +326,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase { withdrawnAmount: _streams[streamId].amounts.withdrawn }); } - // Calculate streamed amount for the Lockup Tranched model. + // Calculate the streamed amount for the Lockup Tranched model. else if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { streamedAmount = VestingMath.calculateLockupTranchedStreamedAmount({ depositedAmount: depositedAmount, diff --git a/src/libraries/VestingMath.sol b/src/libraries/VestingMath.sol index be75ac47b..fd507bbd0 100644 --- a/src/libraries/VestingMath.sol +++ b/src/libraries/VestingMath.sol @@ -115,9 +115,9 @@ library VestingMath { /// @dev Lockup linear model uses the following distribution function: /// /// $$ - /// ( x * sa + s, cliffTime > blockTimestamp + /// ( x * sa + s, block timestamp < cliff time /// f(x) = ( - /// ( x * sa + s + c, cliffTime <= blockTimestamp + /// ( x * sa + s + c, block timestamp => cliff time /// $$ /// /// Where: