Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Liquidity may be lost when sent to a non-existent token #66

Open
code423n4 opened this issue Dec 12, 2022 · 6 comments
Open

Liquidity may be lost when sent to a non-existent token #66

code423n4 opened this issue Dec 12, 2022 · 6 comments
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue grade-b Q-13 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2022-12-Stealth-Project/blob/fc8589d7d8c1d8488fd97ccc46e1ff11c8426ac2/maverick-v1/contracts/models/Pool.sol#L134

Vulnerability details

Impact

Users may lose funds when providing liquidity to a non-existent token. A wrong token ID may be provided mistakenly or due to a software bug.

Proof of Concept

The protocol implements liquidity position NFT: a collection of NFT tokens that are used to identify liquidity providers in pools (Position.sol#L12). When providing liquidity to pools, liquidity providers must specify an NFT token ID (Pool.sol#L114). Provided liquidity will be associated with the token ID, and only the owner of the token or an approved address will be able to withdraw liquidity from pools (Pool.sol#L217).

However, the addLiquidity function of Pool doesn't validate the user-provided token ID: it can be any value from the full range of the values of uint256, no matter whether a token exists or not:

function addLiquidity(
    uint256 tokenId,
    AddLiquidityParams[] calldata params,
    bytes calldata data
) external override returns (uint256 tokenAAmount, uint256 tokenBAmount, BinDelta[] memory binDeltas) {
    ...
    for (uint256 i; i < params.length; i++) {
      ...
        (temp.deltaA, temp.deltaB, temp.deltaLpBalance) = bin.addLiquidity(
            tokenId, // @audit missing tokenId validation
            Math.fromScale(param.deltaA, tokenAScale),
            Math.fromScale(param.deltaB, tokenBScale),
            currentState.activeTick,
            temp.reserveA,
            temp.reserveB
        );

In case a user provides liquidity to a non existent token, it cannot be withdrawn due to the ownership check (Pool.sol#L217):

function removeLiquidity(
    address recipient,
    uint256 tokenId,
    RemoveLiquidityParams[] calldata params
) external override returns (uint256 tokenAOut, uint256 tokenBOut, BinDelta[] memory binDeltas) {
    ...
    checkPositionAccess(tokenId);
    ...
}

function checkPositionAccess(uint256 tokenId) internal view {
    require(msg.sender == position.ownerOf(tokenId) || msg.sender == position.getApproved(tokenId), "P");
}

Thus, when liquidity is provided to a non-existent NFT, there are two scenarios:

  1. If the ID can be minted in the future, the minter of the token will be the owner of the liquidity.
  2. If it cannot be minted (e.g. the wrong token ID was set to type(uint256).max), the liquidity wil be locked in the contract forever. This is loss of funds.

Here's a proof of concept that demonstrates providing liquidity to a non-existent token ID and the inability to withdraw liquidity afterwards:

// router-v1/test/Router.ts
//   describe("#getOrCreatePoolAndAddLiquidity", () => {
it("allows to add liquidity to non-existent tokens [AUDIT]", async () => {
  let pool: string = await getEthBPool(0.05, 5);
  const prePoolBalance = await balances(pool);
  const nonexistentTokenId = 31337;

  await router.addLiquidityToPool(
    pool,
    nonexistentTokenId,
    [
      {
        kind: 0,
        isDelta: true,
        pos: 0,
        deltaA: floatToFixed(5),
        deltaB: floatToFixed(5),
      },
    ],
    0,
    0,
    maxDeadline
  );

  const postPoolBalance = await balances(pool);
  expect(postPoolBalance.weth9 - prePoolBalance.weth9).to.eq(5);
  expect(postPoolBalance.tokenB - prePoolBalance.tokenB).to.eq(5);

  // Reverts with 'ERC721: invalid token ID'
  await router.removeLiquidity(
    pool,
    await owner.getAddress(),
    nonexistentTokenId,
    [
      {
        binId: 1,
        amount: floatToFixed(5),
        maxDepth: 0,
      },
    ],
    0,
    0,
    maxDeadline
  );
});

Tools Used

Manual review

Recommended Mitigation Steps

Consider disallowing providing liquidity to non-existent NFT IDs.

@code423n4 code423n4 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Dec 12, 2022
code423n4 added a commit that referenced this issue Dec 12, 2022
@hansfriese
Copy link

hansfriese commented Dec 14, 2022

This reminds me of a discussion regarding the NFT standards.

It is not the job of a smart contract to protect against every possible user error (it must protect against invalid actions, not unintended actions). ... Smart contracts don't have the equivalent of a "This seems unsafe, are you sure?" dialog box!

Duplicate of #13

@c4-judge
Copy link
Contributor

kirk-baird marked the issue as duplicate of #13

@c4-judge
Copy link
Contributor

c4-judge commented Jan 3, 2023

kirk-baird marked the issue as not a duplicate

@c4-judge c4-judge added QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax downgraded by judge Judge downgraded the risk level of this issue and removed duplicate-13 3 (High Risk) Assets can be stolen/lost/compromised directly labels Jan 3, 2023
@c4-judge
Copy link
Contributor

c4-judge commented Jan 3, 2023

kirk-baird changed the severity to QA (Quality Assurance)

@kirk-baird
Copy link

For reasons stated in #13 I'm going to downgrade this to QA.

@c4-judge
Copy link
Contributor

kirk-baird marked the issue as grade-b

@C4-Staff C4-Staff added the Q-13 label Jan 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue grade-b Q-13 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax
Projects
None yet
Development

No branches or pull requests

5 participants