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: 47/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
Function setLockDuration(uint256 _duration)
uses block.timestamp
instead of lockedTokens[msg.sender][tokenContract].lastLockTime
to validate the new duration doesn't make the lock to be unlocked earlier than originally set. This makes possible for a user to reduce their lockTime even when they have locked tokens.
In LockManager.sol:256
, the contract uses block.timestamp
instead of lastLockTime
to check if the new lock duration extends the current unlock time. This allows users to reduce their lock time even when they have locked tokens, which is likely unintended behavior.
// Setup in gist https://gist.github.com/Tychaios/e35d1716f8dc7739554fb19ab0ac60a1 function testUserCanReduceUnlockTimeBySettingNewDuration() external { address alice = vm.addr(0x1); fundEther(alice, 1000 ether); // Lock token so it shouldn't be possible to reduce lock duration vm.startPrank(alice); lockManager.lock{value: 1 ether}(address(0x0), 1 ether); uint256 balanceBeforeUnlocking = address(alice).balance; // Retrieve the locked token details for Alice for the token at address(0x0) (,,, uint256 unlockTimeBefore) = lockManager.lockedTokens(alice, address(0x0)); // notice newDuration is half the duration of the lock uint256 newDuration = 10 days; vm.warp(block.timestamp + 11 days); lockManager.setLockDuration(newDuration); (,,, uint256 unlockTimeAfter) = lockManager.lockedTokens(alice, address(0x0)); lockManager.unlock(address(0), 1 ether); uint256 balanceAfterUnlocking = address(alice).balance; assertGt(balanceAfterUnlocking, balanceBeforeUnlocking); assertLt(unlockTimeAfter, unlockTimeBefore); vm.stopPrank(); }
Manual Review
Instead of using block.timestamp
, the contract should use lastLockTime
when checking if the new lock duration extends the current unlock time. This will prevent users from reducing their lock time while tokens are locked.
if (lockedTokens[msg.sender][tokenContract].lastLockTime + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime) { revert LockDurationReducedError(); }
Invalid Validation
#0 - CloudEllie
2024-06-02T10:55:52Z
See sponsor comments on #189
#1 - c4-judge
2024-06-04T12:41:49Z
alex-ppg marked the issue as duplicate of #89
#2 - c4-judge
2024-06-05T12:54:00Z
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
The contract allows a PriceFeed to both approve and disapprove the same USD price proposal, leading to an inconsistent and invalid state where a single entity has conflicting actions on the same proposal.
In LockManager.sol:177
, the approveUSDPrice
function does not check if the msg.sender
has previously disapproved the proposal before allowing them to approve it. The contract already has a disapprovals
mapping to track disapprovals, but this mapping is not being checked in the approveUSDPrice
function. This means that a PriceFeed can potentially both approve and disapprove the same proposal, leading to an inconsistent state.
Manual Review
To prevent this inconsistency, the approveUSDPrice
function should check the disapprovals
mapping to ensure that the msg.sender
has not previously disapproved the proposal. If they have, the function should revert to maintain consistency.
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 (disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); usdUpdateProposal.approvals[msg.sender] = _usdProposalId; emit ProposalApproved(msg.sender, _price); // ... }
By checking the disapprovals
mapping, the function will ensure that a PriceFeed cannot both approve and disapprove the same proposal, maintaining consistency in the contract's state.
Governance
Governance
#0 - c4-judge
2024-06-05T12:42:38Z
alex-ppg marked the issue as satisfactory