hats-finance / SeeR-PM-0x899bc13919880db76edf4ccd72bdfa5dfa666fb7

1 stars 0 forks source link

Markets that share the same reality question can `steal` each other earnings #101

Open hats-bug-reporter[bot] opened 1 month ago

hats-bug-reporter[bot] commented 1 month ago

Github username: -- Twitter username: -- Submission hash (on-chain): 0xc71a1d98f00c557a83d211a217a5e66cc16d3acb4db412bb0a069c472273587e Severity: high

Description:

Description

Seer app seems to allow to have multiple markets sharing the same reality question, but that use case doesn't seem to be working as expected. Seems like this allow for multiple edges cases, the main one being that a certain market can steal earning from another market if resolved first, even if there was not split on it.

PoC

Add this test in MainnetRouter.test and run yarn hardhat test

We can see that here we have 2 market sharing the same reality question. That is confirmed by marketAddress being different but not the conditionId, questionId, questionsIds, and even the ERC20 (please see output).

One user is splitting against the first market (market). Later on, marketA, which nobody split on it, get the response first and is resolved. Another user can redeem against marketA without any issue and without having invested anything, which is actually stealing from the first user which splitted on the other market which is not yet resolved.

    it.only("redeems a winning position from a market could steal another market earning when sharing the same reality question", async function () {
      const ANSWER = 1;
      const REDEEMED_POSITION = 1;

      // Create 2 markets. market and marketA
      await marketFactory.createCategoricalMarket(categoricalMarketParams);
      await marketFactory.createCategoricalMarket(categoricalMarketParams);
      let outcomeSlotCount = categoricalMarketParams.outcomes.length + 1;
      let marketAddress = (await marketFactory.allMarkets())[0];
      let market = await ethers.getContractAt("Market", marketAddress);
      let questionId = await market.questionId();
      let questionsIds = await market.questionsIds();
      let oracleAddress = await realityProxy.getAddress();
      let conditionId = await conditionalTokens.getConditionId(oracleAddress, questionId, outcomeSlotCount);

      console.log("marketAddress", marketAddress);
      console.log("conditionId", conditionId);
      console.log("questionId", questionId);
      console.log("outcomeSlotCount", outcomeSlotCount);
      console.log("questionsIds", questionsIds);

      let outcomeSlotCountA = categoricalMarketParams.outcomes.length + 1;
      let marketAddressA = (await marketFactory.allMarkets())[1];
      let marketA = await ethers.getContractAt("Market", marketAddressA);
      let questionIdA = await marketA.questionId();
      let questionsIdsA = await marketA.questionsIds();
      let oracleAddressA = await realityProxy.getAddress();
      let conditionIdA = await conditionalTokens.getConditionId(oracleAddressA, questionIdA, outcomeSlotCountA);

      console.log("marketAddressA", marketAddressA);
      console.log("conditionIdA", conditionIdA);
      console.log("questionIdA", questionIdA);
      console.log("outcomeSlotCountA", outcomeSlotCountA);
      console.log("questionsIdsA", questionsIdsA);

      // approve mainnetRouter to transfer user token to the contract
      await DAI.approve(mainnetRouter, ethers.parseEther(SPLIT_AMOUNT));
      // split collateral token to outcome tokens
      await mainnetRouter.splitFromDai(market, ethers.parseEther(SPLIT_AMOUNT));

      //const { outcomeSlotCount1, conditionId1, questionsIds1, market1 } = await createMarketAndSplitPosition1();
      const amountInSDai = await sDAI.convertToShares(ethers.parseEther(SPLIT_AMOUNT));
      // answer the question and resolve the market
      // past opening_ts
      await time.increase(OPENING_TS);

      // submit answer
      await realitio.submitAnswer(questionsIdsA[0], ethers.toBeHex(BigInt(ANSWER), 32), 0, {
        value: ethers.parseEther(MIN_BOND),
      });

      // past finalized_ts
      await time.increase(QUESTION_TIMEOUT);

      await realityProxy.resolve(marketA);

      // allow mainnetRouter to transfer position tokens to the contract
      for (let i = 0; i < outcomeSlotCount; i++) {
        const [wrapped1155] = await market.wrappedOutcome(i);                          
        const token = await ethers.getContractAt("Wrapped1155", wrapped1155);
        console.log("token", wrapped1155);
        await token.connect(owner).approve(mainnetRouter, ethers.parseEther(SPLIT_AMOUNT));
      }
      for (let i = 0; i < outcomeSlotCountA; i++) {
        const [wrapped1155] = await marketA.wrappedOutcome(i);                         
        const tokenA = await ethers.getContractAt("Wrapped1155", wrapped1155);
        console.log("tokenA", wrapped1155);
        await tokenA.connect(owner).approve(mainnetRouter, ethers.parseEther(SPLIT_AMOUNT));
      }

      const balanceBeforeRedeemA = await DAI.balanceOf(owner);
      await mainnetRouter.redeemToDai(marketA, [REDEEMED_POSITION]);                    
      const balanceAfterRedeemA = await DAI.balanceOf(owner);
      console.log("Earning from MarketA: ", balanceAfterRedeemA - balanceBeforeRedeemA);

      const balanceBeforeRedeem = await DAI.balanceOf(owner);
      await mainnetRouter.redeemToDai(market, [REDEEMED_POSITION]);                    
      const balanceAfterRedeem = await DAI.balanceOf(owner);
      console.log("Earning from Market: ", balanceAfterRedeem - balanceBeforeRedeem);
    });

OUTPUT


  MainnetRouter
    redeemPositions
marketAddress 0x650b920A858C060B3a56de29AcB85fac8D86C92c
conditionId 0x55dda986c8bd596bd2def320dab39a85d6ef71cf2e003e9cb115dc7346e69819
questionId 0x927763bfb06fb42e484f7904de6a39fa5c1330da946973aad123afad24d49fef
outcomeSlotCount 3
questionsIds Result(1) [
  '0x3b95c2b311275d13dfce20f0bffd931c2c33bbddac83512f0203b6cbc9b739c7'
]
marketAddressA 0x27e9a9Ba8cdf078CadC8c439f10c5B5B87256B11
conditionIdA 0x55dda986c8bd596bd2def320dab39a85d6ef71cf2e003e9cb115dc7346e69819
questionIdA 0x927763bfb06fb42e484f7904de6a39fa5c1330da946973aad123afad24d49fef
outcomeSlotCountA 3
questionsIdsA Result(1) [
  '0x3b95c2b311275d13dfce20f0bffd931c2c33bbddac83512f0203b6cbc9b739c7'
]
token 0x31b6cD25d41946963ddFb3c13fED34990010a793
token 0x4B05208F95aEe7e6FEf93a094Aa30A0a513AbCE8
token 0x4480128E074316FAB2e1383C5f8995aD78524bcf
tokenA 0x31b6cD25d41946963ddFb3c13fED34990010a793
tokenA 0x4B05208F95aEe7e6FEf93a094Aa30A0a513AbCE8
tokenA 0x4480128E074316FAB2e1383C5f8995aD78524bcf
Earning from MarketA:  10005655748582799392n
Earning from Market:  0n
      ✔ redeems a winning position from  and send collateral tokens to user (3342ms)
dontonka commented 1 month ago

Be aware that while 98 is similar and probaly demostrating the root cause, the fact that he/she is not confirming and demostrating clear impacts (hence why submitted as Low), I would assume that if my report is confirmed, I should be fully awarded, as the first one to proove the most damaging impact.

xyzseer commented 1 month ago

correct me if I'm wrong, but the POC has only one user interacting with the markets, so is he stealing from himself? who lost money in the process? can you show a POC with two users, where one of the users is unable to redeem his shares?

what I see is: both markets share the same "pot" on the Conditional Tokens contract. The user splits on market 1 and then redeems on market 2. The result is the same as spliting on market 1 and then redeeming on market 1, as both markets are interacting with the same questionId / conditionId.

dontonka commented 1 month ago

Indeed, I had an oversight for this, at least now I understand better the system, thanks for clarifying.