Platform: Code4rena
Start Date: 22/05/2024
Pot Size: $20,000 USDC
Total HM: 6
Participants: 126
Period: 5 days
Judge: 0xsomeone
Total Solo HM: 1
Id: 379
League: ETH
Rank: 16/126
Findings: 3
Award: $28.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Circolors
Also found by: 0rpse, 0x175, 0xAadi, 0xHash, 0xMax1mus, 0xMosh, 0xblack_bird, 0xdice91, 0xfox, 0xhacksmithh, 0xloscar01, 0xrex, 4rdiii, Audinarey, AvantGard, Bigsam, DPS, Dots, Drynooo, Dudex_2004, Evo, Kaysoft, King_, Limbooo, MrPotatoMagic, PENGUN, Sabit, SovaSlava, SpicyMeatball, TheFabled, Utsav, Varun_05, Walter, adam-idarrha, araj, aslanbek, ayden, bctester, biakia, bigtone, brgltd, carrotsmuggler, cats, crypticdefense, dd0x7e8, dhank, fandonov, fyamf, grearlake, iamandreiski, ilchovski, jasonxiale, joaovwfreire, lanrebayode77, m4ttm, merlinboii, niser93, nnez, octeezy, oxchsyston, pamprikrumplikas, rouhsamad, tedox, trachev, turvy_fuzz, twcctop, yotov721, zhaojohnson
0.0056 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L275-L294 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L381-L387
Griefing Attack:
Lack of access control in lockOnBehalf
function allows
malicious users to lock the tokens of other players by repeatedly extending the unlockTime
.
Impact: High Likelihood : High
Contract : LockManager
As it stands
lockOnBehalf
function allows any msg.sender
to lock tokens on behalf of another address _onBehalfOf
,
which is the root cause of this attack.
The following example demonstrates the attack on a single user, executed one time. But note that, this attack could be performed repeatedly on multiple player accounts.
Bob : Player/ victim Alice : Attacker
Step 1:
Bob completes the registration and locks 5 WETH on May 1, 2024 ( block.timestamp = 1714521600)
vm.startPrank(Bob); lockManagerContract.lock(address(weth), 5 ether); vm.stopPrank();
Going by the minLockDuration
of 30 days
lockedToken.unlockTime
for weth is set to May 30, 2024 (block.timestamp = 1717027200)
playerSettings[_lockRecipient].lockDuration
is set to minLockDuration
= 30 days
Step 2:
Alice wants to keep Bob's funds locked in the contract to execute a griefing attack or gain any potential advantage in game.
On May 29, 2024 (block.timestamp = 1716940800)
Alice calls the lockOnBehalf
function with dust amount of weth
passing Bob's player account as the lockRecipient
vm.startPrank(Alice); lockManagerContract.lockOnBehalf( address(weth), 1 wei, address(Bob)) vm.stopPrank();
Since playerSettings[_lockRecipient].lockDuration
was set to 30 days.
The following line extends Bob's unlockTime till June 28, 2024.
lockedToken.unlockTime = uint32(block.timestamp) + uint32(_lockDuration);
Step 3:
Bob attempts to withdraw his weth funds on May 31st
vm.startPrank(Bob); vm.expectRevert(); lockManagerContract.unlock(address(weth), 5 ether); vm.stopPrank();
and gets reverted with TokenStillLockedError
due to the following check
if (lockedToken.unlockTime > uint32(block.timestamp)) revert TokenStillLockedError();
Any user is able to execute this attack repeatedly, keeping other players' funds locked indefintely.
Manual Review, Foundry
To prevent this,
lockOnBehalf
should require explicit consent from the lockRecipient
.
One way to implement this is by introducing an approval mechanism where users must approve another address to lock tokens on their behalf.
mapping(address => mapping(address => bool)) public lockOnBehalfApproval;
function approveLockOnBehalf(address _approved) external { lockOnBehalfApproval[msg.sender][_approved] = true; emit ApprovalForLockOnBehalf(msg.sender, _spender); }
lockOnBehalf
to check if the msg.sender
is approvedWith these changes, users can control who is allowed to lock tokens on their behalf, mitigating the risk of unauthorized extensions of the lock period.
DoS
#0 - c4-judge
2024-06-05T12:58:50Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: SpicyMeatball
Also found by: 0rpse, 0xMosh, 0xblack_bird, 0xdice91, 0xhacksmithh, 0xleadwizard, 0xmystery, Audinarey, AvantGard, Bigsam, Dots, EPSec, Eeyore, Janio, Limbooo, LinKenji, Mahmud, MrPotatoMagic, Myd, Oxsadeeq, Sabit, SovaSlava, Stefanov, Tychai0s, Utsav, Varun_05, Walter, adam-idarrha, ahmedaghadi, araj, aslanbek, ayden, bigtone, c0pp3rscr3w3r, carrotsmuggler, crypticdefense, dhank, fyamf, gajiknownnothing, gavfu, itsabinashb, jasonxiale, joaovwfreire, ke1caM, leegh, merlinboii, mitko1111, n4nika, pfapostol, prapandey031, rouhsamad, sandy, snakeeaterr, stakog, steadyman, swizz, tedox, th3l1ghtd3m0n, trachev, turvy_fuzz, xyz, yashgoel72, zhaojohnson
0.0105 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L263-L267
Likelihood : High Impact : High
The following is one of the main invariants mentioned in the protocol documentation.
people cannot reduce lockup times that are previously set
However, any user is able to exploit the following math error in setLockDuration
function
to unlock locked funds earlier than intended by the protocol.
lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration);
This breaks the above mentioned invariant.
Contract : lockManager
This vulnerability stems from the way unlockTime
is calculated in the setLockDuration
lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration);
The above calculation of unlockTime
takes into account
the lastLockTime
instead of using the block.timestamp
value,
which is the root cause of this issue.
Lets demonstrate with an example
Step 1:
Bob locks 5 Ether of WETH tokens for the first time on 12 am, May 1, 2024 ( block.timestamp = 1714521600)
vm.startPrank(Bob); lockManagerContract.lock ( address(weth), 5 ether )
_lockDuration
is assigned a minLockDuration
of 30 days.
such that
lastLockTime
= 12 am, May 1, 2024 ( block.timestamp = 1714521600)
unlockTime
= 12 am, May 30, 2024 (block.timestamp = 1717027200)
Step 2:
Bob waits till 1 am, May 16, 2024 ( block.timestamp = 1715821200)
Step 3 :
Bob calls setLockDuration
at 1 am, May 16th 2024
with a _duration
= 15 days,
inorder to avoid the following revert
if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); }
The unlockTime
is calculated as
uint32 lastLockTime = lockedTokens[msg.sender][tokenContract].lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration);
1714521600 + 15 days = 1715817600 (12 am , May 16, 2024 )
which is clearly in the past.
Step 4 :
Bob immediately calls unlock
and withdraws the WETH tokens
much earlier than the minimum lock duration intended by the protocol
vm.startPrank(Bob); lockManagerContract.unlock(address(weth), 5 ether);
Manual Review, Foundry
To prevent this exploit,
the new unlockTime
should be calculated as
the sum of current block.timestamp
and _duration
block.timestamp + uint32(_duration)
if (lockedTokens[msg.sender][tokenContract].quantity > 0) { // Ensure new unlock time is in the future and greater than current unlock time uint32 newUnlockTime = uint32(block.timestamp) + uint32(_duration); lockedTokens[msg.sender][tokenContract].unlockTime = newUnlockTime; if (newUnlockTime < lockedTokens[msg.sender][tokenContract].unlockTime) { revert LockDurationReducedError(); } }
As an additional measure,
check should be made after the update
to ensure that the updated unlockTime
remains larger than the previous unlockTime
Math
#0 - c4-judge
2024-06-04T12:41:42Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:52:43Z
alex-ppg marked the issue as partial-75
28.7985 USDC - $28.80
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L401-L427 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L352-L384
Likelihood : High Impact : High
The unlock
function is currently missing the logic to
adjust lockedToken.remainder
correctly when unlocking tokens in some edge cases.
This results in an unfair benefit where an user gains an additional NFT without locking the required amount.
Consider
Step 1
Bob locks 1900 USD, gets 1 NFT. lockedToken.remainder = 900 USDB; lockedToken.quantity = 1900 USDB;
Step 2
Bob Waits 5 days (minimum lockup duration)
Step 3
Bob calls `unlock` with _quantity of 1900 USDB. remainder is still at 900 USDB Adjust the locked quantity: lockedToken.quantity -= _quantity; // lockedToken.quantity -= 1900 = 0 However note that, `lockedToken.remainder` is not adjusted and remains 900
Step 4
Bob calls `lock` again with `_quantity` of 100 USDB. `quantity` = 100 + 900 (previous remainder) = 1000 USDB remainder = 0 numberNFTs = (quantity - remainder) / configuredToken.nftCost = (1000 - 0 ) / 1000 = 1 Thus Bob is able to acquire the subsequent NFT by locking only 100 USDB
Manual Review, Foundry
To prevent this issue,
the unlock function should be corrected
to adjust the lockedToken.remainder
appropriately when tokens are unlocked.
Math
#0 - c4-judge
2024-06-05T13:04:17Z
alex-ppg marked the issue as satisfactory
#1 - c4-judge
2024-06-05T13:04:46Z
alex-ppg changed the severity to 2 (Med Risk)