Asking a MetricQuestion on the FlatCFMRealityAdapter contract has a flaw when MetricQuestion is ask again after the first time for the purpose of confirmation or any other necessary reasons. The functions returns the call without refunding the caller since the call has previously gone through thereby locking caller's fund in contract
The function above from the FlatCFMRealityAdapter contract shows how metric question is asked typically during creation of conditional scalar market in the FlatCFMFactory contract. askMetricQuestion(...) function is a payable function and the msg.value is used to call the askQuestionWithMinBond function, It can be noted from the pointer above that a call is made to the _askQuestion(...) function in the same contract. https://github.com/immunefi-team/audit-comp-butter-cfm-v1/blob/main/src/FlatCFMRealityAdapter.sol#L121-L123
function _askQuestion(uint256 templateId, string memory formattedQuestionParams, uint32 openingTime)
private
returns (bytes32)
{
// See RealityETH reference for how question IDs are derived.
bytes32 contentHash = keccak256(abi.encodePacked(templateId, openingTime, formattedQuestionParams));
bytes32 questionId = keccak256(
abi.encodePacked(
contentHash, arbitrator, questionTimeout, minBond, address(oracle), address(this), uint256(0)
)
);
>>> // If already asked, return existing questionId.
if (oracle.getTimeout(questionId) != 0) {
>>> return questionId;
}
// Otherwise ask a new question with the provided parameters.
return oracle.askQuestionWithMinBond{value: msg.value}(
templateId, formattedQuestionParams, arbitrator, questionTimeout, openingTime, 0, minBond
);
}
The implementation of the _askQuestion(...) function as noted in the two pointers above shows that when question has already being asked the code returns without the need to call askQuestionWithMinBond with the fund instead of reverting with the implication that fund attached to this call would be locked in contract since no refund mechanism is in place in the function.
Impact Details
Callers Fund will be completely lost to the FlatCFMRealityAdapter contract when call is made to the askMetricQuestion(...) function subsequent times after the first call
Mitigation
Protocol should either revert the ask Metric call in subsequent calls to revert fund used or msg.value used to make the call can be refunded in the code directly
Proof of Concept
Proof of Concept
function testAskMetricQuestionLostFund(uint256 sendValue) public {
vm.assume(sendValue >= 1 ether);
deal(address(this), sendValue * 2);
// Store the initial balance before the first call
uint256 initialBalance = address(this).balance;
// Expect the first call to askMetricQuestion with the specified sendValue
vm.expectCall(
address(reality), sendValue, abi.encodeWithSelector(IRealityETHCore.askQuestionWithMinBond.selector)
);
realityAdapter.askMetricQuestion{value: sendValue}(metricTemplateId, genericScalarQuestionParams, "A");
// Assert the balance after the first call (balance should have decreased by sendValue)
uint256 balanceAfterFirstCall = address(this).balance;
assertEq(balanceAfterFirstCall, initialBalance - sendValue, "Balance after first call should decrease by sendValue");
// Prepare for the second call
uint256 secondSendValue = sendValue ;
uint256 balanceBeforeSecondCall = address(this).balance;
// Ensure the balance is enough for the second call
assert(balanceBeforeSecondCall >= secondSendValue, "Insufficient balance for second call");
// Expect the second call to askMetricQuestion with the second sendValue
vm.expectCall(
address(reality), secondSendValue, abi.encodeWithSelector(IRealityETHCore.askQuestionWithMinBond.selector)
);
realityAdapter.askMetricQuestion{value: secondSendValue}(metricTemplateId, genericScalarQuestionParams, "B");
// the balance after the second call would have decreased and lost
uint256 balanceAfterSecondCall = address(this).balance;
assertEq(balanceAfterSecondCall, balanceBeforeSecondCall , " fund lost to contract after second call ");
}
code can be copied and pasted to the FlatCFMRealityAdapter.t.sol test contract. The test will fail as callers balance have been lost to the contract after the second call