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: 56/126
Findings: 2
Award: $0.01
🌟 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.0042 USDC - $0.00
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L278
An attacker can prevent an user from unlocking just by locking a small amount (or zero) on behalf of the user to update its unlockTime
. If the attacke does so just before the user is going to unlock, its unlockTime
updates to a larger value, so the user should wait again.
The attacker calls the function lockOnBehalf
by providing just small (or zero) quantity of the token that the user has already locked on.
function lockOnBehalf( address _tokenContract, uint256 _quantity, address _onBehalfOf ) external payable notPaused onlyActiveToken(_tokenContract) onlyConfiguredToken(_tokenContract) nonReentrant { address tokenOwner = msg.sender; address lockRecipient = msg.sender; if (_onBehalfOf != address(0)) { lockRecipient = _onBehalfOf; } _lock(_tokenContract, _quantity, tokenOwner, lockRecipient); }
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L278
By doing so, the attacker does not transfer anything, just pays gas fee, but it will update the user's lastLockTime
, unlockTime
, and lockDuration
.
lockedToken.remainder = remainder; lockedToken.quantity += _quantity; lockedToken.lastLockTime = uint32(block.timestamp); lockedToken.unlockTime = uint32(block.timestamp) + uint32(_lockDuration); // set their lock duration in playerSettings playerSettings[_lockRecipient].lockDuration = _lockDuration;
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L379-L387
So, for example, suppose the user's token unlockTime
is passed, and now he is going to unlock it. The attacker locks on behalf of the user small (or zero) quantity. By doing so, the user's token unlockTime
is delayed by _lockDuration
(which is at least equal to minLockDuration
). Thus, the user should wait for another _lockDuration
to be able to unlock it.
Moreover, if the user has set his lockDuration
to zero. By applying this attack, the attacker can update the user's lockDuration
to minLockDuration
.
It should be enforced that the quantity
be larger than a minimum amount. So, the attacker must pay the minimum amount to apply this attack. Thus, this attack will be more expensive for the attacker.
struct ConfiguredToken { uint256 usdPrice; uint256 nftCost; uint8 decimals; bool active; uint256 minimum; } function lockOnBehalf( address _tokenContract, uint256 _quantity, address _onBehalfOf ) external payable notPaused onlyActiveToken(_tokenContract) onlyConfiguredToken(_tokenContract) nonReentrant { address tokenOwner = msg.sender; address lockRecipient = msg.sender; if (_onBehalfOf != address(0)) { lockRecipient = _onBehalfOf; require(_quantity >= configuredTokens[_tokenContract].minimum, "quantity is smaller than the minimum."); } _lock(_tokenContract, _quantity, tokenOwner, lockRecipient); }
Context
#0 - c4-judge
2024-06-05T12:58:16Z
alex-ppg marked the issue as partial-75
#1 - c4-judge
2024-06-05T13:00:02Z
alex-ppg changed the severity to 3 (High Risk)
🌟 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#L245
A malicious user can reduce the unlockTime
maliciously, due to the wrong implementation of the function setLockDuration
.
Suppose after locking, we have the following timing. Where, unlockTime = lastLockTime + _lockDuration
.
lastLockTime
= 86_400 = day 1_lockDuration
= 7 * 24 * 60 * 60 = 604_800 = 7 daysunlockTime
= 86_400 + 604_800 = 691_200 = day 8--------------|--------------------------------------------------|-------------> lastLockTime unlockTime
function _lock( address _tokenContract, uint256 _quantity, address _tokenOwner, address _lockRecipient ) private { //..... uint32 _lockDuration = playerSettings[_lockRecipient].lockDuration; //..... lockedToken.lastLockTime = uint32(block.timestamp); lockedToken.unlockTime = uint32(block.timestamp) + uint32(_lockDuration); //..... }
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L382
The malicious user (lockRecipient
) can reduce the unlock time by calling the function setLockDuration
. In this function, the new unlockTime
is incorrectly calculated as lastLockTime + uint32(_duration)
.
function setLockDuration(uint256 _duration) external notPaused { if (_duration > configStorage.getUint(StorageKey.MaxLockDuration)) revert MaximumLockDurationError(); playerSettings[msg.sender].lockDuration = uint32(_duration); // update any existing lock uint256 configuredTokensLength = configuredTokenContracts.length; for (uint256 i; i < configuredTokensLength; i++) { address tokenContract = configuredTokenContracts[i]; if (lockedTokens[msg.sender][tokenContract].quantity > 0) { // check they are not setting lock time before current unlocktime if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); } uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] .lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration); } } emit LockDuration(msg.sender, _duration); }
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L245
Suppose the malicious user at day 3 (block.timestamp = 86_400 * 3
) calls this function with _duration = 86_400 * 5 = 5 days
as a parameter. By doing so, the following condition is passed, because day 3 + 5 days = day 8 = unlockTime
.
if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); }
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L256
But, the lockedTokens[msg.sender][tokenContract].unlockTime
is set to lastLockTime + uint32(_duration) = day 1 + 5 days = day 6
. While the unlockTime
was scheduled at day 8 before. So, it is reduced by two days.
day 1 day 3 day 6 day 8 --------------|--------------|-----------------|------------------|-------------> t lastLockTime now newUnlockTime unlockTime
If the attacker repeats this many times, the unlockTime
will reduce further, even less than minLockDuration
or even equal to lastLockTime
. For example, the attacker can apply this attack by calling the function reduceUnlockTime
in the following complete example:
// SPDX-License-Identifier: MIT pragma solidity 0.8.22; contract Test { error LockDurationReducedError(); struct LockedToken { uint256 quantity; uint256 remainder; uint32 lastLockTime; uint32 unlockTime; } mapping(address => mapping(address => LockedToken)) public lockedTokens; // simple lock function function lock() public { LockedToken memory lockedToken = LockedToken({ quantity: 0, remainder: 0, lastLockTime: uint32(block.timestamp), unlockTime: uint32(block.timestamp) + 8 days }); lockedTokens[msg.sender][address(1)] = lockedToken; } // simple set lock duration function function setLockDuration(uint256 _duration) public { if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][address(1)].unlockTime ) { revert LockDurationReducedError(); } uint32 lastLockTime = lockedTokens[msg.sender][address(1)].lastLockTime; lockedTokens[msg.sender][address(1)].unlockTime = lastLockTime + uint32(_duration); } // if the attacker calls this function, it will reduce the unlockTime maliciously function reduceUnlockTime() public { uint256 duration; while ( lockedTokens[msg.sender][address(1)].unlockTime != lockedTokens[msg.sender][address(1)].lastLockTime ) { if ( lockedTokens[msg.sender][address(1)].unlockTime > block.timestamp ) { duration = lockedTokens[msg.sender][address(1)].unlockTime - block.timestamp; } else { duration = 0; } setLockDuration(duration); // this will set the unlockTime to lastLockTime + duration } } }
The code should be modified as:
function setLockDuration(uint256 _duration) external notPaused { //...... lockedTokens[msg.sender][tokenContract].unlockTime = uint32(block.timestamp) + uint32(_duration); //..... }
Context
#0 - c4-judge
2024-06-04T12:41:36Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:52:54Z
alex-ppg marked the issue as partial-75