Polynomial Protocol contest - bin2chen's results

The DeFi Derivatives Powerhouse.

General Information

Platform: Code4rena

Start Date: 13/03/2023

Pot Size: $72,500 USDC

Total HM: 33

Participants: 35

Period: 7 days

Judge: Dravee

Total Solo HM: 16

Id: 222

League: ETH

Polynomial Protocol

Findings Distribution

Researcher Performance

Rank: 8/35

Findings: 5

Award: $1,621.84

QA:
grade-b

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: MiloTruck

Also found by: 0x52, 0xRobocop, Bauer, bin2chen, chaduke

Labels

bug
3 (High Risk)
partial-50
duplicate-206

Awards

131.4335 USDC - $131.43

External Links

Lines of code

https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/ShortToken.sol#L82-L84

Vulnerability details

Impact

adjustPosition() when burn NFT , lack of judgment on whether collateral is still available, which may result in loss of collateral

Proof of Concept

When Exchange.sol needs to adjust the position, shortToken.adjustPosition() will be called The code to implement adjustPosition() is as follows:

    function adjustPosition(
        uint256 positionId,
        address trader,
        address collateral,
        uint256 shortAmount,
        uint256 collateralAmount
    ) external onlyExchange returns (uint256) {
        if (positionId == 0) {
....
        } else {
            require(trader == ownerOf(positionId));

            ShortPosition storage position = shortPositions[positionId];

            if (shortAmount >= position.shortAmount) {
                totalShorts += shortAmount - position.shortAmount;
            } else {
                totalShorts -= position.shortAmount - shortAmount;
            }

            position.collateralAmount = collateralAmount;
            position.shortAmount = shortAmount;

            if (position.shortAmount == 0) {  //<--------There is no check if collateralAmount is 0
                _burn(positionId);
            }
        }

The current implementation, if shortAmount is adjusted to 0, puts burn NFT Here is a problem, only check the shortAmount is 0, and not check whether the position.collateralAmount is 0 so there may be still have collateral on the NFT, but the NFT is destroyed, the user can not retrieve collateral

Example: Exchange.sol#closeTrade will call this method, and the user can specify the reduced shortAmount and collateralAmount The user adjusts the shortAmount to 0, and wants to retain the collateral, but the current implementation , the entire NFT will be burned, the collateral will be lost

    function _closeTrade(TradeParams memory params) internal returns (uint256) {
        if (params.isLong) {
...
        } else {
...
            uint256 totalShortAmount = shortPosition.shortAmount - params.amount;
            uint256 totalCollateralAmount = shortPosition.collateralAmount - params.collateralAmount;
....
            shortToken.adjustPosition(   //<------if totalShortAmount =0 ,but totalCollateralAmount>0 ,NFT Will still be burned
                params.positionId, msg.sender, params.collateral, totalShortAmount, totalCollateralAmount
            );                            

Example: alice has shortToken: positionId=1 shortAmount=10 collateralAmount=100

alice executes closeTrade: positionId=1 TradeParams.Amount=10 params.collateralAmount=0 //retains collateral

This will cause NFT to be burned and collateral will be lost 100

Tools Used

    function adjustPosition(
        uint256 positionId,
        address trader,
        address collateral,
        uint256 shortAmount,
        uint256 collateralAmount
    ) external onlyExchange returns (uint256) {
        if (positionId == 0) {
....
        } else {
            require(trader == ownerOf(positionId));

            ShortPosition storage position = shortPositions[positionId];

            if (shortAmount >= position.shortAmount) {
                totalShorts += shortAmount - position.shortAmount;
            } else {
                totalShorts -= position.shortAmount - shortAmount;
            }

            position.collateralAmount = collateralAmount;
            position.shortAmount = shortAmount;

-           if (position.shortAmount == 0) {
+           if (position.shortAmount == 0 && position.collateralAmount==0) {
                _burn(positionId);
            }

#0 - c4-judge

2023-03-21T00:42:33Z

JustDravee marked the issue as satisfactory

#1 - c4-judge

2023-03-21T00:42:42Z

JustDravee marked the issue as primary issue

#2 - c4-judge

2023-03-21T02:28:46Z

JustDravee marked issue #65 as primary and marked this issue as a duplicate of 65

#3 - JustDravee

2023-05-02T23:31:25Z

Partial for lack of coded POC on a High

#4 - c4-judge

2023-05-02T23:31:30Z

JustDravee marked the issue as partial-50

Findings Information

🌟 Selected for report: joestakey

Also found by: 0x52, Bauer, KIntern_NA, auditor0517, bin2chen, chaduke, juancito

Labels

bug
3 (High Risk)
partial-50
duplicate-189

Awards

79.8459 USDC - $79.85

External Links

Lines of code

https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/KangarooVault.sol#L436-L447

Vulnerability details

Impact

KangarooVault.removeCollateral() Lack of actual calls EXCHANGE.removeCollateral(), Resulting in the collateral not being retrieved

Proof of Concept

removeCollateral() is used to remove collateral The code is as follows:

    function removeCollateral(uint256 collateralToRemove) external requiresAuth nonReentrant {
        (uint256 markPrice,) = LIQUIDITY_POOL.getMarkPrice();
        uint256 minColl = positionData.shortAmount.mulWadDown(markPrice);
        minColl = minColl.mulWadDown(collRatio);

        require(positionData.totalCollateral >= minColl + collateralToRemove);

        //<------no execute EXCHANGE.removeCollateral()

        usedFunds -= collateralToRemove;
        positionData.totalCollateral -= collateralToRemove;

        emit RemoveCollateral(positionData.positionId, collateralToRemove);
    }

In the above code, forget to actually call EXCHANGE.removeCollateral()

But positionData.totalCollateral has a corresponding decrease, so that the reduced number of collateral can no longer be retrieved and is locked

Tools Used

    function removeCollateral(uint256 collateralToRemove) external requiresAuth nonReentrant {
        (uint256 markPrice,) = LIQUIDITY_POOL.getMarkPrice();
        uint256 minColl = positionData.shortAmount.mulWadDown(markPrice);
        minColl = minColl.mulWadDown(collRatio);

        require(positionData.totalCollateral >= minColl + collateralToRemove);

+       EXCHANGE.removeCollateral(positionData.positionId,collateralToRemove);

        usedFunds -= collateralToRemove;
        positionData.totalCollateral -= collateralToRemove;

        emit RemoveCollateral(positionData.positionId, collateralToRemove);
    }

#0 - c4-judge

2023-03-21T00:15:43Z

JustDravee marked the issue as satisfactory

#1 - c4-judge

2023-03-21T00:15:49Z

JustDravee marked the issue as duplicate of #111

#2 - JustDravee

2023-05-02T23:21:26Z

Partial for lack of coded POC on a High

#3 - c4-judge

2023-05-02T23:21:30Z

JustDravee marked the issue as partial-50

Findings Information

🌟 Selected for report: Lirios

Also found by: bin2chen

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-103

Awards

1201.9527 USDC - $1,201.95

External Links

Lines of code

https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L284

Vulnerability details

Impact

The malicious user specifies receive user=address(SUSD), thus blocking the entire withdrawal queue

Proof of Concept

processWithdraws() is used to process the user's withdrawal request Currently the processing logic is one by one in order and cannot be skipped The implementation code is as follows:

    function processWithdraws(uint256 count) external override nonReentrant whenNotPaused("POOL_PROCESS_WITHDRAWS") {
....

        for (uint256 i = 0; i < count; i++) {
            uint256 tokenPrice = getTokenPrice();
            QueuedWithdraw storage current = withdrawalQueue[queuedWithdrawalHead];

           if (susdToReturn > availableFunds) {
....
            } else {

                current.returnedAmount = susdToReturn;
                totalQueuedWithdrawals -= current.withdrawnTokens;
                totalFunds -= susdToReturn;

                SUSD.safeTransfer(current.user, susdToReturn); //<--------execute transfer

                emit ProcessWithdrawal(
                    current.id, current.user, current.withdrawnTokens, susdToReturn, current.requestedTime
                    );

                current.withdrawnTokens = 0;
            }  
            queuedWithdrawalHead++; //<----- ++, next turn          

There is a problem, if one user's SUSD.safeTransfer() fails, then the queue behind it will be blocked and cannot be cancelled

So SUSD.transfer() could possibly revert? Check inside SUSD.transfer() method, SUSD has a restriction on to, it can't be address(SUSD) https://etherscan.io/address/0x10A5F7D9D65bCc2734763444D4940a31b109275f#code

    function _internalTransfer(
        address from,
        address to,
        uint value
    ) internal returns (bool) {
        /* Disallow transfers to irretrievable-addresses. */
        require(to != address(0) && to != address(this) && to != address(proxy), "Cannot transfer to this address"); //<-----if to == address(this) or address(proxy) will revert

        // Insufficient balance will be handled by the safe subtraction.
        tokenState.setBalanceOf(from, tokenState.balanceOf(from).sub(value));
        tokenState.setBalanceOf(to, tokenState.balanceOf(to).add(value));

        // Emit a standard ERC20 transfer event
        emitTransfer(from, to, value);

        return true;
    }

The code above shows that if execute SUSD.transfer(to=address(SUSD)) it will revert

And we can specify the user when we join the withdrawal queue

    function queueWithdraw(uint256 tokens, address user)   //<-------can set user=address(SUSD)    

As mentioned above, if a malicious user generates a withdrawal queue with to=address(SUSD), the subsequent user's withdrawals will all be blocked and the funds in the queue will be lost

Note: KangarooVault.processWithdrawalQueue() also has the same problem

Tools Used

queueWithdraw() cancel parameter user

-   function queueWithdraw(uint256 tokens, address user)
+   function queueWithdraw(uint256 tokens)
        external
        override
        nonReentrant
        whenNotPaused("POOL_QUEUE_WITHDRAW")
    {
        require(liquidityToken.balanceOf(msg.sender) >= tokens);
        QueuedWithdraw storage newWithdraw = withdrawalQueue[nextQueuedWithdrawalId];
        newWithdraw.id = nextQueuedWithdrawalId++;
-       newWithdraw.user = user;
+       newWithdraw.user = msg.sender;
        newWithdraw.withdrawnTokens = tokens;
        newWithdraw.requestedTime = block.timestamp;
        totalQueuedWithdrawals += tokens;
        liquidityToken.burn(msg.sender, tokens);

        emit InitiateWithdrawal(newWithdraw.id, msg.sender, user, tokens);
    }

#0 - c4-judge

2023-03-22T17:37:04Z

JustDravee marked the issue as duplicate of #103

#1 - c4-judge

2023-03-22T18:13:00Z

JustDravee changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-05-02T21:33:28Z

JustDravee marked the issue as satisfactory

#3 - c4-judge

2023-05-15T23:29:23Z

JustDravee changed the severity to 3 (High Risk)

Findings Information

🌟 Selected for report: MiloTruck

Also found by: adriro, bin2chen, chaduke, csanuragjain

Labels

bug
2 (Med Risk)
satisfactory
duplicate-236

Awards

105.1468 USDC - $105.15

External Links

Lines of code

https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/Exchange.sol#L333

Vulnerability details

Impact

_liquidate () Lack of slippage protection,The amount of collateral is lower than user expectations

Proof of Concept

_liquidate () code is as follows:

    function _liquidate(uint256 positionId, uint256 debtRepaying) internal {//<------without minCollateralReturned
        uint256 maxDebtRepayment = shortCollateral.maxLiquidatableDebt(positionId);
        require(maxDebtRepayment > 0);
        if (debtRepaying > maxDebtRepayment) debtRepaying = maxDebtRepayment;

        IShortToken.ShortPosition memory position = shortToken.shortPositions(positionId);

        uint256 totalCollateralReturned = shortCollateral.liquidate(positionId, debtRepaying, msg.sender);

        address user = shortToken.ownerOf(positionId);

        uint256 finalPosition = position.shortAmount - debtRepaying;
        uint256 finalCollateralAmount = position.collateralAmount - totalCollateralReturned;

        shortToken.adjustPosition(positionId, user, position.collateral, finalPosition, finalCollateralAmount);

        pool.liquidate(debtRepaying);
        
        powerPerp.burn(msg.sender, debtRepaying);

        emit Liquidate(user, positionId, msg.sender, debtRepaying, position.collateral, totalCollateralReturned);
    }

Since markPrice and collateralPrice are real-time prices, they may be different from the user's original expectation, resulting in slippage in the resulting collateral. Need to add minCollateralReturned to protect

Tools Used

-   function _liquidate(uint256 positionId, uint256 debtRepaying) internal {
+   function _liquidate(uint256 positionId, uint256 debtRepaying ,uint256 minCollateralReturned) internal {
        uint256 maxDebtRepayment = shortCollateral.maxLiquidatableDebt(positionId);
        require(maxDebtRepayment > 0);
        if (debtRepaying > maxDebtRepayment) debtRepaying = maxDebtRepayment;

        IShortToken.ShortPosition memory position = shortToken.shortPositions(positionId);

        uint256 totalCollateralReturned = shortCollateral.liquidate(positionId, debtRepaying, msg.sender);
+       require(totalCollateralReturned>=minCollateralReturned,"bad totalCollateralReturned");

        address user = shortToken.ownerOf(positionId);

        uint256 finalPosition = position.shortAmount - debtRepaying;
        uint256 finalCollateralAmount = position.collateralAmount - totalCollateralReturned;

        shortToken.adjustPosition(positionId, user, position.collateral, finalPosition, finalCollateralAmount);

        pool.liquidate(debtRepaying);
        
        powerPerp.burn(msg.sender, debtRepaying);

        emit Liquidate(user, positionId, msg.sender, debtRepaying, position.collateral, totalCollateralReturned);
    }

#0 - c4-judge

2023-03-25T04:55:25Z

JustDravee marked the issue as duplicate of #236

#1 - c4-judge

2023-05-03T00:01:14Z

JustDravee marked the issue as satisfactory

Findings Information

Labels

bug
grade-b
QA (Quality Assurance)
Q-06

Awards

103.4639 USDC - $103.46

External Links

L-01:Exchange._addCollateral() Missing to determine whether the current collateral is still valid, _removeCollateral() and _openTrade() both have checks, it is recommended to also add checks to avoid the collateral is no longer valid

   function _addCollateral(uint256 positionId, uint256 amount) internal {
..

        shortToken.adjustPosition(
            positionId,
            msg.sender,
            shortPosition.collateral,
            shortPosition.shortAmount,
            shortPosition.collateralAmount + amount
        );
+       //for check collateral valid
+       shortCollateral.getMinCollateral(shortPosition.shortAmount, shortPosition.collateralAmount + amount);
        shortCollateral.collectCollateral(shortPosition.collateral, positionId, amount);

        emit AddCollateral(msg.sender, positionId, shortPosition.collateral, amount);
    }

L-02:processDepositQueue() Each QueuedDeposit re-fetches the price more realistically, and each QueuedDeposit may modify the TokenPrice like processWithdrawalQueue():

    function processDepositQueue(uint256 idCount) external nonReentrant {
-       uint256 tokenPrice = getTokenPrice();

        for (uint256 i = 0; i < idCount; i++) {
+           uint256 tokenPrice = getTokenPrice();        
            QueuedDeposit storage current = depositQueue[queuedDepositHead];

            if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minDepositDelay) {
                return;
            }

            uint256 tokensToMint = current.depositedAmount.divWadDown(tokenPrice);

            current.mintedTokens = tokensToMint;
            totalQueuedDeposits -= current.depositedAmount;
            totalFunds += current.depositedAmount;
            VAULT_TOKEN.mint(current.user, tokensToMint);

            emit ProcessDeposit(current.id, current.user, current.depositedAmount, tokensToMint, current.requestedTime);

            current.depositedAmount = 0;
            queuedDepositHead++;
        }
    }

L-03 LiquidityPool.minDepositDelay The limit must be greater than 0, to avoid QueuedDeposit and deposit() can be the same as real-time, but do not have to pay depositFee

    function setMinDelays(uint256 _minDepositDelay, uint256 _minWithdrawDelay) external requiresAuth {
+       require(_minDepositDelay>0 && _minWithdrawDelay>0,">0");
        emit UpdateDelays(minDepositDelay, _minDepositDelay, minWithdrawDelay, _minWithdrawDelay);
        minDepositDelay = _minDepositDelay;
        minWithdrawDelay = _minWithdrawDelay;
    }

L-04:EXCHANGE.sol#_addCollateral() Remove the restriction on determining whether the MarkPrice is valid or not, because to avoid liquidation, the user should be able to add collateral regardless of the current MarkPrice being temporarily invalid.

    function _addCollateral(uint256 positionId, uint256 amount) internal {
        require(positionId != 0);
-       (, bool isInvalid) = getMarkPrice();
-       require(!isInvalid);

#0 - JustDravee

2023-03-25T07:08:56Z

Will share with Sponsor

Edit: L1 is invalid as there's currently no way to remove a collateral. L4 can be an applied, but the "to avoid liquidation" argument doesn't apply as, if the price is invalid, it wouldn't trigger a liquidation.

Will keep the QA Report as valid given the unique lows but the size and amount of signal isn't enough for grade-A. Marking as grade-B

#1 - c4-judge

2023-05-03T03:43:07Z

JustDravee marked the issue as grade-b

#2 - JustDravee

2023-05-09T15:55:28Z

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter