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

The amount of debt removed during liquidation may be worth more than the account's collateral #1620

Open
code423n4 opened this issue Aug 4, 2023 · 3 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working H-03 primary issue Highest quality submission among a set of duplicates selected for report This submission will be included/highlighted in the audit report sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L822

Vulnerability details

Impact

The contract decreases user's debts but may not take the full worth in collateral from the user, leading to the contract losing potential funds from the missing collateral.

Proof of concept

During the liquidate() function call, the function _updateBorrowAndCollateralShare() is eventually invoked. This function liquidates a user's debt and collateral based on the value of the collateral they own.

In particular, the equivalent amount of debt, availableBorrowPart is calculated from the user's collateral on line 225 through the computeClosingFactor() function call.

Then, an additional fee through the liquidationBonusAmount is applied to the debt, which is then compared to the user's debt on line 240. The minimum of the two is assigned borrowPart, which intuitively means the maximum amount of debt that can be removed from the user's debt.

borrowPart is then increased by a bonus through liquidationMultiplier, and then converted to generate collateralShare, which represents the amount of collateral equivalent in value to borrowPart (plus some fees and bonus).

This new collateralShare may be more than the collateral that the user owns. In that case, the collateralShare is simply decreased to the user's collateral.

collateralShare is then removed from the user's collateral.

The problem lies in that although the collateralShare is equivalent to the borrowPart, or the debt removed from the user's account, it could be worth more than the collateral that the user owns in the first place. Hence, the contract loses out on funds, as debt is removed for less than it is actually worth.

To demonstrate, we provide a runnable POC.

Preconditions

... 
if (collateralShare > userCollateralShare[user]) {
        require(false, "collateralShare and borrowPart not worth the same"); //@audit add this line
        collateralShare = userCollateralShare[user];
    }
    userCollateralShare[user] -= collateralShare;
...

Add the require statement to line 261. This require statement essentially reverts the contract when the if condition satisfies. The if condition holds true when the collateralShare is greater that the user's collateral, which is the target bug.

Once the changes have been made, add the following test into the singularity.test.ts test in tapioca-bar-audit/test

Code

it('POC', async () => {
            const {
                usdc,
                wbtc,
                yieldBox,
                wbtcDepositAndAddAsset,
                usdcDepositAndAddCollateralWbtcSingularity,
                eoa1,
                approveTokensAndSetBarApproval,
                deployer,
                wbtcUsdcSingularity,
                multiSwapper,
                wbtcUsdcOracle,
                __wbtcUsdcPrice,
            } = await loadFixture(register);

            const assetId = await wbtcUsdcSingularity.assetId();
            const collateralId = await wbtcUsdcSingularity.collateralId();
            const wbtcMintVal = ethers.BigNumber.from((1e8).toString()).mul(1);
            const usdcMintVal = wbtcMintVal
                .mul(1e10)
                .mul(__wbtcUsdcPrice.div((1e18).toString()));

            // We get asset
            await wbtc.freeMint(wbtcMintVal);
            await usdc.connect(eoa1).freeMint(usdcMintVal);

            // We approve external operators
            await approveTokensAndSetBarApproval();
            await approveTokensAndSetBarApproval(eoa1);

            // We lend WBTC as deployer
            await wbtcDepositAndAddAsset(wbtcMintVal);
            expect(
                await wbtcUsdcSingularity.balanceOf(deployer.address),
            ).to.be.equal(await yieldBox.toShare(assetId, wbtcMintVal, false));

            // We deposit USDC collateral
            await usdcDepositAndAddCollateralWbtcSingularity(usdcMintVal, eoa1);
            expect(
                await wbtcUsdcSingularity.userCollateralShare(eoa1.address),
            ).equal(await yieldBox.toShare(collateralId, usdcMintVal, false));

            // We borrow 74% collateral, max is 75%

            console.log("Collateral amt: ", usdcMintVal);
            const wbtcBorrowVal = usdcMintVal
                .mul(74)
                .div(100)
                .div(__wbtcUsdcPrice.div((1e18).toString()))
                .div(1e10);
            console.log("WBTC borrow val: ", wbtcBorrowVal);
            console.log("[$] Original price: ", __wbtcUsdcPrice.div((1e18).toString()));
            await wbtcUsdcSingularity
                .connect(eoa1)
                .borrow(eoa1.address, eoa1.address, wbtcBorrowVal.toString());
            await yieldBox
                .connect(eoa1)
                .withdraw(
                    assetId,
                    eoa1.address,
                    eoa1.address,
                    wbtcBorrowVal,
                    0,
                );

            const data = new ethers.utils.AbiCoder().encode(['uint256'], [1]);
            // Can't liquidate
            await expect(
                wbtcUsdcSingularity.liquidate(
                    [eoa1.address],
                    [wbtcBorrowVal],
                    multiSwapper.address,
                    data,
                    data,
                ),
            ).to.be.reverted;
            console.log("Price Drop: 120%");
            const priceDrop = __wbtcUsdcPrice.mul(40).div(100);
            await wbtcUsdcOracle.set(__wbtcUsdcPrice.add(priceDrop));
            await wbtcUsdcSingularity.updateExchangeRate()
            console.log("Running liquidation... ");
            await expect(
                wbtcUsdcSingularity.liquidate(
                    [eoa1.address],
                    [wbtcBorrowVal],
                    multiSwapper.address,
                    data,
                    data,
                ),
            ).to.be.revertedWith("collateralShare and borrowPart not worth the same");
            console.log("[*] Reverted with reason: collateralShare and borrowPart not worth the same [Bug]");
            //console.log("Collateral Share after liquidation: ", (await wbtcUsdcSingularity.userCollateralShare(eoa1.address)));
            //console.log("Borrow part after liquidation: ", (await wbtcUsdcSingularity.userBorrowPart(eoa1.address)));
        });

Expected Result

Collateral amt:  BigNumber { value: "10000000000000000000000" }
WBTC borrow val:  BigNumber { value: "74000000" }
[$] Original price:  BigNumber { value: "10000" }
Price Drop: 120%
Running liquidation... 
[*] Reverted with reason: collateralShare and borrowPart not worth the same [Bug]
      ✔ POC (2289ms)

As demonstrated, the function call reverts due to the require statement added in the preconditions.

Recommended Mitigation

One potential mitigation for this issue would be to calculate the borrowPart depending on the existing users' collateral factoring in the fees and bonuses. The collateralShare with the fees and bonuses should not exceed the user's collateral.

Assessed type

Math

@code423n4 code423n4 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Aug 4, 2023
code423n4 added a commit that referenced this issue Aug 4, 2023
@c4-pre-sort c4-pre-sort added the primary issue Highest quality submission among a set of duplicates label Aug 9, 2023
@c4-pre-sort
Copy link

minhquanym marked the issue as primary issue

@c4-sponsor
Copy link

cryptotechmaker (sponsor) confirmed

@c4-sponsor c4-sponsor added the sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") label Sep 6, 2023
@c4-judge
Copy link

dmvt marked the issue as selected for report

@c4-judge c4-judge added the selected for report This submission will be included/highlighted in the audit report label Sep 29, 2023
@C4-Staff C4-Staff added the H-03 label Oct 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working H-03 primary issue Highest quality submission among a set of duplicates selected for report This submission will be included/highlighted in the audit report sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
Projects
None yet
Development

No branches or pull requests

5 participants