Contract fails to deliver promised returns, but doesn't lose value
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
Min and max values are not validated when creating a FlatCFM, allowing minValue == maxValue. This results in the scalar market incorrectly resolving all payout to the short tokens if the numeric answer equals that boundary.
Vulnerability Details
Because both min and max can be the same when creating a FlatCFM generic question params for the conditional scalar markets the metric question resolution interprets that numeric answer as valid for short tokens.
When we call FlatCFMFactory::createConditionalScalarMarket to create the conditional scalar market everything will work as expected. However, once the answer is provided by the oracle and if the numeric answer equals the same value as the min and max (since they are equal), the payouts will be received only from the short tokens.
function resolve() external {
bytes32 answer = oracleAdapter.getAnswer(ctParams.questionId);
uint256[] memory payouts = new uint256[](3);
if (oracleAdapter.isInvalid(answer)) {
// 'Invalid' outcome receives full payout
payouts[2] = 1;
} else {
uint256 numericAnswer = uint256(answer);
if (numericAnswer <= scalarParams.minValue) {
payouts[0] = 1; // short
} else if (numericAnswer >= scalarParams.maxValue) {
payouts[1] = 1; // long
} else {
payouts[0] = scalarParams.maxValue - numericAnswer;
payouts[1] = numericAnswer - scalarParams.minValue;
}
}
conditionalTokens.reportPayouts(ctParams.questionId, payouts);
}
Impact Details
Funds can be diverted solely to the short outcome, causing mispricing and potentially incorrect compensation for other positions if exploited to the bug.
From the unit test folder, in the CondtionalScalarMarket.t.sol paste the following code to prove that if the min and max values are the same and only short tokens will receive payouts
contract WrongMinMaxTest is Test {
FlatCFMOracleAdapter oracleAdapter;
IConditionalTokens conditionalTokens;
IWrapped1155Factory wrapped1155Factory;
ConditionalScalarMarket csm;
IERC20 collateralToken;
IERC20 shortToken;
IERC20 longToken;
IERC20 invalidToken;
address constant USER = address(0x1111);
uint256 constant DEAL = 10;
bytes32 constant QUESTION_ID = bytes32("some question id");
bytes32 constant CONDITION_ID = bytes32("some condition id");
bytes32 constant PARENT_COLLECTION_ID = bytes32("someParentCollectionId");
// set the min valud to be equal to the max value
uint256 constant MIN_VALUE = 11000;
uint256 constant MAX_VALUE = 11000;
function setUp() public virtual {
// 1. Deploy or mock the external dependencies
oracleAdapter = new DummyFlatCFMOracleAdapter();
conditionalTokens = new DummyConditionalTokens();
wrapped1155Factory = new DummyWrapped1155Factory();
collateralToken = new DummyERC20("Collateral", "COL");
shortToken = new DummyERC20("Short", "ST");
longToken = new DummyERC20("Long", "LG");
invalidToken = new DummyERC20("Invalid", "XX");
// 2. Deploy the ConditionalScalarMarket
csm = new ConditionalScalarMarket();
csm.initialize(
oracleAdapter,
conditionalTokens,
wrapped1155Factory,
ConditionalScalarCTParams({
questionId: QUESTION_ID,
conditionId: CONDITION_ID,
parentCollectionId: PARENT_COLLECTION_ID,
collateralToken: collateralToken
}),
ScalarParams({minValue: MIN_VALUE, maxValue: MAX_VALUE}),
WrappedConditionalTokensData({
shortData: "",
longData: "",
invalidData: "",
shortPositionId: 1,
longPositionId: 2,
invalidPositionId: 2,
wrappedShort: shortToken,
wrappedLong: longToken,
wrappedInvalid: invalidToken
})
);
}
function testResolveWhenMinAndMaxAreEqualAndResultIsTheSame() public {
uint256 answer = MIN_VALUE;
uint256[] memory expectedPayout = new uint256[](3);
// even when the result is the same as the max value
// only the short position will receive payout
// the long position will receive 0 as payout
expectedPayout[0] = 1;
expectedPayout[1] = 0;
expectedPayout[2] = 0;
vm.mockCall(
address(oracleAdapter),
abi.encodeWithSelector(FlatCFMOracleAdapter.getAnswer.selector, QUESTION_ID),
abi.encode(answer)
);
vm.expectCall(
address(conditionalTokens),
abi.encodeWithSelector(IConditionalTokens.reportPayouts.selector, QUESTION_ID, expectedPayout)
);
csm.resolve();
}
}
and to prove that a cfm can be created with equal min and max generic scalar parameters paste this at the bottom of the file FlatCFMFactory.t.sol