-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Abort function in Merkle vesting campaigns #35
Comments
Note for @sablier-labs/frontend — we don't need to rush with implementing this functionality in the UI. It's likely that it will be needed only in special circumstances, e.g., when there are conflicts between Sablier users. So as far as the frontend is concerned, we can keep in on the back-burner for a while. |
The cancel function should also create stream for tokens already vested in case of ranged airstreams, and mark it as claimed. |
Why do that when we can just add another mapping Though I guess that by creating a stream and actually claiming the airstream, we would keep the gas cost for the |
I have an idea. What if we add both a uint128 claimableAmount = VestingMath.calculateLockupLinearStreamedAmount({
depositedAmount: depositedAmount,
blockTimestamp: canceledCampaignTimestamp // pass the timestamp at which it got canceled
timestamps: timestamps,
cliffTime: _cliffs[streamId],
unlockAmounts: _unlockAmounts[streamId],
withdrawnAmount: _streams[streamId].amounts.withdrawn
});
// then just transfer the claimableAmount until that point
TOKEN.transfer(recipient, claimableAmount);
return; In this way, the recipient will also receive the funds streamed up to the cancellation point, thus both parties are happy. |
If the cancel does not create stream, then we will also have to store refunded amount in the storage, to ensure that when user claims, the stream is created for (airdrop amount - refunded amount). This will also make claim execution look ugly since the original amount is the part of the merkle tree. |
To @andreivladbrg, interesting idea but
imo if we create stream or transfer streamed tokens directly to the recipient without creating stream when user cancels and then mark it as claimed, we wont have to introduce any additional logic or storage variable at all. Wdyt? |
Ooh, right. In this case, yeah, creating a stream is the easiest way. However, I'm reluctant to call this function How about |
"terminate" sounds good to me. Instead of creating the stream, I'd recommend to transfer the tokens directly to the recipient, since the vesting would come to an end when sender calls this function (similar to #37). |
That makes sense, but it may create trouble for data analytics for airstreams. A portion of the airstream data would need to be sourced from Lockup, and another portion from Merkle. |
I see. Wouldn't it be the same problem in #37 as well? |
Yeah, fair enough, let's go with the immediate transfer option. |
We will have to go with @andreivladbrg's proposal of storing termination timestamp in the storage. And then in claim, calculate the amount vested until termination timestamp and transfer it to the recipient. Otherwise if recipient is a blacklisted address, sender wont be able to terminate it. |
Sheesh, nice one @smol-ninja, you are correct. I will pursue with this implementation. |
Re terminology, my preferences are |
Isn't that what is happening here? A vesting schedule is already created and then an action (cancel) is being taken on it. |
IMO the vesting schedule is created when the stream is created. Anytime before this, I would refer to it as "schedule setup" |
I disagree. Vesting schedule is created as soon as the campaign is launched in case of ranged airstreams. End users don't care about the internal logic. For them and also for the campaign creators, streams vesting schedule are synchronous for all users. But I am not strongly against |
Agree with @smol-ninja, but I also don't think there's any meaningful semantic difference between Let's go with
Good catch. This risk was first reported in the 2023 audit performed by Cantina: https://github.com/cantinasec/review-sablier/issues/11 |
Question re implementation. Should we add some relevant checks? Or the admin should be allowed to abort a stream no matter what? The checks I have in mind are these (lmk if you have other in mind):
The time checks are important because, in the case of a stream being aborted after the end time (for the linear Merkle contract), the Relevant test for Vesting math
function test_VestingMathFunctions_Tranched_BlockTime_Greater() public view {
uint40 time = _defaultParams.tranches[_defaultParams.tranches.length - 1].timestamp + 10_000;
uint128 amount = VestingMath.calculateLockupTranchedStreamedAmount(time, _defaultParams.tranches);
console.log("amount:", amount);
console.log("is equal to deposit: ", amount == defaults.DEPOSIT_AMOUNT());
}
function test_VestingMathFunctions_Linear_BlockTime_Greater() public view {
uint40 time = _defaultParams.tranches[_defaultParams.tranches.length - 1].timestamp + 10_000;
uint128 amount = VestingMath.calculateLockupLinearStreamedAmount(
defaults.DEPOSIT_AMOUNT(),
time,
_defaultParams.createWithTimestamps.timestamps,
_defaultParams.cliffTime,
_defaultParams.unlockAmounts,
0 // pass 0 for the withadrawn amount
);
console.log("amount:", amount);
console.log("is equal zero ", amount == 0);
}
function test_VestingMathFunctions_Tranched_BlockTime_Less() public view {
uint40 time = _defaultParams.createWithTimestamps.timestamps.start - 10_000;
uint128 amount = VestingMath.calculateLockupTranchedStreamedAmount(time, _defaultParams.tranches);
console.log("amount:", amount);
console.log("is equal zero: ", amount == 0);
}
function test_VestingMathFunctions_Linear_BlockTime_Less() public {
uint40 time = _defaultParams.createWithTimestamps.timestamps.start - 10_000;
_defaultParams.cliffTime = 0;
_defaultParams.createWithTimestamps.totalAmount =
_defaultParams.createWithTimestamps.totalAmount + _defaultParams.unlockAmounts.cliff;
_defaultParams.unlockAmounts.cliff = 0;
uint128 amount = VestingMath.calculateLockupLinearStreamedAmount(
defaults.DEPOSIT_AMOUNT(),
time,
_defaultParams.createWithTimestamps.timestamps,
_defaultParams.cliffTime,
_defaultParams.unlockAmounts,
0 //
);
console.log("amount:", amount);
console.log("is equal zero: ", amount == 0);
}
The issue with the end-time check for the tranche Merkle is that there’s no straightforward way to calculate the end time, apart from calling _calculateStartTimeAndTranches and then using @sablier-labs/solidity RFF |
It should not revert if campaign has expired. Stream end time can be greater than expiry timestamp.
Agree.
I think it should revert but as you said its complicated to calculate end time. What if we don't revert and let sender receive 0 amount (since there is no amount to be canceled anymore).
We should let sender abort it even if the vesting has not begun. I don't see a reason why it should revert. We allow sender to cancel a Lockup stream even if it has not started right? |
Yes, that’s correct—the end time can be greater than the expiry timestamp. However, that’s not the issue here. It simply doesn’t make sense to abort something that can’t be claimed
There would be a discrepancy between MerkleLL and MerkleLT. MerkleLL would receive 0, while MerkleLT would receive the deposit amount. Refer to the tests
i agree. |
Actually, I don’t agree. It’s not the same because, in the |
Good point. I missed that.
I see. Shouldn't the Vesting Math function return deposit amount for both LL and LT if block time > end time? We should change that in vesting math logic right?
If the abort time < start time, why do we need to interact with |
We could do that, but it would mean duplicating logic across all functions, and I think it’s fine as we’ve also made the change to include the also (since it is a public library), would the linking be made automatically? 🤔
I think there’s a misunderstanding. I’ve implemented the functionality in the Now that we have an initial implementation, I’m starting to feel like the change might be too big for such an unlikely edge case |
What do you mean?
After looking at the implementation, indeed there is a misunderstanding. The implementation looks good.
Whats the big change? The implementation seems not so big. |
we have deployable libraries. calling
glad to hear
well it is the biggest change for this audit, it changes the logic of how a claim works. |
Got it. So to answer your question, when we deploy the contracts, foundry should automatically links the new libraries with the contract. |
I just realized something important about finding 12. We don't need to implement this abort function because .. When the campaign creator detects an ill-intentioned user for whom they want to cancel the airdrop .. They can just claim the airdrop stream NFT for them, and then cancel as usual. So I'm closing this issue. Sorry for the sunk cost @andreivladbrg @smol-ninja. |
that's right
no worries, the fix was very ugly, so this is even better we can achieve it without any other problems |
UPDATE: we don't need to implement this. See my comment below.
Address Cantina finding 12 by implementing an abort function in the Merkle abstract contract.
The function should take an array of recipient addresses as inputs, and it should allow the campaign creator to effectively cancel the airstreams for particular users.
The
_claim
function should be modified accordingly to check for cancelations.The text was updated successfully, but these errors were encountered: