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: 24/126
Findings: 1
Award: $28.80
🌟 Selected for report: 0
🚀 Solo Findings: 0
28.7985 USDC - $28.80
When minLockDuration<lockdrop.end - lockdrop.start, it means that users can unlock funds during the lockdrop period and then relock them. Resulting in duplicate calculation of NFT for the same funds github:https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L352
if ( lockdrop.start <= uint32(block.timestamp) && lockdrop.end >= uint32(block.timestamp) ) { if ( _lockDuration < lockdrop.minLockDuration || _lockDuration > uint32(configStorage.getUint(StorageKey.MaxLockDuration)) ) revert InvalidLockDurationError(); if (msg.sender != address(migrationManager)) { // calculate number of nfts remainder = quantity % configuredToken.nftCost; numberNFTs = (quantity - remainder) / configuredToken.nftCost; if (numberNFTs > type(uint16).max) revert TooManyNFTsError(); // Tell nftOverlord that the player has new unopened Munchables nftOverlord.addReveal(_lockRecipient, uint16(numberNFTs)); } }
Assuming lockdrop.start is 100, Lockdrop. end is 200, MinLockDuration is 10, ConfiguredToken. nftCost is 100, and User1 holds 99 tokens
Manual Audit
if ( lockdrop.start <= uint32(block.timestamp) && lockdrop.end >= uint32(block.timestamp) && + quantity + lockedToken.quantity > totaluserused ) { if ( _lockDuration < lockdrop.minLockDuration || _lockDuration > uint32(configStorage.getUint(StorageKey.MaxLockDuration)) ) revert InvalidLockDurationError(); if (msg.sender != address(migrationManager)) { // calculate number of nfts remainder = quantity % configuredToken.nftCost; numberNFTs = (quantity - remainder) / configuredToken.nftCost; if (numberNFTs > type(uint16).max) revert TooManyNFTsError(); // Tell nftOverlord that the player has new unopened Munchables nftOverlord.addReveal(_lockRecipient, uint16(numberNFTs)); + totaluserused += quantity - remainder; } }
function unlock( address _tokenContract, uint256 _quantity ) external notPaused nonReentrant { LockedToken storage lockedToken = lockedTokens[msg.sender][ _tokenContract ]; if (lockedToken.quantity < _quantity) revert InsufficientLockAmountError(); if (lockedToken.unlockTime > uint32(block.timestamp)) revert TokenStillLockedError(); // force harvest to make sure that they get the schnibbles that they are entitled to accountManager.forceHarvest(msg.sender); lockedToken.quantity -= _quantity; // send token if (_tokenContract == address(0)) { payable(msg.sender).transfer(_quantity); } else { IERC20 token = IERC20(_tokenContract); token.transfer(msg.sender, _quantity); } + if(lockedToken.quantity < lockedToken.remainder){ + lockedToken.remander = lockedToken.quantity; + } emit Unlocked(msg.sender, _tokenContract, _quantity); }
Other
#0 - michaeljyeates
2024-05-30T22:54:39Z
We understand this, we will set min lock duration to the same as the lockdrop time so that people cannot do this
#1 - michaeljyeates
2024-05-30T23:26:25Z
remainder is now set to 0 on unlock
#2 - 0xinsanity
2024-05-30T23:29:21Z
remainder reset to 0 on unlock fix: https://github.com/munchablesorg/munchables-common-core-latest/pull/15
#3 - c4-judge
2024-06-05T13:04:21Z
alex-ppg marked the issue as satisfactory