Refactor lockup contracts to move all common logic in SablierV2Lockup
#805
Replies: 4 comments 12 replies
-
I want to mention that I would like to make a decision on this proposal first and then create a PR on this repo for the new |
Beta Was this translation helpful? Give feedback.
-
Thanks for the thoughtful proposal, @andreivladbrg. It seems hat you've put a lot of effort in this. Motivation
The development work time and the maintenance cost are a valid concern, but:
SolutionIt is an interesting proposal.
Not correct. All child contracts will have to also implement
I agree. Other considerations
I did not understand this part. When you say "the same gas cost as the current version", do you mean that if we removed
If we keep the API unchanged, I expect the impact to be minimal.
I've made the calculations myself using this query.
Harm 1Harm 2ComparisonIt thus seems that the harm factor is greater when all users pay 138k gas for the cliff feature compared to when only a portion pay 182k. Combined with the additional development and audit cost of implementing the DecisionI'm tentatively in favor of this proposal, but I have some concerns and I want to mention them:
|
Beta Was this translation helpful? Give feedback.
-
Motivation & Solution
we can't know it precisely in advance
probably, but why build a system that requires additional components on top to catch these types of errors (identity code check) when you can design it from the beginning to not be concerned with them?
yeah, forgot about Other considerations
yes, that's what i meant
the tests were not modified, and none failed, so i don't think there is even a change in the API
yes, this is the dilemma
yes, this is correct for the current version of the refactor. but an idea came to me (see the code snippet below) what if we: Scenario 1LL user wants to create a linear curve stream:
Scenario 2LL user wants to create a cliff curve stream:
in this way, we would minimize the costs for all users: linear curve users would not pay the gas for the storage of the cliff time variable, linear cliff curve users would pay the extra costs that dynamic contract would have. code snip /// create function
function _createWithTimestamps(LockupLinear.CreateWithTimestamps memory params)
internal
returns (uint256 streamId)
{
// -- rest of the code --
// this function needs to be updated
Helpers.checkCreateWithTimestamps(createAmounts.deposit, params.range);
if (params.range.cliff > 0) {
_cliffs[streamId] = params.range.cliff;
}
// -- rest of the code --
}
function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) {
uint256 cliffTime = uint256(_cliffs[streamId]);
uint256 currentTime = block.timestamp;
if (cliffTime > 0 && cliffTime > currentTime) {
return 0;
}
// -- rest of the code --`
}
so far, this version looks the best for me. we would have the robust design without the gas implications for the majority of the linear users, 72% as per your finding, (Linear-Cliff user would still have to pay the extra 21k gas). Decision
i am glad
yes, this would increase the audit costs, but i doubt it would be by a lot, since the API remains the same. also, we already have a lot of things on the line (tranched + campaigns for them), so this would be a small thing, imo.
yeah, that was my concern as well. I have looked at the storage properties a lot, and all of them simply make sense in a
yeah, i want to add that this concern can be applied to the current version as well: |
Beta Was this translation helpful? Give feedback.
-
The problem that you have identified @andreivladbrg is really good. I am very glad that you started this thoughtful discussion and worked on the changes. Clearly, there is a trade-off between gas cost optimization and maintenance cost. But this looks like a balance between the two (except for the extra storage slot in LL). So, I am in favour of it. However, I haven't reviewed the code yet but could see optimizations such as mapping(uint256 id => uint256 cliff) internal _cliffs instead of mapping(uint256 id => uint40 cliff) internal _cliffs Regardless, I will review it in detail when you raise a PR for it. |
Beta Was this translation helpful? Give feedback.
-
Motivation
Because I've been working on implementing the new Sablier streaming model, i.e. LockupTranched, it reminded my self how much duplicated logic exists between
LockupLinear
andLockupDynamic
contracts.This thing is caused by the different
Stream
(LD,LL) data structures stored in the contracts:v2-core/src/SablierV2LockupDynamic.sol
Line 62 in ea6f097
v2-core/src/SablierV2LockupLinear.sol
Line 50 in ea6f097
As we plan to implement multiple models of
Lockup
contracts in the future, this issue will increasingly cause more harm, in terms of dev work time, maintenance, and ofc more prone to errors (as we discussed here #771 - also, we would not need to implement that "code identity" test) and I don't think we should leave it as it is.Solution
Note: I've already implemented the changes on this branch.
Given that the structs share more than 90% of the variables, it was inevitable to find a solution by adding a new data structure within the
Lockup
namespace. This structure will be used in all child contracts ofSablierV2Lockup
.So the design I am proposing is:
Stream
entity and it is going to be placed inSablierV2Lockup
mapping(uint256 id => uint40 cliff) internal _cliffs;
mapping(uint256 id => LockupDynamic.Segment[] segments) internal _segments;
ERC721
)create
functions and the calculation function:_calculateStreamedAmount
getStream
function will return the same data structure as the current version, but this one is going to be built in the view functionThe only downside I can think of is the gas implications in the
create
function, this design will require one moremapping
.I've performed a benchmark and this is the result:
Later edit: the gas costs are increased in the linear contract only when there is a cliff linear curve stream created. when there is a simple linear stream, no gas increases (benchmarked at 116052).
Note: the gas costs are for the
createWithTimestamps
function and in dynamic contract 2 segments were used.In the linear contract, there is indeed a significant difference, due to the introduction of a new storage slot. In the dynamic contract, there is not a significant difference because the segments were already located at the end of the stream struct.
Despite the increase in gas, I believe that this refactor is the right choice to make. The code reduction amounts to almost 300 lines lines in the
src
dir. If we were to addTranched
, we would have even greater improvement.It is a more robust and a much cleaner design.
Other considerationLater edit: as per my comment below, this consideration is not needed anymore because we would leave cliff = 0 in simple linear sterams.
Since the only unique thing in the linear stream is the
cliff time
variable, and we can achieve the same feature in the dynamic contract with a 2-segment stream, we can consider removing the cliff streaming curve for linear streams. In this case, the only curve achievable by this contract would be:This approach would allow us to maintain the same gas cost as the current version or even slightly reduce it due to the removal of the
cliffTime
. However, before making this decision, we should make a query on Dune for lockup linear contracts to see how frequently this curve is used. Also, we need to see what's the impact at the app level.I am very curious about your opinion @PaulRBerg @smol-ninja.
You can view the work here.
Beta Was this translation helpful? Give feedback.
All reactions