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: 62/126
Findings: 1
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.0056 USDC - $0.01
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#L275-L294 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L311-L398
Due to lack of validation of the _onBehalfOf
address in the LockManager.sol#lockOnBehalf()
function, an attacker can access arbitrary players. Therefore, by capturing the player's unlock()
function, the attacker can use the lockOnBehalf()
function to lock a small amount of ETH (1wei) and revert the player's unlock()
function.
In the LockManager.sol#lockOnBehalf()
function, the _onBehalfOf
address can be entered arbitrarily, so the attacker can directly access any player.
function lockOnBehalf( address _tokenContract, uint256 _quantity, address _onBehalfOf ) external payable notPaused onlyActiveToken(_tokenContract) onlyConfiguredToken(_tokenContract) nonReentrant { address tokenOwner = msg.sender; address lockRecipient = msg.sender; 289: if (_onBehalfOf != address(0)) { 290: lockRecipient = _onBehalfOf; 291: } 293: _lock(_tokenContract, _quantity, tokenOwner, lockRecipient); }
Additionally, the LockManager.sol#_lock()
function updates lockedToken.quantity
and then updates lockedToken.unlockTime
based on the current timestamp (block.timestamp
).
function _lock( address _tokenContract, uint256 _quantity, address _tokenOwner, address _lockRecipient ) private { ( address _mainAccount, MunchablesCommonLib.Player memory _player ) = accountManager.getPlayer(_lockRecipient); if (_mainAccount != _lockRecipient) revert SubAccountCannotLockError(); if (_player.registrationDate == 0) revert AccountNotRegisteredError(); // check approvals and value of tx matches if (_tokenContract == address(0)) { if (msg.value != _quantity) revert ETHValueIncorrectError(); } else { if (msg.value != 0) revert InvalidMessageValueError(); IERC20 token = IERC20(_tokenContract); uint256 allowance = token.allowance(_tokenOwner, address(this)); if (allowance < _quantity) revert InsufficientAllowanceError(); } LockedToken storage lockedToken = lockedTokens[_lockRecipient][ _tokenContract ]; ... ... lockedToken.remainder = remainder; 380: lockedToken.quantity += _quantity; 381: lockedToken.lastLockTime = uint32(block.timestamp); 382: lockedToken.unlockTime = 383: uint32(block.timestamp) + 384: uint32(_lockDuration); // set their lock duration in playerSettings 387: playerSettings[_lockRecipient].lockDuration = _lockDuration; ... ... }
Meanwhile, the LockManager.sol#unlock()
function checks whether the asset can be unlocked.
function unlock( address _tokenContract, uint256 _quantity ) external notPaused nonReentrant { LockedToken storage lockedToken = lockedTokens[msg.sender][ _tokenContract ]; if (lockedToken.quantity < _quantity) revert InsufficientLockAmountError(); 410: if (lockedToken.unlockTime > uint32(block.timestamp)) 411: revert TokenStillLockedError(); ... ... }
Therefore, when the attacker catches the player's LockManager.sol#unlock()
function in mempool
, he first executes the lockOnBehalf()
function with 1wei's assets, thereby extending the player's lockedToken.unlockTime
,
Thereby the attacker can revert the unlock()
function and then the player will not be able to unlock his assets.
Manual Review
It is recommended to add a validation function for the _onBehalfOf
address of the LockManager.sol#lockOnBehalf()
function.
Invalid Validation
#0 - c4-judge
2024-06-05T12:57:43Z
alex-ppg marked the issue as satisfactory
#1 - c4-judge
2024-06-05T13:00:01Z
alex-ppg changed the severity to 3 (High Risk)