hats-finance / SeeR-PM-0x899bc13919880db76edf4ccd72bdfa5dfa666fb7

1 stars 0 forks source link

Unlimited Token Approval Vulnerability in Router Contract #121

Open hats-bug-reporter[bot] opened 2 months ago

hats-bug-reporter[bot] commented 2 months ago

Github username: -- Twitter username: -- Submission hash (on-chain): 0xac22b432f475565c9ca28e80fa970c0b30d912cf334aec73ee9b521fe07c2129 Severity: medium

Description:

Details

The Router contract contains a potential vulnerability related to unlimited token approvals in its _splitPosition function. This issue could expose users to unnecessary risk if the contract is compromised or contains bugs.

In the _splitPosition function, the contract approves an unlimited amount of collateral tokens to be spent by the ConditionalTokens contract. This approval is not revoked after the operation, leaving it open for potential misuse.

Code Snippet

function _splitPosition(IERC20 collateralToken, Market market, uint256 amount) internal {
    // ... (earlier code omitted)

    if (parentCollectionId == bytes32(0)) {
        collateralToken.approve(address(conditionalTokens), amount);
    }

    conditionalTokens.splitPosition(address(collateralToken), parentCollectionId, conditionId, partition, amount);

    // ... (rest of the function)
}

Impact

If the ConditionalTokens contract is compromised or contains bugs, it could potentially spend more of the user's collateral tokens than intended. This could lead to:

  1. Loss of user funds beyond the intended amount.
  2. Unexpected behavior in the market operations.
  3. Reduced trust in the protocol due to lingering approvals.

The severity of this vulnerability is high, as it doesn't immediately lead to fund loss but creates a significant risk if other parts of the system are compromised.

Steps to Reproduce

  1. Deploy the Router contract and associated contracts (ConditionalTokens, Market, etc.).
  2. Call splitPosition with a certain amount of collateral tokens.
  3. Check the approval amount of the Router contract for the ConditionalTokens contract - it will be unlimited.
  4. Subsequent calls to splitPosition will not change this unlimited approval.

Fix

Implement a pattern of approving only the necessary amount and then resetting the approval to zero after the operation. This can be achieved by:

  1. Approving only the exact amount needed for the operation.
  2. Using OpenZeppelin's SafeERC20 library, which provides safeApprove and safeIncreaseAllowance functions to handle approvals more safely.
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract Router is ERC1155Holder {
    using SafeERC20 for IERC20;

    // ... (other code remains the same)

    function _splitPosition(IERC20 collateralToken, Market market, uint256 amount) internal {
        // ... (earlier code remains the same)

        if (parentCollectionId == bytes32(0)) {
            collateralToken.safeApprove(address(conditionalTokens), 0); // Reset approval
            collateralToken.safeApprove(address(conditionalTokens), amount);
        }

        conditionalTokens.splitPosition(address(collateralToken), parentCollectionId, conditionId, partition, amount);

        if (parentCollectionId == bytes32(0)) {
            collateralToken.safeApprove(address(conditionalTokens), 0); // Reset approval after use
        }

        // ... (rest of the function remains the same)
    }

    // ... (other code remains the same)
}

Proof of Concept

describe("Router - Unlimited Approval Bug", function () {
  it("leaves unlimited approval after splitting position", async function () {
    const [owner] = await ethers.getSigners();
    
    // Create a market and split position
    const { market } = await createMarketAndSplitPosition();

    // Check the approval amount after splitting
    const approvalAfterSplit = await collateralToken.allowance(router.address, conditionalTokens.address);
    
    // The approval should be unlimited (represented by the maximum uint256 value)
    expect(approvalAfterSplit).to.equal(ethers.constants.MaxUint256);

    // Attempt to split again with a different amount
    const newSplitAmount = ethers.parseEther("100");  // Assuming this is different from the original SPLIT_AMOUNT
    await collateralToken.mint(owner, newSplitAmount);
    await collateralToken.approve(router.address, newSplitAmount);
    
    await router.splitPosition(
      collateralToken.address,
      market.address,
      newSplitAmount
    );

    // Check the approval amount again
    const approvalAfterSecondSplit = await collateralToken.allowance(router.address, conditionalTokens.address);
    
    // The approval should still be unlimited
    expect(approvalAfterSecondSplit).to.equal(ethers.constants.MaxUint256);

    // Verify that the ConditionalTokens contract could potentially spend more than intended
    const routerBalance = await collateralToken.balanceOf(router.address);
    expect(approvalAfterSecondSplit).to.be.gt(routerBalance);
  });
});

Here's what the test is doing:

  1. It creates a market and splits a position using the existing createMarketAndSplitPosition helper function.
  2. It checks the approval amount after splitting, expecting it to be unlimited (represented by the maximum uint256 value).
  3. It attempts to split again with a different amount to show that the approval remains unlimited.
  4. Finally, it verifies that the approval amount is greater than the Router's balance, demonstrating that the ConditionalTokens contract could potentially spend more than intended.
greenlucid commented 2 months ago

That code snippet makes it look like it doesn't approve an unlimited amount.

clesaege commented 2 months ago

Yeah, the report and code snippet are contradicting themselves. It only approve the amount of tokens which will be transferred in splitPosition.