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: 45/126
Findings: 2
Award: $0.02
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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-L272
Users can reduce their lockup times, which is not expected by Munchables. The impacts are:
In function setLockDuration
, before a new lock duration is set, the resulted new unlock time is requried not to be earlier than current unlock time (L257). This is guaranteed by reverting the tx if uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime
(L256-L261). If that condition is met, the new unlock time is then updated. However, the new unlock time is set as lastLockTime + uint32(_duration)
(L265-L267). The updating is wrong, as the resulted unlock time could be earlier than current unlock time.
The reason for the problem is the time used for the new unlock time checking is block.timestamp
, while the time used for updating new unlock time is lastLockTime
, and block.timestamp >= lastLockTime
always holds.
Let's look at an example:
lastLockTime = day 0, duration = 30 days, unlockTime = day 30
day 10
(i.e. block.timestamp = day 10
), and the new duration is 15 days (i.e. _duration = 25 days
). The checking in L256-L261 passes as blocl.timestamp + _duration = day 10 + 25 days = day 35 > currentUnlockTime = day 30
. Then the unlock time will be updated with lastLockTime + _duration = day 0 + 25 days = day 25 < currentUnlockTime = day 30
. The unlock time is reduced.245: function setLockDuration(uint256 _duration) external notPaused { 246: if (_duration > configStorage.getUint(StorageKey.MaxLockDuration)) 247: revert MaximumLockDurationError(); 248: 249: playerSettings[msg.sender].lockDuration = uint32(_duration); 250: // update any existing lock 251: uint256 configuredTokensLength = configuredTokenContracts.length; 252: for (uint256 i; i < configuredTokensLength; i++) { 253: address tokenContract = configuredTokenContracts[i]; 254: if (lockedTokens[msg.sender][tokenContract].quantity > 0) { 255: // check they are not setting lock time before current unlocktime 256: if ( 257:@> uint32(block.timestamp) + uint32(_duration) < 258: lockedTokens[msg.sender][tokenContract].unlockTime 259: ) { 260: revert LockDurationReducedError(); 261: } 262: 263: uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] 264: .lastLockTime; 265:@> lockedTokens[msg.sender][tokenContract].unlockTime = 266: lastLockTime + 267: uint32(_duration); 268: } 269: } 270: 271: emit LockDuration(msg.sender, _duration); 272: }
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L245-L272
VS Code
Update the new unlock time with uint32(block.timestamp) + uint32(_duration)
rather than lastLockTime + uint32(_duration)
.
Timing
#0 - c4-judge
2024-06-04T12:41:00Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:53:32Z
alex-ppg marked the issue as partial-75
🌟 Selected for report: robertodf99
Also found by: 0xAadi, 0xAkira, 0xdice91, 0xhacksmithh, 0xleadwizard, AgileJune, Bauchibred, Bbash, Beosin, Bigsam, Dots, EPSec, EaglesSecurity, Eeyore, Evo, John_Femi, Mahmud, MrPotatoMagic, RotiTelur, Rushkov_Boyan, Sabit, Sentryx, Stormreckson, Topmark, Tychai0s, Utsav, Walter, ZanyBonzy, ZdravkoHr, adam-idarrha, araj, aslanbek, avoloder, bigtone, brevis, brgltd, carrotsmuggler, crypticdefense, dd0x7e8, dhank, djanerch, falconhoof, iamandreiski, joaovwfreire, leegh, merlinboii, mitko1111, pamprikrumplikas, pfapostol, prapandey031, swizz, trachev, twcctop, typicalHuman, unique, xyz
0.0148 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L177-L200
An oracle can approve an USD price even if it has disapproved the price already.
When an oracle approves an USD price, function approveUSDPrice
only check if the oracle has approved the price already, and reverts the tx if it has. But the function does not check if the oracle has disapproved the price already. As a result, the oracle can first disapprove the price, and then approve the price again. This may mess up the pricing of tokens.
177: function approveUSDPrice( 178: uint256 _price 179: ) 180: external 181: onlyOneOfRoles( 182: [ 183: Role.PriceFeed_1, 184: Role.PriceFeed_2, 185: Role.PriceFeed_3, 186: Role.PriceFeed_4, 187: Role.PriceFeed_5 188: ] 189: ) 190: { 191: if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); 192: if (usdUpdateProposal.proposer == msg.sender) 193: revert ProposerCannotApproveError(); 194:@> if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) 195: revert ProposalAlreadyApprovedError(); 196: if (usdUpdateProposal.proposedPrice != _price) 197: revert ProposalPriceNotMatchedError(); 198: 199: usdUpdateProposal.approvals[msg.sender] = _usdProposalId; 200: usdUpdateProposal.approvalsCount++;
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L177-L200
VS Code
In function approveUSDPrice
, check if the oracle has disapproved the USD price, and revert the tx if it has.
function approveUSDPrice( uint256 _price ) ... { ... if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); + if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) + revert ProposalAlreadyDisapprovedError();
Other
#0 - c4-judge
2024-06-05T12:42:34Z
alex-ppg marked the issue as satisfactory