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: 117/126
Findings: 1
Award: $0.01
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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-L207
Price feeds that vote against a proposal can still approve it and can influence the end result of the proposal effectively gaining an extra vote.
When a USD price proposal is made price feeds can effectively have 2 votes depending on whether they approve or disapprove first. If they first approve a proposal, they don't get to vote for the same proposal again no matter for or against it. If a price feed disapproves first, however, it can still call approveUSDPrice()
and affect the outcome of a proposal.
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.proposedPrice != _price) revert ProposalPriceNotMatchedError(); usdUpdateProposal.approvals[msg.sender] = _usdProposalId; usdUpdateProposal.approvalsCount++; if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) { _execUSDPriceUpdate(); } emit ApprovedUSDPrice(msg.sender); }
We can see that the function checks for the following things:
msg.sender
is not the proposer themselfmsg.sender
has not approved alreadyIt never checks, however, that the price feed has already disapproved the proposed price, which effectively gives them a second vote they can cast.
To give an illustrative example, suppose we have a threshold of 3 (as initially defined in the LockManager contract). At proposal creation, the proposer's vote's counted towards the approvals already, so we are starting off with a 1-0 ratio.
Price Feed # voting | Approve | Disapprove |
---|---|---|
(proposal creation) 1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 2 | 2 |
From then on, the 2nd price feed also approves, the 3rd one disapproves and the 4th one disapproves as well. Logically, the 5th price feed should have the final word, but in reality the 3rd and 4th price feeds still have power to approve the proposal and tilt the outcome in the opposite direction.
Even if the price feeds are trusted, this is still not how proposals are supposed to develop and each party involved should have the right to cast their vote exactly one time.
Manual review
Add the following check to approveUSDPrice()
:
if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError();
Invalid Validation
#0 - michaeljyeates
2024-05-30T22:51:10Z
Oracles acting badly can be removed
#1 - c4-judge
2024-06-05T12:42:52Z
alex-ppg marked the issue as satisfactory