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: 103/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#L225-L228 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L194
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) revert ProposalAlreadyApprovedError(); >>> if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError(); if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); usdUpdateProposal.disapprovalsCount++; usdUpdateProposal.disapprovals[msg.sender] = _usdProposalId; emit DisapprovedUSDPrice(msg.sender); if (usdUpdateProposal.disapprovalsCount >= DISAPPROVE_THRESHOLD) { delete usdUpdateProposal; emit RemovedUSDProposal(); } }
The function above from the LockManager contract shows how price disapproval is implemented, it can be noted from the validation from the two pointers above that if a msg.sender or caller has previously approved or disapproved a USD price proposal, the the function will revert, the summary of this validation is simply to ensure a caller does not get two votes for the same Proposal i.e
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); }
On the other hand the function above from the same contract shows how price approval is implemented, it can be noted from the validation from the pointer above that if a msg.sender or caller has previously approved a USD price proposal, the the function will revert, the summary of this validation is also to ensure a caller does not get two votes for the same Proposal i.e Double Approval. The problem is this implementation is not complete as a caller can still get a double vote for an Approval and a Disapproval. This would not be possible by first doing Approval since Disapproval function is correctly implemented and would stop the second vote. But the protocol missed the fact that a caller can simply take the backward direction by first calling Disapproval function to get the first vote then calling Approval function to get the second vote without reversion.
Note: This issue might seem like it has no impact since the caller simply gets 1 votes of both approval and disapproval which means this votes cancel out each other and could be simply be interpreted that the caller didn't vote at all. But why this is a problem is that the vote count system is working with a threshold as noted at L135-L136, this means this irrelevant votes affects the power that threshold have as bunch of useless votes end up filling the threshold affecting actual votes counts that hold tangible value
lets assume there are 5 callers or voters and Approval threshold = 3 Disapproval threshold = 2 current Approval count = 0 current Disapproval count = 0 lets assume 2 of the callers made this useless disapproval and approval vote current Approval count = 2 current Disapproval count = 2 Disenfranchised Voters = 3
Manual Review
As adjusted below the approval of USDPrice should not only check to to see if caller has previously approved Price but also check to ensure that caller has not previously disapprove Price before proceeding with Approval
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(); usdUpdateProposal.approvals[msg.sender] = _usdProposalId; usdUpdateProposal.approvalsCount++; if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) { _execUSDPriceUpdate(); } emit ApprovedUSDPrice(msg.sender); }
Access Control
#0 - c4-judge
2024-06-05T12:42:24Z
alex-ppg marked the issue as satisfactory
#1 - c4-judge
2024-06-05T12:43:23Z
alex-ppg changed the severity to 2 (Med Risk)