code-423n4 / 2024-06-badger-findings

7 stars 5 forks source link

_doSwapChecks might fail to validate the expectedMinOut constraint. #43

Closed howlbot-integration[bot] closed 3 months ago

howlbot-integration[bot] commented 4 months ago

Lines of code

https://github.com/code-423n4/2024-06-badger/blob/9173558ee1ac8a78a7ae0a39b97b50ff0dd9e0f8/ebtc-protocol/packages/contracts/contracts/LeverageMacroBase.sol#L485-L497

Vulnerability details

Impact

Detailed description of the impact of this finding.

LeverageMacroBase._doSwap() is used to swap one token for another and it has a post check to ensure that the output token is no less than expectedMinOut. However, it simply checks the balance of the output token, which is error-prone since there might be some non-zero balance right before the swap.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

First, LeverageMacroBase._doSwap() is used to swap one token for another and it has a post check to ensure that the output token is no less than expectedMinOut.

https://github.com/code-423n4/2024-06-badger/blob/9173558ee1ac8a78a7ae0a39b97b50ff0dd9e0f8/ebtc-protocol/packages/contracts/contracts/LeverageMacroBase.sol#L448-L481

Second, LeverageMacroBase._doSwapChecks() does the check, but only against the balance of the output token, not the actual output amount from the swap operation.

https://github.com/code-423n4/2024-06-badger/blob/9173558ee1ac8a78a7ae0a39b97b50ff0dd9e0f8/ebtc-protocol/packages/contracts/contracts/LeverageMacroBase.sol#L485-L497

Third, below, we show the following:

  1. Before the swap, the router has stETh: 91019547657512117.
  2. The swap will return stEth in the amount of : 17994749596122777329
  3. after the swap, the router has a balance of: 18085769143780289446
  4. expectedMinOut = 18000000000000000000
  5. Although the swap output amount is smaller than expectedMinOut, the check will fail to detect this since it will only check the final balance of stEth, which is indeed greater than expectedMinOut.

I revised file LeverageZaps.t.sol. Please run forge test --match-test testOpenCdp1 -vv.

 // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import {EbtcZapRouter} from "../src/EbtcZapRouter.sol";
import {IERC20} from "@ebtc/contracts/Dependencies/IERC20.sol";
import {ZapRouterBaseInvariants} from "./ZapRouterBaseInvariants.sol";
import {IERC3156FlashLender} from "@ebtc/contracts/Interfaces/IERC3156FlashLender.sol";
import {IBorrowerOperations, IPositionManagers} from "@ebtc/contracts/LeverageMacroBase.sol";
import {ICdpManagerData} from "@ebtc/contracts/Interfaces/ICdpManager.sol";
import {IEbtcZapRouter} from "../src/interface/IEbtcZapRouter.sol";
import {IEbtcLeverageZapRouter} from "../src/interface/IEbtcLeverageZapRouter.sol";
import {IEbtcZapRouterBase} from "../src/interface/IEbtcZapRouterBase.sol";
import {IWstETH} from "../src/interface/IWstETH.sol";
import {IWrappedETH} from "../src/interface/IWrappedETH.sol";

interface ICdpCdps {
    function Cdps(bytes32) external view returns (ICdpManagerData.Cdp memory);
}

contract LeverageZaps is ZapRouterBaseInvariants {
    address user1 = vm.addr(userPrivateKey);
    uint256 price;

    function setUp() public override {
        super.setUp();
        price = priceFeedMock.fetchPrice(); // price of stETH in terms of eBTC
    }

    function testOpenCdp1() public{

        uint256 amount = 10000 ether;
        uint256 marginAmount = 12.3 ether;
        uint256 debt = 1.34e18;
        uint256 flAmount = (debt * 1e18) / price;

        console2.log("ccccccccccccccccccccccccc");
        console2.log("price: ", price);
        console2.log("marginAmount: ", marginAmount);
        console2.log("debt: ", debt);
        console2.log("flAmount: ", flAmount);

        seedActivePool();

        console2.log("$$$$$$$$$$$$$$$$$$$$$$$ \n \n");

        vm.deal(user1, amount);
        vm.startPrank(user1);
        collateral.deposit{value: amount}();
        collateral.approve(address(testWstEth), type(uint256).max);
        IWstETH(testWstEth).wrap(marginAmount); 
        IERC20(testWstEth).approve(address(leverageZapRouter), type(uint256).max);
        vm.stopPrank();

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user1);
        vm.startPrank(user1);
          leverageZapRouter.openCdpWithWstEth(
                debt, // Debt amount
                bytes32(0),
                bytes32(0),
                flAmount,
                marginAmount, // Margin amount
                (flAmount + IWstETH(testWstEth).getStETHByWstETH(marginAmount)) * 9970 / 10000, // debt equivalent + margin 
                abi.encode(pmPermit),
                _getOpenCdpTradeData(debt, flAmount)
        );
        vm.stopPrank();
        console2.log("collateral balance of router: ", collateral.balanceOf(address(leverageZapRouter)));
        console2.log("collateral balance of user: ", collateral.balanceOf(address(user1)));
        console2.log("eBTC balance of user: ", eBTCToken.balanceOf(address(user1)));

    }

    function seedActivePool() private returns (address) {
        address whale = vm.addr(0xabc456);
        _dealCollateralAndPrepForUse(whale); // 1000 ether collateral

        vm.startPrank(whale);
        collateral.approve(address(borrowerOperations), type(uint256).max);

        // Seed AP
        console2.log("before whale collateral balance: ", collateral.balanceOf(whale)); 

        borrowerOperations.openCdp(2.12e18, bytes32(0), bytes32(0), 617 ether); // get 2e18 Ebtc with 600 sETH
        console2.log("after whale collateral balance: ", collateral.balanceOf(whale));

        console2.log("\n Cdps for whale...");
        printCdps(whale);

        // Seed mock dex
        eBTCToken.transfer(address(mockDex), 2e18); // now mockDEX has 2e18 EBtc

        vm.stopPrank();

        // Give stETH to mock dex
        mockDex.setPrice(priceFeedMock.fetchPrice());
        vm.deal(address(mockDex), type(uint96).max);
        vm.prank(address(mockDex));
        collateral.deposit{value: 10000 ether}();   // now mockDEX has 10000 stETH
    }

    function createPermit(
        address user
    ) private returns (IEbtcZapRouter.PositionManagerPermit memory pmPermit) {
        uint _deadline = (block.timestamp + deadline);
        IPositionManagers.PositionManagerApproval _approval = IPositionManagers
            .PositionManagerApproval
            .OneTime;

        vm.startPrank(user);

        // Generate signature to one-time approve zap
        bytes32 digest = _generatePermitSignature(user, address(leverageZapRouter), _approval, _deadline);
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest);

        pmPermit = IEbtcZapRouterBase.PositionManagerPermit(_deadline, v, r, s);

        vm.stopPrank();
    }

    function _debtToCollateral(uint256 _debt) public returns (uint256) {
        uint256 price = priceFeedMock.fetchPrice(); // price of stETH in terms of eBTC
        return (_debt * 1e18) / price;
    }

    uint256 internal constant SLIPPAGE_PRECISION = 1e4;
    /// @notice Collateral buffer used to account for slippage and fees (zap fee included)
    /// 9970 = 0.30%
    uint256 internal constant COLLATERAL_BUFFER = 9970;

    function createLeveragedPosition(MarginType marginType) private returns (address user, bytes32 expectedCdpId) {
        user = vm.addr(userPrivateKey);

        console2.log("createLeveragePosition...");
        uint256 _debt = 1.34e18;
        uint256 flAmount = _debtToCollateral(_debt);
        uint256 marginAmount = 120.3 ether;
        console2.log("_debt: ", _debt);
        console2.log("flAmount: ", flAmount);
        console2.log("marginAmount: ", marginAmount);

        if (marginType == MarginType.stETH) {
            _dealCollateralAndPrepForUse(user);
            vm.prank(user);
            collateral.approve(address(leverageZapRouter), type(uint256).max);
        } else if (marginType == MarginType.wstETH) {
            _dealCollateralAndPrepForUse(user);
            vm.startPrank(user);
            collateral.approve(address(testWstEth), type(uint256).max);
            IWstETH(testWstEth).wrap(collateral.balanceOf(user)); // wrap 5 stETH to WstETH
            IERC20(testWstEth).approve(address(leverageZapRouter), type(uint256).max);
            marginAmount = IWstETH(testWstEth).getWstETHByStETH(marginAmount);
            vm.stopPrank();
        } else if (marginType == MarginType.ETH) {
            vm.deal(user, type(uint96).max);
        } else if (marginType == MarginType.WETH) {
            vm.deal(user, type(uint96).max);
            vm.startPrank(user);
            IWrappedETH(testWeth).deposit{value: marginAmount}();
            IERC20(testWeth).approve(address(leverageZapRouter), type(uint256).max);
            vm.stopPrank();
        } else {
            revert();
        }
        console2.log("new marginAmount: ", marginAmount);

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user);

/*
        // let a random user to set the permit first before it expires
        vm.startPrank(address(1234));
        borrowerOperations.permitPositionManagerApproval(
                user,      // _borrower
                address(leverageZapRouter),   // _positionManger
                IPositionManagers.PositionManagerApproval.OneTime, // _approval
                pmPermit.deadline,
                pmPermit.v,
                pmPermit.r,
                pmPermit.s
            );
        vm.stopPrank();

        vm.warp(block.timestamp+deadline+1); // now the permit has expired
*/

        vm.startPrank(user);

        expectedCdpId = sortedCdps.toCdpId(user, block.number, sortedCdps.nextCdpNonce());

        // Get before balances
        assertEq(
            _openTestCdp(marginType, _debt, flAmount, marginAmount, pmPermit),
            expectedCdpId,
            "CDP ID should match expected value"
        );

        vm.stopPrank();
    }

    function _openTestCdp(
        MarginType marginType,
        uint256 _debt, 
        uint256 _flAmount, 
        uint256 _marginAmount,
        IEbtcZapRouter.PositionManagerPermit memory pmPermit
    ) private returns (bytes32) {
        console2.log("\n _openTEstCdp....");
        if (marginType == MarginType.stETH) {
            return leverageZapRouter.openCdp(
                _debt, // Debt amount
                bytes32(0),
                bytes32(0),
                _flAmount,
                _marginAmount, // Margin amount
                (_flAmount + _marginAmount) * COLLATERAL_BUFFER / SLIPPAGE_PRECISION,
                abi.encode(pmPermit),
                _getOpenCdpTradeData(_debt, _flAmount)
            );
        } else if (marginType == MarginType.wstETH) {
            return leverageZapRouter.openCdpWithWstEth(
                _debt, // Debt amount
                bytes32(0),
                bytes32(0),
                _flAmount,
                _marginAmount, // Margin amount
                (_flAmount + IWstETH(testWstEth).getStETHByWstETH(_marginAmount)) * COLLATERAL_BUFFER / SLIPPAGE_PRECISION,
                abi.encode(pmPermit),
                _getOpenCdpTradeData(_debt, _flAmount)
            );
        } else if (marginType == MarginType.ETH) {
            return leverageZapRouter.openCdpWithEth{value: _marginAmount}(
                _debt, // Debt amount
                bytes32(0),
                bytes32(0),
                _flAmount,
                _marginAmount, // Margin amount
                (_flAmount + _marginAmount) * COLLATERAL_BUFFER / SLIPPAGE_PRECISION,
                abi.encode(pmPermit),
                _getOpenCdpTradeData(_debt, _flAmount)
            );
        } else if (marginType == MarginType.WETH) {
            return leverageZapRouter.openCdpWithWrappedEth(
                _debt, // Debt amount
                bytes32(0),
                bytes32(0),
                _flAmount,
                _marginAmount, // Margin amount
                (_flAmount + _marginAmount) * COLLATERAL_BUFFER / SLIPPAGE_PRECISION,
                abi.encode(pmPermit),
                _getOpenCdpTradeData(_debt, _flAmount)
            );
        } else {
            revert();
        }
    }

    function _getOpenCdpTradeData(uint256 _debt, uint256 expectedMinOut) 
        private returns (IEbtcLeverageZapRouter.TradeData memory) {
        _debt = _debt - (_debt * defaultZapFee / 10000) - 100; // we can only trade the part that is after the fee, let try to trade less 100, in this case, the 100 will be simplyed returned back to the user
        console2.log("defaultZapFee: ", defaultZapFee);
        // expectedMinOut = _debt * 1 ether / price; // basically we sell the eBth that we got 
        expectedMinOut = 18000000000000000000;
        return IEbtcLeverageZapRouter.TradeData({
            performSwapChecks: true,
            expectedMinOut: expectedMinOut,
            exchangeData: abi.encodeWithSelector(
                mockDex.swap.selector,
                address(eBTCToken),
                address(collateral),
                _debt // Debt amount
            ),
            approvalAmount: _debt,
            collValidationBufferBPS: 10500 // 5%
        });
    }

function printCdps(address user) public{
    console2.log("\n Print out the Cdps or user :", user);
     bytes32[] memory userCdps = sortedCdps.getCdpsOf(user);
     for(uint i; i < userCdps.length; i++){
         printCdp(userCdps[i]);
     }
}

function printCdp(bytes32 cdp) public{
    console2.log("cdpId:");
    console2.logBytes32(cdp);
    console2.log("status: ", cdpManager.getCdpStatus(cdp));
    console2.log("debt: ", cdpManager.getCdpDebt(cdp));
    console2.log("stake: ", cdpManager.getCdpStake(cdp));
    console2.log("collShares: ", cdpManager.getCdpCollShares(cdp));
    console2.log("liquidatorRewardShares: ", cdpManager.getCdpLiquidatorRewardShares(cdp));
}

    function test_ZapOpenCdp_WithStEth_LowLeverage() public {
        console2.log("\n aaaaaaaaaaaaaaaaaaaaaaa");
        console2.log("seedActivePool...");
        seedActivePool();

        console2.log("end of SeedActivePool....");
        _before();
        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.stETH);
        _after();

        console2.log("$$$$$$$$$$$$$$$$$$$");

        // Confirm Cdp opened for user
        bytes32[] memory userCdps = sortedCdps.getCdpsOf(user);
        assertEq(userCdps.length, 1, "User should have 1 cdp");

        // Test zap fee
        assertEq(eBTCToken.balanceOf(testFeeReceiver), 1.34e18 * defaultZapFee / 10000); 

        console2.log("user eBTC balance:", eBTCToken.balanceOf(user)); // zero since it has been swapped for stETH

        _checkZapStatusAfterOperation(user);
        _ensureSystemInvariants();
        _ensureZapInvariants();
        printCdps(user);
    }

    function test_ZapOpenCdp_WithWstEth_LowLeverage() public {
            console2.log("\n bbbbbbbbbbbbbbbbbbbbbb");
             console2.log("seedActivePool...");
        seedActivePool();
        console2.log("end of SeedActivePool....\n\n");

        _before();
        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.wstETH);
        _after();

         console2.log("$$$$$$$$$$$$$$$$$$$");

        // Confirm Cdp opened for user
        bytes32[] memory userCdps = sortedCdps.getCdpsOf(user);
        assertEq(userCdps.length, 1, "User should have 1 cdp");

        // Test zap fee
        assertEq(eBTCToken.balanceOf(testFeeReceiver), 1.34e18 * defaultZapFee / 10000); 

        _checkZapStatusAfterOperation(user);
        _ensureSystemInvariants();
        _ensureZapInvariants();
        printCdps(user);
    }

    function test_ZapOpenCdp_WithEth_LowLeverage() public {
        seedActivePool();

        _before();
        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.ETH);
        _after();

        // Confirm Cdp opened for user
        bytes32[] memory userCdps = sortedCdps.getCdpsOf(user);
        assertEq(userCdps.length, 1, "User should have 1 cdp");

        // Test zap fee
        assertEq(eBTCToken.balanceOf(testFeeReceiver), 1e18 * defaultZapFee / 10000); 

        _checkZapStatusAfterOperation(user);
        _ensureSystemInvariants();
        _ensureZapInvariants();
    }

    function test_ZapOpenCdp_WithWrappedEth_LowLeverage() public {
        seedActivePool();

        _before();
        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.WETH);
        _after();

        // Confirm Cdp opened for user
        bytes32[] memory userCdps = sortedCdps.getCdpsOf(user);
        assertEq(userCdps.length, 1, "User should have 1 cdp");

        // Test zap fee
        assertEq(eBTCToken.balanceOf(testFeeReceiver), 1e18 * defaultZapFee / 10000); 

        _checkZapStatusAfterOperation(user);
        _ensureSystemInvariants();
        _ensureZapInvariants();
    }

    function test_ZapCloseCdp_WithStEth_LowLeverage() public {
        seedActivePool();

        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.stETH);

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user);

        (uint256 debt, uint256 collShares) = cdpManager.getSyncedDebtAndCollShares(cdpId);

        assertEq(cdpManager.getCdpStatus(cdpId), uint256(ICdpManagerData.Status.active));

        uint256 stEthAmount = collateral.getPooledEthByShares(collShares);
        IEbtcLeverageZapRouter.TradeData memory tradeData = _getExactOutCollateralToDebtTradeData(
            debt, 
            stEthAmount * COLLATERAL_BUFFER / SLIPPAGE_PRECISION
        );

        vm.startPrank(vm.addr(0x11111));
        vm.expectRevert("EbtcLeverageZapRouter: not owner for close!");
        leverageZapRouter.closeCdp(
            cdpId,
            abi.encode(pmPermit),
            tradeData
        );
        vm.stopPrank();

        vm.startPrank(user);

        _before();
        leverageZapRouter.closeCdp(
            cdpId,
            abi.encode(pmPermit),
            tradeData
        );
        _after();

        vm.stopPrank();

        assertEq(cdpManager.getCdpStatus(cdpId), uint256(ICdpManagerData.Status.closedByOwner));

        _checkZapStatusAfterOperation(user);
    }

    function test_ZapCloseCdp_WithWstEth_LowLeverage() public {
        seedActivePool();

        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.stETH);

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user);

        vm.prank(user);
        collateral.transfer(address(leverageZapRouter), 1);

        vm.startPrank(user);

        (uint256 debt, uint256 collShares) = cdpManager.getSyncedDebtAndCollShares(cdpId);

        assertEq(cdpManager.getCdpStatus(cdpId), uint256(ICdpManagerData.Status.active));
        uint256 _stETHValBefore = IERC20(address(testWstEth)).balanceOf(user);

        _before();
        leverageZapRouter.closeCdpForWstETH(
            cdpId,
            abi.encode(pmPermit),
            _getExactOutCollateralToDebtTradeData(
                debt, 
                collateral.getPooledEthByShares(collShares) * COLLATERAL_BUFFER / SLIPPAGE_PRECISION
            )
        );
        _after();

        assertEq(cdpManager.getCdpStatus(cdpId), uint256(ICdpManagerData.Status.closedByOwner));

        uint256 _stETHValAfter = IERC20(address(testWstEth)).balanceOf(user);
        assertEq(_stETHValAfter - _stETHValBefore, 4940573505654281098);

        vm.stopPrank();

        _checkZapStatusAfterOperation(user);
    }

    function test_ZapCloseCdpWithDonation_WithStEth_LowLeverage() public {
        seedActivePool();

        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.stETH);

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user);

        vm.prank(user);
        collateral.transfer(address(leverageZapRouter), 1);

        vm.startPrank(user);

        (uint256 debt, uint256 collShares) = cdpManager.getSyncedDebtAndCollShares(cdpId);

        assertEq(cdpManager.getCdpStatus(cdpId), uint256(ICdpManagerData.Status.active));

        _before();
        leverageZapRouter.closeCdp(
            cdpId,
            abi.encode(pmPermit),
            _getExactOutCollateralToDebtTradeData(
                debt, 
                collateral.getPooledEthByShares(collShares) * COLLATERAL_BUFFER / SLIPPAGE_PRECISION
            )
        );
        _after();

        assertEq(cdpManager.getCdpStatus(cdpId), uint256(ICdpManagerData.Status.closedByOwner));

        vm.stopPrank();

        _checkZapStatusAfterOperation(user);
    }

    function _getAdjustCdpParams(
        uint256 _flAmount, 
        int256 _debtChange,
        int256 _collValue,
        int256 _marginBalance,
        bool _useWstETHForDecrease
    ) private view returns (IEbtcLeverageZapRouter.AdjustCdpParams memory) {
        return IEbtcLeverageZapRouter.AdjustCdpParams({
            flashLoanAmount: _flAmount,
            debtChange: _debtChange < 0 ? uint256(-_debtChange) : uint256(_debtChange),
            isDebtIncrease: _debtChange > 0,
            upperHint: bytes32(0),
            lowerHint: bytes32(0),
            stEthBalanceChange: _collValue < 0 ? uint256(-_collValue) : uint256(_collValue),
            isStEthBalanceIncrease: _collValue > 0,
            stEthMarginBalance: _marginBalance < 0 ? uint256(-_marginBalance) : uint256(_marginBalance),
            isStEthMarginIncrease: _marginBalance > 0,
            useWstETHForDecrease: _useWstETHForDecrease
        });
    }

    function _getExactOutCollateralToDebtTradeData(
        uint256 _debtAmount,
        uint256 _collAmount
    ) private view returns (IEbtcLeverageZapRouter.TradeData memory) {
        uint256 flashFee = IERC3156FlashLender(address(borrowerOperations)).flashFee(
            address(eBTCToken),
            _debtAmount
        );

        return IEbtcLeverageZapRouter.TradeData({
            performSwapChecks: false,
            expectedMinOut: 0,
            exchangeData: abi.encodeWithSelector(
                mockDex.swapExactOut.selector,
                address(collateral),
                address(eBTCToken),
                _debtAmount + flashFee
            ),
            approvalAmount: _collAmount,
            collValidationBufferBPS: 10500 // 5%
        });
    }

    function _getExactInDebtToCollateralTradeData(
        uint256 _amount
    ) private view returns (IEbtcLeverageZapRouter.TradeData memory) {
        _amount = _amount - (_amount * defaultZapFee / 10000);
        return IEbtcLeverageZapRouter.TradeData({
            performSwapChecks: false,
            expectedMinOut: 0,
            exchangeData: abi.encodeWithSelector(
                mockDex.swap.selector,
                address(eBTCToken),
                address(collateral),
                _amount // Debt amount
            ),
            approvalAmount: _amount,
            collValidationBufferBPS: 10500 // 5%
        });
    }

   function _getExactInCollateralToDebtTradeData(
        uint256 _amount
    ) private view returns (IEbtcLeverageZapRouter.TradeData memory) {
        return IEbtcLeverageZapRouter.TradeData({
            performSwapChecks: false,
            expectedMinOut: 0,
            exchangeData: abi.encodeWithSelector(
                mockDex.swap.selector,
                address(collateral),
                address(eBTCToken),
                _amount // Debt amount
            ),
            approvalAmount: _amount,
            collValidationBufferBPS: 10500 // 5%
        });
    }

    function test_adjustCdp_debtIncrease_stEth() public {
        seedActivePool();

        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.stETH);

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user);

        uint256 debtChange = 2e18;
        uint256 marginIncrease = 0.5e18;
        uint256 collValue = _debtToCollateral(debtChange) * COLLATERAL_BUFFER / 10000;
        uint256 flAmount = _debtToCollateral(debtChange);

        vm.startPrank(vm.addr(0x11111));
        vm.expectRevert("EbtcLeverageZapRouter: not owner for adjust!");
        leverageZapRouter.adjustCdp(
            cdpId, 
            _getAdjustCdpParams(flAmount, int256(debtChange), int256(collValue), 0, false), 
            abi.encode(pmPermit), 
            _getExactInDebtToCollateralTradeData(debtChange)
        );
        vm.stopPrank();

        _before();
        vm.startPrank(user);
        leverageZapRouter.adjustCdp(
            cdpId, 
            _getAdjustCdpParams(flAmount, int256(debtChange), int256(collValue), int256(marginIncrease), false),
            abi.encode(pmPermit), 
            _getExactInDebtToCollateralTradeData(debtChange)
        );
        vm.stopPrank();
        _after();

        // Test zap fee
        assertEq(eBTCToken.balanceOf(testFeeReceiver), (1e18 + debtChange) * defaultZapFee / 10000); 

        _checkZapStatusAfterOperation(user);
    }

    function test_adjustCdp_debtDecrease_stEth() public {
        seedActivePool();

        (address user, bytes32 cdpId) = createLeveragedPosition(MarginType.stETH);

        IEbtcZapRouter.PositionManagerPermit memory pmPermit = createPermit(user);

        uint256 debtChange = 0.8e18;
        uint256 marginBalance = 0.5e18;
        uint256 collValue = _debtToCollateral(debtChange) * 10004 / 10000;

        _before();
        vm.startPrank(user);
        leverageZapRouter.adjustCdp(
            cdpId, 
            _getAdjustCdpParams(debtChange, -int256(debtChange), -int256(collValue), -int256(marginBalance), false), 
            abi.encode(pmPermit),
            _getExactInCollateralToDebtTradeData(collValue)
        );
        vm.stopPrank();
        _after();

        // Test zap fee (no fee if debt decrease)
        assertEq(eBTCToken.balanceOf(testFeeReceiver), 1e18 * defaultZapFee / 10000); 

        _checkZapStatusAfterOperation(user);
    }
}

Tools Used

foundry

Recommended Mitigation Steps

We need to track the actual swap output using before and after balance and then check the actualSwapOutput.

Assessed type

Invalid Validation

GalloDaSballo commented 4 months ago

Dup of #19

c4-judge commented 3 months ago

alex-ppg marked the issue as not a duplicate

c4-judge commented 3 months ago

alex-ppg marked the issue as duplicate of #38

c4-judge commented 3 months ago

alex-ppg marked the issue as unsatisfactory: Invalid