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.
Second, LeverageMacroBase._doSwapChecks() does the check, but only against the balance of the output token, not the actual output amount from the swap operation.
Before the swap, the router has stETh: 91019547657512117.
The swap will return stEth in the amount of : 17994749596122777329
after the swap, the router has a balance of: 18085769143780289446
expectedMinOut = 18000000000000000000
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.
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 thanexpectedMinOut
. 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 thanexpectedMinOut
.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:
expectedMinOut
, the check will fail to detect this since it will only check the final balance of stEth, which is indeed greater thanexpectedMinOut
.I revised file LeverageZaps.t.sol. Please run
forge test --match-test testOpenCdp1 -vv
.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