Ignite Market Disclosed Report

Users can skip calling FixedProductMarketMaker.buy() and directly call ConditionalTokens.splitPosition() to get more conditional tokens than intended

Created date
May 13 2025

Target

https://github.com/hackenproof-public/ignite-market-smart-contracts

Vulnerability Details

When users call buy() in FixedProductMarketMaker.sol, they trade their collateral amount for a portion of the conditional token. The exchange rate is calculated through the AMM. Note that they only get 1 type of conditional token regardless of the number of outcomes available since they are only purchasing that particular outcome.

For example, for a condition with 3 outcomes, the user buys the 3rd outcome which he thinks is the most probable (index 2).

function buy(uint investmentAmount, uint outcomeIndex, uint minOutcomeTokensToBuy) external nonReentrant() {
        require(canTrade(), "trading not allowed");
        require((investmentAmount * 100) / fundingAmountTotal <= 10, "amount can be up to 10% of fundingAmountTotal");

        uint outcomeTokensToBuy = calcBuyAmount(investmentAmount, outcomeIndex);
        require(outcomeTokensToBuy >= minOutcomeTokensToBuy, "minimum buy amount not reached");

>       collateralToken.safeTransferFrom(msg.sender, address(this), investmentAmount);
        uint feeAmount = (investmentAmount * fee) / ONE;
        feePoolWeight += feeAmount;
        uint investmentAmountMinusFees = investmentAmount - feeAmount;

        collateralToken.forceApprove(address(conditionalTokens), investmentAmountMinusFees);
        splitPositionThroughAllConditions(investmentAmountMinusFees);

>       conditionalTokens.safeTransferFrom(address(this), msg.sender, positionIds[outcomeIndex], outcomeTokensToBuy, "");
        emit FPMMBuy(msg.sender, investmentAmount, feeAmount, outcomeIndex, outcomeTokensToBuy);
    }

Instead of getting one type of conditional token, the user can directly call splitPosition() and fill in all the parameters (the parameters are known with no access control to the function), this way the user can get all the different types of conditional tokens with just one amount.

  function splitPosition(
        IERC20 collateralToken,
        bytes32 parentCollectionId,
        bytes32 conditionId,
        uint[] calldata partition,
        uint amount
    ) external {
        require(partition.length > 1, "got empty or singleton partition");
        uint outcomeSlotCount = payoutNumerators[conditionId].length;
        require(outcomeSlotCount > 0, "condition not prepared yet");

        uint fullIndexSet = (1 << outcomeSlotCount) - 1;
        uint freeIndexSet = fullIndexSet;
        uint[] memory positionIds = new uint[](partition.length);
        uint[] memory amounts = new uint[](partition.length);
        for (uint i = 0; i < partition.length; i++) {
            uint indexSet = partition[i];
            require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
            require((indexSet & freeIndexSet) == indexSet, "partition not disjoint");
            freeIndexSet ^= indexSet;
            positionIds[i] = CTHelpers.getPositionId(collateralToken, CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));
 >          amounts[i] = amount;
        }

        if (freeIndexSet == 0) {
            if (parentCollectionId == bytes32(0)) {
                require(collateralToken.transferFrom(msg.sender, address(this), amount), "could not receive collateral tokens");
            } else {
                _burn(
                    msg.sender,
                    CTHelpers.getPositionId(collateralToken, parentCollectionId),
                    amount
                );
            }
        } else {
            _burn(
                msg.sender,
                CTHelpers.getPositionId(collateralToken,
                    CTHelpers.getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
                amount
            );
        }

>       _mintBatch(
            msg.sender,
            positionIds,
            amounts,
            ""
        );
        emit PositionSplit(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount);
    }

The function will mint conditional tokens for all the positionIds.

Validation steps

One condition with three outcomes will give 3 unique positionIds:

1 << i gives outcome slot:

1 << 0 = 1 → Outcome A

1 << 1 = 2 → Outcome B

1 << 2 = 4 → Outcome C

Bob spends 100e6 , and assume he gets about 25e18 of conditional token with positionID 0xabc.

Instead, Bob calls splitPosition() directly, and he gets 3 types of conditional tokens, positionId 0xabc, positionId 0xdef, and positionId 0xghi. Bob can then sell his other conditional tokens and get back collateral, while still holding the conditional token that he wants.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
High
Bounty$2,000
Visibilitypartially
VulnerabilityImproper Access Control
Participants (2)