Closed howlbot-integration[bot] closed 4 months ago
alex-ppg marked the issue as not a duplicate
The submission specifies that locks do not expire beyond their lock time and that they are still eligible for daily rewards.
Based on my evaluation of the codebase, this appears to be desirable behavior as the daily rewards are immediately releasable (i.e. by simply relocking) and the actual time you acquire rewards for equates to the time you had them locked, regardless of whether they could be released or not. Longer lock times affect the "lockdrop" which is a separate rewarding mechanism that should not affect the "base" incentives of acquiring Schnibbles.
alex-ppg marked the issue as unsatisfactory: Invalid
Hi @alex-ppg thanks a lot for judging.
Expanding on your comment above there are a few things that should be taken into consideration as well:
I believe this should be valid, as without having awards based on only lock time, this defeats the whole purpose of having a locking manager as none of the incentives are based on lock time.
Hey @iamandreiski, thanks for your feedback. You state that it is not economically viable to distribute rewards to users who can immediately withdraw their rewards, however, this is precisely the model of Synthetix, Sushiswap, and other well-known protocols. Effectively, the LockManager
acts as a rudimentary staking system when no lockdrop is active and as a locking system whenever a lockdrop is active. Being able to configure the time you wish to lock your tokens for is imperative in case supplemental infrastructure is desired by the Munchables team (i.e. a contract that locks your token for 1 year and provides a boost on your Schnibble rewards).
I do not see any security vulnerability arising from the present behavior, and while we can criticize its structure it does not mean that such criticism would be a high risk or medium risk flaw.
Lines of code
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L461-L487 https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L349-L371 https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L410-L411
Vulnerability details
Impact
Users whose lock has expired but they're still holding/keeping the tokens in the LockManager will receive the same benefits as if their tokens are locked.
This opens up the possibility for malicious users to take advantage of this mechanics and game the system by locking their tokens for the minimum amount of time, while collecting all of the rewards and being able to unlock/withdraw their tokens at any time.
Proof of Concept
There are two ways in which users are rewarded for locking their tokens inside the LockManager:
getLockedWeightedValue()
function, and the the time betweenblock.timestamp
and the last harvest of said Schnibbles (either the user manually harvesting it by calling the functionharvest()
on the accounting manager, or automatically doing so when locking/unlocking tokens byforceHarvest()
being called).If there is a lockdrop event, the amount of NFTs that a user will receive is calculated immediately when the lock is made and
addReveal()
on the NFT Overlord is called.During or outside of lock events, users can stake so that they accrue daily Schnibbles, which is another form of reward. As explained above this reward is dependent on the
getLockedWeightedValue()
and the time from the last harvest of said Schnibbles.If we take a look at the
getLockedWeightedValue()
:If the token in-question is active, the weighted value is based solely on the quantity of tokens that the user has in the Lock Manager multiplied by the price of that token.
Let's examine the following scenario:
A user has set their
lockDuration
as the minimum one (for example 10 hours), and if there's no lockdrop event currently this can be set to as little as 1 second, as there are no minimum duration checks outside of the lockdrop event block in_lock()
.Once
_lock()
is called, it continues to callforceHarvest()
which will harvest any accumulated Schnibbles on behalf of the user and update thelastHarvestDate
on the Accounting Manager, upon which the harvesting of Schnibbles is also based.Since none of the two mechanisms is based upon the actual lock period, this makes it obsolete. The only thing which can be prevented by the
lastHarvestDate
is depositing and withdrawing in the same block, asforceHarvest()
will be called on both lock and unlock, making this 0, subsequently the schnibbles to claim as well.Taking this into consideration, there's no mechanism preventing users, to lock their tokens for 1 second (if no lockdrop period is active), and keeping them in the contract to accrue rewards, allowing them to withdraw/unlock them at any time they want and re-lock them again whenever they want for 1 second, continuing to accrue more Schnibbles.
If
unlockTime
passes the current block.timestamp and tokens can be unlocked, there's no mechanism which makes the rewards to stop accruing or forces the user to unlock them.A coded PoC which can be used in the already existing SpeedRun.t.sol test suite:
Results:
Tools Used
Manual Review
Recommended Mitigation Steps
Re-factor the
getLockedWeightedValue
to only accrue value if the unlock time of the user-in-question is less than the block.timestamp (i.e. meaning that the stake is still active).Assessed type
Invalid Validation