Adversary can prevent updating price feed addresses by creating poisonous proposals ending in `_confirm`
c4-bot-9 opened this issue · 16 comments
Lines of code
Vulnerability details
Impact
An adversary can prevent the creation of setContract
and websiteUpdate
proposals by creating a poisonous proposal with the same ballot name + _confirm
. This attack can be performed repeatedly every few days (when the voting period ends).
Affected proposals:
setContract:priceFeed1
->setContract:priceFeed1_confirm
setContract:priceFeed2
->setContract:priceFeed2_confirm
setContract:priceFeed3
->setContract:priceFeed3_confirm
setContract:accessManager
->setContract:accessManager_confirm
setURL:{{newWebsiteURL}}
->setURL:{{newWebsiteURL}}_confirm
This is especially worrisome for the setContract
proposals, as it prevents changing the price feed contracts, which are used for USDS borrowing and liquidations.
Note: This Impact fits into the Attack Ideas: "Any issue that would prevent the DAO from functioning correctly."
Vulnerability Details
setContract
and websiteUpdate
proposals are executed in multiple steps.
Here's the process for changing the Price Feed 1, as an example:
- A new proposal is created with a ballot name
setContract:priceFeed1
- The proposal is voted, and when it wins, a confirmation proposal is created, appending
_confirm
to the ballot name, resulting insetContract:priceFeed1_confirm
. - When the confirmation proposal wins, it checks its ballot name and then it sets the new price feed.
The problem is that there is a check in _possiblyCreateProposal()
that can be exploited:
require( openBallotsByName[ string.concat(ballotName, "_confirm")] == 0, "Cannot create a proposal for a ballot with a secondary confirmation" );
This check is intended to prevent creating new proposals if there is a pending confirmation proposal being voted for the same change, but an adversary can use it to create a proposal with a ballot name setContract:priceFeed1_confirm
, by setting priceFeed1_confirm
as the contract name.
If someone tries to create a legit priceFeed1
proposal later, it will revert
, leading to a DOS.
This holds for all proposals mentioned in the Impact
section.
Proof of Concept
- Add the test to
src/dao/tests/Proposals.t.sol
- Run
COVERAGE="no" NETWORK="sep" forge test -vv --rpc-url https://1rpc.io/sepolia --mt "testProposeSetContractAddress_confirm"
function testProposeSetContractAddress_confirm() public {
uint256 initialNextBallotId = proposals.nextBallotID();
// Alice is the adversary
vm.startPrank(alice);
staking.stakeSALT(1000 ether);
// Create a poisonous proposal with a ballot name ending in `_confirm`
address newAddress = address(0x1111111111111111111111111111111111111112);
proposals.proposeSetContractAddress("priceFeed1_confirm", newAddress, "description");
assertEq(proposals.nextBallotID(), initialNextBallotId + 1); // proposal was created successfully
vm.stopPrank();
// Transfer some tokens to Bob who wants to create a legit proposal
vm.startPrank(DEPLOYER);
salt.transfer(bob, 1000 ether);
vm.stopPrank();
// Bob can't create a legit proposal to change the contract address of `priceFeed1`
vm.startPrank(bob);
staking.stakeSALT(1000 ether);
vm.expectRevert("Cannot create a proposal for a ballot with a secondary confirmation");
proposals.proposeSetContractAddress("priceFeed1", newAddress, "description");
}
Recommended Mitigation Steps
Given the current implementation, the most straightforward fix would be to prevent the creation of proposals with ballot names ending in _confirm
for proposals that need a confirmation.
This would mean checking the contractName
in proposeSetContractAddress()
, and the newWebsiteURL
in proposeWebsiteUpdate()
.
But, as a recommendation, I would suggest refactoring createConfirmationProposal()
to pass a "confirmation" parameter to _possiblyCreateProposal()
, so that confirmation proposals don't rely on the ballot name.
Assessed type
Governance
Picodes marked the issue as primary issue
othernet-global (sponsor) confirmed
confirm_ is now prepended to automatic confirmation ballots form setWebsiteURL and setContract proposals.
Picodes marked the issue as satisfactory
Picodes changed the severity to QA (Quality Assurance)
This previously downgraded issue has been upgraded by Picodes
Picodes marked the issue as selected for report
Hi @Picodes, thanks for the judging so far.
I am just curious about why this issue is currently of Medium risk. Considering it completely prevents the DAO from setting new price feed and access manager contracts. I'd argue it should be high because of those reasons.
The stablecoin framework: /stablecoin, /price_feed, WBTC/WETH collateral, PriceAggregator, price feeds and USDS have been removed:
othernet-global/salty-io@88b7fd1
I don't think the severity of a finding depends on changes made after the contest. It depends on how it affects the codebase at the time of the contest.
@nonseodion isn't it clear from the definitions of severities under C4?
"2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals)."
Here assets are not at direct risk but a "function of the protocol or its availability could be impacted"
@J4X-98 sorry I am not sure to understand, the findings you are referring to are already flagged as duplicates?
@Pico, sorry that was a mistake on my side. I only saw that both were still open and missed the duplicate tag. Sry for the inconvenience.
No worries! Thanks for flagging in case it was a mistake