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: 35/126
Findings: 4
Award: $0.02
🌟 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
The lockOnBehalf
function allows locking tokens on behalf of another player, but it lacks validation to ensure that msg.sender has permission to act on behalf of the player.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L275
File: src/managers/LockManager.sol#L275 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); }
Manual review
Implement proper authorization checks in the lockOnBehalf
function to ensure that msg.sender
has explicit permission to act on behalf of the player.
This can be achieved by incorporating an approval mechanism.
Access Control
#0 - c4-judge
2024-06-05T12:58:07Z
alex-ppg marked the issue as satisfactory
#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.014 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L258
Users can increase the unlockTime by using the setLockDuration function. Although there is validation to prevent reducing the lock duration, it can be bypassed due to the incorrect setting of unlockTime.
After one block, lastLockTime becomes less than block.timestamp. Users can call setLockDuration with playerSettings' duration minus 1, thereby bypassing the LockDurationReducedError.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L258
File: src/managers/LockManager.sol#L245 function setLockDuration(uint256 _duration) external notPaused { ... 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 // @audit block.timestamp > .lastLockTime ) { revert LockDurationReducedError(); } uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] .lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration); // @audit unlockTime should be block.timestamp + _duration } ... } File: test.t.sol function test_SpeedRun() public { uint256 lockAmount = 100e18; deployContracts(); amp.register(MunchablesCommonLib.Realm(3), address(0)); lm.lock{value: lockAmount}(address(0), lockAmount); ILockManager.LockedTokenWithMetadata memory m = lm.getLocked(address(this))[0]; assertEq(m.lockedToken.unlockTime, 86401); // unlock time is 1 day + 1 second vm.warp(block.timestamp + 1); for (uint i; i <= 1 days; i++) { lm.setLockDuration(1 days - i); } m = lm.getLocked(address(this))[0]; assertEq(m.lockedToken.unlockTime, block.timestamp - 1); // unlock time is block.timestamp - 1 ... lm.unlock(address(0x0), lockAmount); // unlock all tokens }
Manual review
It's recommended to update the unlockTime as follows :
File: src/managers/LockManager.sol#L245 function setLockDuration(uint256 _duration) external notPaused { ... if (lockedTokens[msg.sender][tokenContract].quantity > 0) { + uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] + .lastLockTime; // check they are not setting lock time before current unlocktime if ( - uint32(block.timestamp) + uint32(_duration) < + lastLockTime + 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); }
DoS
#0 - c4-judge
2024-06-04T12:41:29Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:53:09Z
alex-ppg marked the issue as satisfactory
🌟 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.0037 USDC - $0.00
Judge has assessed an item in Issue #550 as 2 risk. The relevant finding follows:
There are no checks to ensure that msg.sender has not already disapproved the price in the approveUSDPrice function. This means a feeder who has previously disapproved the same price can also approve it.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L177
File: src/managers/LockManager.sol#L177 function approveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.proposer == msg.sender) // @audit missed disapprovals checks revert ProposerCannotApproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); ... }
Manual review
It is advised to include validation to check if it has already been disapproved.
File: src/managers/LockManager.sol#L177 function approveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); + if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) + revert ProposalAlreadyDisapprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); ... }
#0 - c4-judge
2024-06-05T12:34:37Z
alex-ppg marked the issue as duplicate of #495
#1 - c4-judge
2024-06-05T12:34:41Z
alex-ppg marked the issue as partial-25
🌟 Selected for report: MrPotatoMagic
Also found by: 0xAkira, Bauchibred, Deivitto, Walter, bigtone, merlinboii
0 USDC - $0.00
This range covers timestamps up to around the year 2106.
File: src/interfaces/ILockManager.sol#L61 struct USDUpdateProposal { uint32 proposedDate; // @audit around year 2106? address proposer; address[] contracts; uint256 proposedPrice; mapping(address => uint32) approvals; mapping(address => uint32) disapprovals; uint8 approvalsCount; uint8 disapprovalsCount; } File: src/managers/LockManager.sol#L104 uint32(block.timestamp) File: src/managers/LockManager.sol#L166 usdUpdateProposal.proposedDate = uint32(block.timestamp); File: src/managers/LockManager.sol#L249 playerSettings[msg.sender].lockDuration = uint32(_duration); File: src/managers/LockManager.sol#L257 uint32(block.timestamp) + uint32(_duration) < File: src/managers/LockManager.sol#L267 uint32(_duration); File: src/managers/LockManager.sol#L353 lockdrop.start <= uint32(block.timestamp) && File: src/managers/LockManager.sol#L354 lockdrop.end >= uint32(block.timestamp) File: src/managers/LockManager.sol#L359 uint32(configStorage.getUint(StorageKey.MaxLockDuration)) File: src/managers/LockManager.sol#L381 lockedToken.lastLockTime = uint32(block.timestamp); File: src/managers/LockManager.sol#L383 uint32(block.timestamp) + File: src/managers/LockManager.sol#L384 uint32(_lockDuration); File: src/managers/LockManager.sol#L410 if (lockedToken.unlockTime > uint32(block.timestamp))
Manual review
It's recommended to use uint48 for timestamps.
APPROVE_THRESHOLD can be set as 1, so it's needed to check if approvalsCount is greater than APPROVE_THRESHOLD in the proposedUSDPrice function.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L171
File: src/managers/LockManager.sol#L171 function proposeUSDPrice( uint256 _price, address[] calldata _contracts ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { ... usdUpdateProposal.approvalsCount++; emit ProposedUSDPrice(msg.sender, _price); }
Manual review
It's recommended to add the validation for thresholds.
File: src/managers/LockManager.sol#L171 function proposeUSDPrice( uint256 _price, address[] calldata _contracts ) { ... usdUpdateProposal.approvalsCount++; + if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) { + _execUSDPriceUpdate(); + } emit ProposedUSDPrice(msg.sender, _price); }
The thresholds for _approve and _disapprove should be validated.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L135
File: src/managers/LockManager.sol#L135 function setUSDThresholds( uint8 _approve, uint8 _disapprove ) external onlyAdmin { if (usdUpdateProposal.proposer != address(0)) revert ProposalInProgressError(); APPROVE_THRESHOLD = _approve; //@audit should be >1 DISAPPROVE_THRESHOLD = _disapprove; // should be >=1? emit USDThresholdUpdated(_approve, _disapprove); }
Manual review
It's recommended to add the validation for thresholds.
File: src/managers/LockManager.sol#L135 function setUSDThresholds( uint8 _approve, uint8 _disapprove ) external onlyAdmin { if (usdUpdateProposal.proposer != address(0)) revert ProposalInProgressError(); + require(_approve > 1); + require(_disapprove >= 1); APPROVE_THRESHOLD = _approve; DISAPPROVE_THRESHOLD = _disapprove; emit USDThresholdUpdated(_approve, _disapprove); }
There are no checks to ensure that msg.sender has not already disapproved the price in the approveUSDPrice function. This means a feeder who has previously disapproved the same price can also approve it.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L177
File: src/managers/LockManager.sol#L177 function approveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.proposer == msg.sender) // @audit missed disapprovals checks revert ProposerCannotApproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); ... }
Manual review
It is advised to include validation to check if it has already been disapproved.
File: src/managers/LockManager.sol#L177 function approveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); + if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) + revert ProposalAlreadyDisapprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); ... }
There are no checks to ensure that msg.sender is not the proposer in the disapproveUSDPrice function. This means the proposer can also disapprove the proposal.
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L225
File: src/managers/LockManager.sol#L210 function disapproveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) // @audit missed proposer is msg.sender? revert ProposalAlreadyApprovedError(); if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); ... }
Manual review
It is recommended to add validation to ensure that the caller is the proposer.
File: src/managers/LockManager.sol#L177 function disapproveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); + if (usdUpdateProposal.proposer == msg.sender) + revert ProposerCannotDisapproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); ... }
File: src/managers/LockManager.sol#L171 159: if (_contracts.length == 0) revert ProposalInvalidContractsError(); + if (_price == 0) revert USDPriceInvalidError();
Index event fields make the field more quickly accessible to off-chain tools that parse events. This is especially useful when it comes to filtering based on an address. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Where applicable, each event
should use three indexed
fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three applicable fields, all of the applicable fields should be indexed.
File: src/interfaces/ILockManager.sol 159: event TokenConfigured(address _tokenContract, ConfiguredToken _tokenData); 168: event ProposedUSDPrice(address _proposer, uint256 _price); 172: event ApprovedUSDPrice(address _approver); 176: event DisapprovedUSDPrice(address _disapprover); 225: event USDPriceUpdated(address _tokenContract, uint256 _newPrice);
Remove the unused error types.
File: src/interfaces/ILockManager.sol 228: error OnlyAccountManagerError(); 245: error USDPriceInvalidError(); 293: error InvalidTokenContractError();
Remove the unused event types.
File: src/interfaces/ILockManager.sol 220: event DiscountFactorUpdated(uint256 discountFactor);
File: src/interfaces/ILockManager.sol 93: receive() external payable { 94: revert LockManagerRefuseETHError(); 95: }
#0 - c4-judge
2024-06-05T12:37:51Z
alex-ppg marked the issue as grade-a