Skip to content
This repository has been archived by the owner on Apr 28, 2024. It is now read-only.

HHK - Wrong accLoanRatePerSeconds in repay() can lead to underflow #119

Open
sherlock-admin opened this issue Oct 23, 2023 · 16 comments
Open
Assignees
Labels
Has Duplicates A valid issue with 1+ other issues describing the same vulnerability Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed

Comments

@sherlock-admin
Copy link
Contributor

sherlock-admin commented Oct 23, 2023

HHK

medium

Wrong accLoanRatePerSeconds in repay() can lead to underflow

Summary

When a Lender call the repay() function of the LiquidityBorrowingManager contract to do an emergency liquidity restoration using isEmergency = true, the borrowingStorage.accLoanRatePerSeconds is updated if the borrowing position hasn't been fully closed.

The computation is made in sort that the missing collateral can be computed again later so we don't loose the missing fees in case someone take over or the borrower decide to reimburse the fees.

But this computation is wrong and can lead to underflow.

Vulnerability Detail

Because the repay() function resets the dailyRateCollateralBalance to 0 when the lender call didn't fully close the position. We want to be able to compute the missing collateral again.

To do so we substract the percentage of collateral not paid to the accLoanRatePerSeconds so on the next call we will be adding extra second of fees that will allow the contract to compute the missing collateral.

The problem lies in the fact that we compute a percentage using the borrowed amount left instead of the initial borrow amount causing the percentage to be higher. In practice this do allows the contract to recompute the missing collateral.

But in the case of the missing collateralBalance or removedAmt being very high (ex: multiple days not paid or the loan removed was most of the position's liquidity) we might end up with a percentage higher than the accLoanRatePerSeconds which will cause an underflow.

In case of an underflow the call will revert and the lender will not be able to get his tokens back.

Consider this POC that can be copied and pasted in the test files (replace all tests and just keep the setup & NFT creation):

it("Updated accRate is incorrect", async () => {
        const amountWBTC = ethers.utils.parseUnits("0.05", 8); //token0
        let deadline = (await time.latest()) + 60;
        const minLeverageDesired = 50;
        const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);

        const loans = [
            {
                liquidity: nftpos[3].liquidity,
                tokenId: nftpos[3].tokenId,
            },
            {
                liquidity: nftpos[5].liquidity,
                tokenId: nftpos[5].tokenId,
            },
        ];

        const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
            swapTarget: constants.AddressZero,
            swapAmountInDataIndex: 0,
            maxGasForCall: 0,
            swapData: swapData,
        };

        const borrowParams = {
            internalSwapPoolfee: 500,
            saleToken: WETH_ADDRESS,
            holdToken: WBTC_ADDRESS,
            minHoldTokenOut: amountWBTC,
            maxCollateral: maxCollateralWBTC,
            externalSwap: swapParams,
            loans: loans,
        };

        //borrow tokens
        await borrowingManager.connect(bob).borrow(borrowParams, deadline);

        await time.increase(3600 * 72); //72h so 2 days of missing collateral
        deadline = (await time.latest()) + 60;

        const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);

        let repayParams = {
            isEmergency: true,
            internalSwapPoolfee: 0,
            externalSwap: swapParams,
            borrowingKey: borrowingKey,
            swapSlippageBP1000: 0,
        };

        const oldBorrowingInfo = await borrowingManager.borrowingsInfo(borrowingKey);
        const dailyRateCollateral = await borrowingManager.checkDailyRateCollateral(borrowingKey);

        //Alice emergency repay but it reverts with 2 days of collateral missing
        await expect(borrowingManager.connect(alice).repay(repayParams, deadline)).to.be.revertedWithPanic();
    });

Impact

Medium. Lender might not be able to use isEmergency on repay() and will have to do a normal liquidation if he want his liquidity back.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L613

Tool used

Manual Review

Recommendation

Consider that when a lender do an emergency liquidity restoration they give up on their collateral missing and so use the initial amount in the computation instead of borrowed amount left.

borrowingStorage.accLoanRatePerSeconds =
                    holdTokenRateInfo.accLoanRatePerSeconds -
                    FullMath.mulDiv(
                        uint256(-collateralBalance),
                        Constants.BP,
                        borrowing.borrowedAmount + removedAmt //old amount
                    );
@github-actions github-actions bot added Medium A valid Medium severity issue Has Duplicates A valid issue with 1+ other issues describing the same vulnerability labels Oct 26, 2023
@fann95 fann95 added Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed labels Oct 30, 2023
@fann95
Copy link

fann95 commented Oct 30, 2023

I think you are wrong in your conclusions. Try playing with emergency close tests.
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/test/WagmiLeverageTests.ts#L990

The calculation must be made with the new borrowing.borrowedAmount, since in the future it will be used in the next calculation. The commission debt should be maintained and increase over time, and in your case, it is decreasing.

@fann95
Copy link

fann95 commented Oct 30, 2023

this is your option
Screenshot from 2023-10-30 11-38-00

and this is my option
Screenshot from 2023-10-30 11-38-15

In your version, the fee debt is reduced, although no one is paying it off.

@cvetanovv cvetanovv removed the Medium A valid Medium severity issue label Oct 30, 2023
@sherlock-admin sherlock-admin changed the title Rough Pearl Wombat - Wrong accLoanRatePerSeconds in repay() can lead to underflow HHK - Wrong accLoanRatePerSeconds in repay() can lead to underflow Oct 30, 2023
@sherlock-admin sherlock-admin added Non-Reward This issue will not receive a payout and removed Has Duplicates A valid issue with 1+ other issues describing the same vulnerability labels Oct 30, 2023
@HHK-ETH
Copy link

HHK-ETH commented Oct 30, 2023

Escalate

While using the new borrowing.borrowedAmount make sense to keep the missing collateral balance it will lead to underflow if the holdTokenRateInfo.accLoanRatePerSeconds is too low (ex: the first user to borrow so accLoanRatePerSeconds started at the same time).

To find the missing collateral balance with a smaller borrowed amount, the only solution is to increase the percentage. But like I said, if you increase the percentage you might end up underflowing as the accLoanRatePerSeconds could be smaller.

Here is a simple example:

  • You have 10 tokens of collateral missing and 200 tokens total and we remove 50 tokens. The rate is 1% daily.
  • 10 / 200 * 100 = 5% is the percentage value of the missing collateral compared to the amount borrowed.
  • Since we were the first to borrow accLoanRatePerSeconds is pretty close to the missing collateral percentage, minus 1 day (user deposits at least 1 day of collateral when borrowing). 200*1/100 = 2 so the accLoanRatePerSeconds is 10+2 / 200 * 100 = 6%.
  • In the contract what we do instead is 10 / (200-50) * 100 = 6.66%. We do this because we want to be able to find the missing collateral using a smaller balance during the next call as the state is updated.
  • The substraction underflow because 6.66% > 6%.

I did play with the emergency close tests. Here I reused it and added comments:

it("emergency repay will be successful for PosManNFT owner if the collateral is depleted", async () => {
        let debt: LiquidityBorrowingManager.BorrowingInfoExtStructOutput[] =
            await borrowingManager.getBorrowerDebtsInfo(bob.address);

        await time.increase(debt[1].estimatedLifeTime.toNumber() + 3600 * 36); //add 36 hours as an example

        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address); //update debt variable

        let borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 1);

        let swap_params = ethers.utils.defaultAbiCoder.encode(
            ["address", "address", "uint256", "uint256"],
            [constants.AddressZero, constants.AddressZero, 0, 0]
        );
        swapData = swapIface.encodeFunctionData("swap", [swap_params]);

        let swapParams: ApproveSwapAndPay.SwapParamsStruct = {
            swapTarget: constants.AddressZero,
            swapAmountInDataIndex: 0,
            maxGasForCall: 0,
            swapData: swapData,
        };

        let params: LiquidityBorrowingManager.RepayParamsStruct = {
            isEmergency: true, //emergency
            internalSwapPoolfee: 0,
            externalSwap: swapParams,
            borrowingKey: borrowingKey,
            swapSlippageBP1000: 0,
        };

        //console.log(debt);
        let loans: LiquidityManager.LoanInfoStructOutput[] = await borrowingManager.getLoansInfo(borrowingKey);
        expect(loans.length).to.equal(3);

        let tokenRate = await borrowingManager.getHoldTokenDailyRateInfo(
            debt[1].info.saleToken,
            debt[1].info.holdToken
        );

        // We're going to repay alice loan which is [0] in loans array
        const aliceLoanPercentage = loans[0].liquidity
            .mul(100)
            .div(loans[0].liquidity.add(loans[1].liquidity).add(loans[2].liquidity));
        console.log("Alice % of total borrow: " + aliceLoanPercentage); //Alice liquidity is around 26% of the borrowing position

        //Here we do like the smart contract does, try to see how much accLoanRatePerSeconds we need to remove to find back our missing collateral
        //without removedAmount we use the new borrowed amount (current implementation)
        let accToRemoveWithoutRemovedAmount = debt[1].collateralBalance
            .mul(-1)
            .mul(10_000)
            .div(debt[1].info.borrowedAmount.mul(100 - aliceLoanPercentage.toNumber()).div(100));
        //with removedAmount (proposed solution), we forfeit alice part of missing collateral since she emergency withdraw
        let accToRemoveWithRemovedAmount = debt[1].collateralBalance
            .mul(-1)
            .mul(10_000)
            .div(debt[1].info.borrowedAmount);

        //this is the accLoanRatePerSeconds that we're going to substract from
        console.log("% to remove: " + accToRemoveWithoutRemovedAmount);
        console.log("accLoanRatePerSeconds: " + tokenRate.holdTokenRateInfo.accLoanRatePerSeconds);

        //for now even after 36hours we're still less than accLoanRatePerSeconds so no underflow
        console.log("% to remove is less than accLoanRatePerSeconds");
        expect(tokenRate.holdTokenRateInfo.accLoanRatePerSeconds.gt(accToRemoveWithoutRemovedAmount));

        //let's add 12 more hours
        await time.increase(3600 * 12); //add 12 hours

        tokenRate = await borrowingManager.getHoldTokenDailyRateInfo(debt[1].info.saleToken, debt[1].info.holdToken); //update token rate
        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address); //update debt variable
        accToRemoveWithoutRemovedAmount = debt[1].collateralBalance
            .mul(-1)
            .mul(10_000)
            .div(debt[1].info.borrowedAmount.mul(100 - aliceLoanPercentage.toNumber()).div(100));
        accToRemoveWithRemovedAmount = debt[1].collateralBalance.mul(-1).mul(10_000).div(debt[1].info.borrowedAmount);

        console.log("% to remove: " + accToRemoveWithoutRemovedAmount);
        console.log("accLoanRatePerSeconds: " + tokenRate.holdTokenRateInfo.accLoanRatePerSeconds);

        //the accToRemoveWithoutRemovedAmount is now greater than accLoanRatePerSeconds
        console.log("% to remove is greater than accLoanRatePerSeconds thus it will underflow");
        expect(tokenRate.holdTokenRateInfo.accLoanRatePerSeconds.lt(accToRemoveWithoutRemovedAmount));

        let deadline = (await time.latest()) + 60;

        //this is going to revert with underflow
        await expect(borrowingManager.connect(alice).repay(params, deadline)).to.be.reverted;
        /**
        expect(await borrowingManager.getLenderCreditsCount(nftpos[0].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[1].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[2].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[3].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[4].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[5].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(2);

        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address);
        //console.log(debt);
        loans = await borrowingManager.getLoansInfo(borrowingKey);
        expect(loans.length).to.equal(2);
        
        await time.increase(100);
        deadline = (await time.latest()) + 60;
        await expect(borrowingManager.connect(bob).repay(params, deadline))
            .to.emit(borrowingManager, "EmergencyLoanClosure")
            .withArgs(bob.address, bob.address, borrowingKey);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[0].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[1].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[2].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[3].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[4].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[5].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(2);
        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address);
        //console.log(debt);
        loans = await borrowingManager.getLoansInfo(borrowingKey);
        expect(loans.length).to.equal(1);

        await time.increase(100);
        deadline = (await time.latest()) + 60;
        await expect(borrowingManager.connect(owner).repay(params, deadline))
            .to.emit(borrowingManager, "EmergencyLoanClosure")
            .withArgs(bob.address, owner.address, borrowingKey);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[0].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[1].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[2].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[3].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[4].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[5].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(1);*/
    });

In this example Alice represents only 26% of the borrowed amount and the accLoanRatePerSeconds started before this loan so a longer time is needed (here I did 48hours) but if the user calling emergency repay has a much higher percentage or it's the first borrow it wouldn't need as much time.

I understand that my solution might not be what you want, but the underflow problem do exist.

@sherlock-admin2
Copy link
Contributor

Escalate

While using the new borrowing.borrowedAmount make sense to keep the missing collateral balance it will lead to underflow if the holdTokenRateInfo.accLoanRatePerSeconds is too low (ex: the first user to borrow so accLoanRatePerSeconds started at the same time).

To find the missing collateral balance with a smaller borrowed amount, the only solution is to increase the percentage. But like I said, if you increase the percentage you might end up underflowing as the accLoanRatePerSeconds didn't change.

Here is a simple example:

  • You have 10 tokens of collateral missing and 200 tokens total and we remove 50 tokens. The rate is 1% daily.
  • 10 / 200 * 100 = 5% is the percentage value of the missing collateral compared to the amount borrowed.
  • Since we were the first to borrow accLoanRatePerSeconds is pretty close to the missing collateral percentage, minus 1 day (user deposits at least 1 day of collateral when borrowing). 200*1/100 = 2 so the accLoanRatePerSeconds is 10+2 / 200 * 100 = 6%.
  • In the contract what we do instead is 10 / (200-50) * 100 = 6.66%. We do this because we want to be able to find the missing collateral using a smaller balance during the next call as the state is updated.
  • The substraction underflow because 6% > 5%.

I did play with the emergency close tests. Here I reused it and added comments:

it("emergency repay will be successful for PosManNFT owner if the collateral is depleted", async () => {
        let debt: LiquidityBorrowingManager.BorrowingInfoExtStructOutput[] =
            await borrowingManager.getBorrowerDebtsInfo(bob.address);

        await time.increase(debt[1].estimatedLifeTime.toNumber() + 3600 * 36); //add 36 hours as an example

        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address); //update debt variable

        let borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 1);

        let swap_params = ethers.utils.defaultAbiCoder.encode(
            ["address", "address", "uint256", "uint256"],
            [constants.AddressZero, constants.AddressZero, 0, 0]
        );
        swapData = swapIface.encodeFunctionData("swap", [swap_params]);

        let swapParams: ApproveSwapAndPay.SwapParamsStruct = {
            swapTarget: constants.AddressZero,
            swapAmountInDataIndex: 0,
            maxGasForCall: 0,
            swapData: swapData,
        };

        let params: LiquidityBorrowingManager.RepayParamsStruct = {
            isEmergency: true, //emergency
            internalSwapPoolfee: 0,
            externalSwap: swapParams,
            borrowingKey: borrowingKey,
            swapSlippageBP1000: 0,
        };

        //console.log(debt);
        let loans: LiquidityManager.LoanInfoStructOutput[] = await borrowingManager.getLoansInfo(borrowingKey);
        expect(loans.length).to.equal(3);

        let tokenRate = await borrowingManager.getHoldTokenDailyRateInfo(
            debt[1].info.saleToken,
            debt[1].info.holdToken
        );

        // We're going to repay alice loan which is [0] in loans array
        const aliceLoanPercentage = loans[0].liquidity
            .mul(100)
            .div(loans[0].liquidity.add(loans[1].liquidity).add(loans[2].liquidity));
        console.log(aliceLoanPercentage); //Alice liquidity is around 26% of the borrowing position

        //Here we do like the smart contract does, try to see how much accLoanRatePerSeconds we need to remove to find back our missing collateral
        //without removedAmount (proposed solution), we forfeit alice part of missing collateral since she emergency withdraw
        let accToRemoveWithoutRemovedAmount = debt[1].collateralBalance
            .mul(-1)
            .mul(10_000)
            .div(debt[1].info.borrowedAmount.mul(100 - aliceLoanPercentage.toNumber()).div(100));
        //with removedAmount (current implmentation)
        let accToRemoveWithRemovedAmount = debt[1].collateralBalance
            .mul(-1)
            .mul(10_000)
            .div(debt[1].info.borrowedAmount);

        //this is the accLoanRatePerSeconds that we're going to substract from
        console.log(tokenRate.holdTokenRateInfo.accLoanRatePerSeconds);

        //for now even after 36hours we're still less than accLoanRatePerSeconds so no underflow
        expect(tokenRate.holdTokenRateInfo.accLoanRatePerSeconds.gt(accToRemoveWithRemovedAmount));

        //let's add 12 more hours
        await time.increase(debt[1].estimatedLifeTime.toNumber() + 3600 * 12); //add 12 hours

        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address); //update debt variable
        accToRemoveWithoutRemovedAmount = debt[1].collateralBalance
            .mul(-1)
            .mul(10_000)
            .div(debt[1].info.borrowedAmount.mul(100 - aliceLoanPercentage.toNumber()).div(100));
        accToRemoveWithRemovedAmount = debt[1].collateralBalance.mul(-1).mul(10_000).div(debt[1].info.borrowedAmount);

        //the accToRemoveWithRemovedAmount is now greater than accLoanRatePerSeconds
        expect(tokenRate.holdTokenRateInfo.accLoanRatePerSeconds.lt(accToRemoveWithRemovedAmount));

        let deadline = (await time.latest()) + 60;

        //this is going to revert with underflow
        await expect(borrowingManager.connect(alice).repay(params, deadline))
            .to.emit(borrowingManager, "EmergencyLoanClosure")
            .withArgs(bob.address, alice.address, borrowingKey);

        expect(await borrowingManager.getLenderCreditsCount(nftpos[0].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[1].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[2].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[3].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[4].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[5].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(2);

        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address);
        //console.log(debt);
        loans = await borrowingManager.getLoansInfo(borrowingKey);
        expect(loans.length).to.equal(2);
        /**
        await time.increase(100);
        deadline = (await time.latest()) + 60;
        await expect(borrowingManager.connect(bob).repay(params, deadline))
            .to.emit(borrowingManager, "EmergencyLoanClosure")
            .withArgs(bob.address, bob.address, borrowingKey);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[0].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[1].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[2].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[3].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[4].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[5].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(2);
        debt = await borrowingManager.getBorrowerDebtsInfo(bob.address);
        //console.log(debt);
        loans = await borrowingManager.getLoansInfo(borrowingKey);
        expect(loans.length).to.equal(1);

        await time.increase(100);
        deadline = (await time.latest()) + 60;
        await expect(borrowingManager.connect(owner).repay(params, deadline))
            .to.emit(borrowingManager, "EmergencyLoanClosure")
            .withArgs(bob.address, owner.address, borrowingKey);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[0].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[1].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[2].tokenId)).to.be.equal(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[3].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[4].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getLenderCreditsCount(nftpos[5].tokenId)).to.be.gt(0);
        expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(1);*/
    });

In this example Alice represents only 26% of the borrowed amount and the accLoanRatePerSeconds started before this loan so a longer time is needed (here I did 48hours) but if the user calling emergency repay has a much higher percentage or it's the first borrow it wouldn't need as much time.

I understand that my solution might not be what you want, but the underflow problem do exist.

The escalation could not be created because you are not exceeding the escalation threshold.

You can view the required number of additional valid issues/judging contest payouts in your Profile page,
in the Sherlock webapp.

@fann95
Copy link

fann95 commented Oct 31, 2023

debt = await borrowingManager.getBorrowerDebtsInfo(bob.address); //update debt variable

tokenRate also needs to be updated like the debt variable

@HHK-ETH
Copy link

HHK-ETH commented Oct 31, 2023

Right, my bad sorry wrote the test quickly. Updated my comment with the new token rate, repay still underflow.

@HHK-ETH
Copy link

HHK-ETH commented Oct 31, 2023

@fann95 @cvetanovv sorry to bother but would appreciate if you could read the escalation comment again and run the 2 POCs provided (comment and initial finding) before escalation period ends.

The repay() reverts with underflow when increasing the missing collateral to a certain amount (just a few days in the examples given). I tried to explain that it comes from the accLoanRatePerSeconds calculation. I'll happily take the blame if I'm wrong and will be sorry for the time wasted but if I'm right this is problably something we want to fix or lenders might not be able to call repay and will have to count on liquidators.

@giraffe0x
Copy link

giraffe0x commented Nov 1, 2023

issue 195 is also similar. I think both issues should be reconsidered to be valid.

This bug is easily replicated using protocol's own default test file WagmiLeverageTests.ts by changing line 1040 to 100_000 seconds (~1 day)

Expecting an under-collateralized loan to not be liquidated in ~1 day is not unrealistic, after which the failure of emergency mode represents a significant impact to a critical protocol function.

@Czar102
Copy link

Czar102 commented Nov 2, 2023

@fann95 @cvetanovv could you take a look at this and #195?

@cvetanovv
Copy link
Collaborator

Should be duplicated and rather seem valid to me

@Czar102
Copy link

Czar102 commented Nov 3, 2023

@fann95 could you post your opinion on this issue? It seems to be valid.

@fann95
Copy link

fann95 commented Nov 3, 2023

I looked at this problem one more and realized: yes, this is a mistake. When a liquidity provider withdraws its liquidity, it also surrenders its fee debt, so the debt must be reduced in proportion to the reduction in liquidity.

@fann95 fann95 added High A valid High severity issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed and removed Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed labels Nov 3, 2023
@sherlock-admin sherlock-admin added Reward A payout will be made for this issue and removed Non-Reward This issue will not receive a payout labels Nov 3, 2023
@sherlock-admin sherlock-admin added Reward A payout will be made for this issue and removed Non-Reward This issue will not receive a payout labels Nov 3, 2023
@fann95
Copy link

fann95 commented Nov 3, 2023

I came to an interesting conclusion: the borrowingStorage.accLoanRatePerSeconds does not need to be recalculated. It should remain the same as it was, and the borrowed amount should be decreased only. So, accordingly, the fee debt will be reduced during a future recalculation.
Fixed: RealWagmi/wagmi-leverage@4d355d8

@fann95 fann95 self-assigned this Nov 3, 2023
@Czar102
Copy link

Czar102 commented Nov 4, 2023

@fann95 may I ask why do you consider this a high severity issue? It seems the watson classified this as medium severity.

@fann95 fann95 added Medium A valid Medium severity issue and removed High A valid High severity issue labels Nov 6, 2023
@Czar102
Copy link

Czar102 commented Nov 6, 2023

Agree with medium severity. Will be accepting the escalation.

@Evert0x Evert0x reopened this Nov 8, 2023
@sherlock-admin sherlock-admin added the Has Duplicates A valid issue with 1+ other issues describing the same vulnerability label Nov 13, 2023
@IAm0x52
Copy link

IAm0x52 commented Nov 16, 2023

Fix looks good. borrowingStorage.accLoanRatePerSeconds does not need to be recalculated and so the calculation has been removed.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Has Duplicates A valid issue with 1+ other issues describing the same vulnerability Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed
Projects
None yet
Development

No branches or pull requests

9 participants