Farseen0/scar-pairs huggingface dataset:
MEDIUM severity: Risk of overborrowing by only using Price * Rate Feed. **Impact**

NOTE: I'm under the impression that the config for Quill is the following:

Meaning that all oracles use the ETH-USD feed instead of their Underlying * Rate Feed

This is in general a good price to protect against redemptions, as you're using ETH-USD * Rate - Meaning you're pricing the.....

Vulnerable code:
IWETH weth = IWETH(0x5300000000000000000000000000000000000004);
IERC20Metadata wsteth = IERC20Metadata(0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32);
IERC20Metadata weeth = IERC20Metadata(0x01f0a31698C4d065659b9bdC21B3610292a1c506);
IERC20Metadata scroll = IERC20Metadata(0xd29687c813D741E2F938F4aC377128810E217b1b);

// https://data.chain.link/feeds/scroll/mainnet/eth-usd
address eth_usd_oracle = 0x6bF14CB0A831078629D993FDeBcB182b21A8774C;

// wstETH steth (exchange rate) | Rate arb??
// https://data.chain.link/feeds/scroll/mainnet/wsteth-steth%20exchangerate
address wsteth_steth_oracle = 0xE61Da4C909F7d86797a0D06Db63c34f76c9bCBDC;

// Exchange rate
// https://data.chain.link/feeds/scroll/mainnet/weeth-eeth-exchange-rate
address weeth_eth_oracle = 0x57bd9E614f542fB3d6FeF2B744f3B813f0cc1258;


// https://data.chain.link/feeds/scroll/mainnet/scr-usd
address scroll_usd_oracle = 0x26f6F7C468EE309115d19Aa2055db5A74F8cE7A5;

// NOTE: No page for this??
// https://scrollscan.com/address/0x45c2b8C204568A03Dc7A2E32B71D67Fe97F908A9#readContract
address chainlinkScrollSequencerUptimeFeed = 0x45c2b8C204568A03Dc7A2E32B71D67Fe97F908A9;

// An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns:
    // - The price
    // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call
    function _fetchPricePrimary(bool /* _isRedemption */ ) internal returns (uint256, bool) {
        assert(priceSource == PriceSource.primary);
        (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle);
        (uint256 lstEthPrice, bool lstEthOracleDown) = _getOracleAnswer(lstEthOracle);

        // If the ETH-USD feed is down, shut down and switch to the last good price seen by the system
        // since we need both ETH-USD and canonical for primary and fallback price calcs
        if (ethUsdOracleDown || lstEthOracleDown) {
            return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);
        }
MEDIUM severity: Risky Footguns from Bold. **Impact**
These 3 lines can be VERY dangerous

https://github.com/GalloDaSballo/quill-review/blob/8d6e4c8ed0759cea1ff0376db9fd55db864cd7e8/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol#L46-L52

The reason why this code is safe for BOLD is becasue `vault` has a Reentrancy...

Vulnerable code:
// This will be used by the callback below no
        receiver = IFlashLoanReceiver(msg.sender);

        vault.flashLoan(this, tokens, amounts, userData);

        // Reset receiver
        receiver = IFlashLoanReceiver(address(0));

function receiveFlashLoan(
        IERC20[] calldata tokens,
        uint256[] calldata amounts,
        uint256[] calldata feeAmounts,
        bytes calldata userData
    ) external override {
        require(msg.sender == address(vault), "Caller is not Vault");
        require(address(receiver) != address(0), "Flash loan not properly initiated");

        // NOTE: Validate initiator (if available) (e.g. AAVE)
        // NOTE: Why not consume receiver?
        IFlashLoanReceiver public cachedReceiver = receiver;
        receiver = IFlashLoanReceiver(address(0));
LOW severity: Low Liquidity Branches will likely use the Redistribution due to 20% vs 5% premium. **Impact**

The difference in premium seems way too high, this can lead to scenarios in which, for low liquidity branches, whales remove bold from the stability pool as a means to trigger a redistribution

This is because redistributions are 4 times more profitable to...

Vulnerable code:
if (_boldInStabPool > 0) {
            debtToOffset = LiquityMath._min(_entireTroveDebt, _boldInStabPool);
            collSPPortion = _collToLiquidate * debtToOffset / _entireTroveDebt;
            (collToSendToSP, collSurplus) =
                _getCollPenaltyAndSurplus(collSPPortion, debtToOffset, liquidationPenaltySP, _price);
        }

        // Redistribution
        debtToRedistribute = _entireTroveDebt - debtToOffset;
        if (debtToRedistribute > 0) {
            uint256 collRedistributionPortion = _collToLiquidate - collSPPortion;
            if (collRedistributionPortion > 0) {
                (collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus(
                    collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty
                    debtToRedistribute,
                    liquidationPenaltyRedistribution, // _penaltyRatio
                    _price
                );
            }
        }
LOW severity: Sequencer Sentinel Config can be updated to follow first principles. **Impact**

The sequencer sentinel has 2 types of checks:
- `_requireSequencerUpAndOverGracePeriod` - Safer check, ensures that prices are updated
- `_requireSequencerUp` - Less safe check, prices may not be updated

A check that is less safe could be used for operations that reduce risk to the...

Vulnerable code:
function adjustTroveInterestRate(
        uint256 _troveId,
        uint256 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external {
        _requireSequencerUp();
        _requireIsNotShutDown();

function setBatchManagerAnnualInterestRate(
        uint128 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external {
        _requireSequencerUp();
        _requireIsNotShutDown();
        _requireValidInterestBatchManager(msg.sender);
        _requireInterestRateInBatchManagerRange(msg.sender, _newAnnualInterestRate);

function removeFromBatch(
        uint256 _troveId,
        uint256 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) public override {
        _requireSequencerUp();
        _requireIsNotShutDown();
LOW severity: Governance Raising the CCR could be used to prevent people from borrowing by griefer. **Impact**

This is partially mitigated by
`_requireNoBorrowingUnlessNewTCRisAboveCCR(_troveChange.debtIncrease, newTCR);`

https://github.com/subvisual/quill/blob/d4a5dcc168dfc315eef6a4c9c465a36c86ca0ddc/contracts/src/TroveManager.sol#L261-L280

**Mitigation**

Perform multiple checks in your...

Vulnerable code:
function setNewBranchConfiguration(
        uint256 _scr,
        uint256 _mcr,
        uint256 _ccr,
        uint256 _newLiquidationPenaltySP,
        uint256 _newLiquidationPenaltyRedistribution
    ) external {
        _requireCallerIsCollateralRegistry();
        _requireValidConfig(_ccr, _mcr, _scr, _newLiquidationPenaltySP, _newLiquidationPenaltyRedistribution);

        SCR = _scr;
        MCR = _mcr;
        CCR = _ccr;
        liquidationPenaltySP = _newLiquidationPenaltySP;
        liquidationPenaltyRedistribution = _newLiquidationPenaltyRedistribution;

        emit BranchConfigurationUpdated(
            _scr, _mcr, _ccr, _newLiquidationPenaltySP, _newLiquidationPenaltyRedistribution
        );
    }
LOW severity: Zombie Trove Spam Risk when Oracle is Highly Predictable could be used to borrow at lower rate and never get redeemed. **Impact**

The logic for Redemptions looks as follows:

The trove data is loaded from the storage pointer

https://github.com/subvisual/quill/blob/d4a5dcc168dfc315eef6a4c9c465a36c86ca0ddc/contracts/src/TroveManager.sol#L821-L829

Then we loop, and a specific edge case could...

Vulnerable code:
SingleRedemptionValues memory singleRedemption;
        // Let’s check if there’s a pending zombie trove from previous redemption
        if (lastZombieTroveId != 0) {
            singleRedemption.troveId = lastZombieTroveId;
            singleRedemption.isZombieTrove = true;
        } else {
            singleRedemption.troveId = sortedTrovesCached.getLast();
        }
        address lastBatchUpdatedInterest = address(0);

// Skip if ICR < 100%, to make sure that redemptions don’t decrease the CR of hit Troves
            if (getCurrentICR(singleRedemption.troveId, _price) < _100pct) {
                singleRedemption.troveId = nextUserToCheck;
                singleRedemption.isZombieTrove = false;
                continue;
            }
LOW severity: BATCH DEBT FIXES - Rebase is economicall unfeasible. 

Vulnerable code:
SHARES * 1e16 // DENOM
9999999.0
SHARES * 1e14 // DENOM
99999.0
SHARES * 1e13 // DENOM
9999.0
SHARES * 1e9 // DENOM
1.0
SHARES * 1e8 // DENOM
0.0
SHARES * 1e10 // DENOM
10.0
SHARES * 2e8 // DENOM
0.0
SHARES * 9e8 // DENOM
0.0
SHARES * 1.9e8 // DENOM
0.0
SHARES * 1.9e9 // DENOM
1.0
0.06 * 1e9 / 1e18
6e-11
0.06 * 1e9 / 1e18 * 3463
2.0778e-07
0.06 * 1e9 / 1e18 * 3463 * 300_000
0.062334
0.06 * 1e9 / 1e18 * 3463 * 300_00
0.0062334
0.06 * 1e9 / 1e18 * 3463 * 3000
0.00062334
1e9 / 1e18 * 3463
3.463e-06
LOW severity: Incorrect Comment about blocks and adjustments. **Impact**

Quill will reduce the min debt by a factor of 10

Bold had a potential risk when batch debt shares were rebased

The exploit was fixed and as far I was able to tell the system is safe from this exploit

Out of caution, as to maintain a similar risk profile as Liquity, it's best to 10x...

Vulnerable code:
uint128 constant MIN_INTEREST_RATE_CHANGE_PERIOD = 120 seconds; // prevents more than one adjustment per ~10 blocks /// @audit this is MAINNET
LOW severity: `ETH_GAS_COMPENSATION` changes may cause unprofitable liquidations during gas fees spikes - Suggested changes. **Impact**

The variable `ETH_GAS_COMPENSATION` in Quill was changed to:

This can be insufficient for liquidations that happen at a high GWEI

**Data**

From these pages we can see that sometimes the gas can spike up to 3 GWEI on...

Vulnerable code:
// Amount of ETH to be locked in gas pool on opening troves
uint256 constant ETH_GAS_COMPENSATION = 0.001 ether;

| batchLiquidateTroves                                                        | 30566           | 714168 | 648149 | 9548229 | 221     |
| liquidate                                                                   | 76475           | 468522 | 389065 | 727942  | 4796    |

>>> ASSUMED_WORST_CASE_LIQUIDATION_COST = 350_000
>>> ASSUMED_WORST_CASE_LIQUIDATION_COST * 2e9 / 1e18
0.0007
>>> ASSUMED_WORST_CASE_LIQUIDATION_COST * 3e9 / 1e18
0.00105
LOW severity: Missing DisableInitializers. **Impact**

The following contracts are now upgradeable

They do not `disableInitializers` on the logic

This is a code smell, I have yet to weaponize

And due to how the system works I doubt it will cause many...

Vulnerable code:
contract HintHelpers is QuillUUPSUpgradeable, IHintHelpers {
    string public constant NAME = "HintHelpers";

    ICollateralRegistry public collateralRegistry;

    function initialize(address _authority, ICollateralRegistry _collateralRegistry) public initializer {
        __QuillUUPSUpgradeable_init(_authority);
        collateralRegistry = _collateralRegistry;
    }

contract MultiTroveGetter is QuillUUPSUpgradeable, IMultiTroveGetter {
    ICollateralRegistry public collateralRegistry;

    function initialize(address _authority, ICollateralRegistry _collateralRegistry) public initializer {
        __QuillUUPSUpgradeable_init(_authority);
        collateralRegistry = _collateralRegistry;
    }

function initialize(address _authority, IBoldToken _boldToken, ISequencerSentinel _sequencerSentinel)
        public
        initializer
    {
        __QuillUUPSUpgradeable_init(_authority);
        lastFeeOperationTime = block.timestamp;
        boldToken = _boldToken;
        sequencerSentinel = _sequencerSentinel;

        // Initialize the baseRate state variable
        baseRate = INITIAL_BASE_RATE;
        emit BaseRateUpdated(INITIAL_BASE_RATE);
    }
LOW severity: Addresses. All addresses sourced from:
https://github.com/subvisual/quill/blob/a7c93057ce2de13a27a774f22bddd63878c9fe18/contracts/src/scripts/DeployQuillSCrollMainnet.s.sol#L64-L68

--------

20% MCR for wstETH | weETH and SCROLL

120 MCR for wstETH looks crazy - What a loss of efficiency for close to zero...

Vulnerable code:
struct TroveManagerParams {
        uint256 CCR;
        uint256 MCR;
        uint256 SCR;
        uint256 LIQUIDATION_PENALTY_SP;
        uint256 LIQUIDATION_PENALTY_REDISTRIBUTION;
        uint256 MIN_DEBT;
        uint256 SP_YIELD_SPLIT;
        uint256 minAnnualInterestRate;
    }

TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](4);
        troveManagerParamsArray[0] = TroveManagerParams(140e16, 110e16, 110e16, 5e16, 10e16, 500e18, 75e16, 6 * _1pct); // WETH
        troveManagerParamsArray[1] = TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, 500e18, 75e16, 6 * _1pct); // wstETH
        troveManagerParamsArray[2] = TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, 500e18, 75e16, 6 * _1pct); // weETH
        troveManagerParamsArray[3] = TroveManagerParams(160e16, 120e16, 120e16, 5e16, 30e16, 500e18, 75e16, 7 * _1pct); // SCROLL

IWETH weth = IWETH(0x5300000000000000000000000000000000000004);
    IERC20Metadata wsteth = IERC20Metadata(0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32);
    IERC20Metadata weeth = IERC20Metadata(0x01f0a31698C4d065659b9bdC21B3610292a1c506);
    IERC20Metadata scroll = IERC20Metadata(0xd29687c813D741E2F938F4aC377128810E217b1b);

    // https://data.chain.link/feeds/scroll/mainnet/eth-usd
    address eth_usd_oracle = 0x6bF14CB0A831078629D993FDeBcB182b21A8774C;

   // wstETH steth (exchange rate) | Rate arb??
// https://data.chain.link/feeds/scroll/mainnet/wsteth-steth%20exchangerate
    address wsteth_steth_oracle = 0xE61Da4C909F7d86797a0D06Db63c34f76c9bCBDC;

// Exchange rate
// https://data.chain.link/feeds/scroll/mainnet/weeth-eeth-exchange-rate
    address weeth_eth_oracle = 0x57bd9E614f542fB3d6FeF2B744f3B813f0cc1258;


// https://data.chain.link/feeds/scroll/mainnet/scr-usd
    address scroll_usd_oracle = 0x26f6F7C468EE309115d19Aa2055db5A74F8cE7A5;

// NOTE: No page for this??
// https://scrollscan.com/address/0x45c2b8C204568A03Dc7A2E32B71D67Fe97F908A9#readContract
    address chainlinkScrollSequencerUptimeFeed = 0x45c2b8C204568A03Dc7A2E32B71D67Fe97F908A9;


// TODO: Check uptime?
    uint256 eth_usd_stalenessThreshold = _48_HOURS;
    uint256 wsteth_steth_stalenessThreshold = _48_HOURS;
    uint256 weeth_eth_stalenessThreshold = _48_HOURS;
    uint256 scroll_usd_stalenessThreshold = _48_HOURS;
LOW severity: [G-01] Gas Optimizations. **5k+ fetch price only if not shutdown**

https://github.com/subvisual/quill/blob/23e53123a16b12614d25bfb715e17dd41bcebbdd/contracts/src/BorrowerOperations.sol#L736-L739

You could fetch the price only if `!hasBeenShutDown`

**2.1k gas Can be made immutable**

I cannot find a setter, so you can...

Vulnerable code:
(uint256 price,) = priceFeed.fetchPrice();
        if (!hasBeenShutDown) {
            _requireNewTCRisAboveCCR(_getNewTCRFromTroveChange(troveChange, price));
        }

sequencerSentinel = _addressesRegistry.sequencerSentinel();

// Gather and accumulate unbacked portions
        for (uint256 index = 0; index < totals.numCollaterals; index++) {
            ITroveManager troveManager = getTroveManager(index);
            (uint256 unbackedPortion, uint256 price, bool redeemable) =
                troveManager.getUnbackedPortionPriceAndRedeemability();
            prices[index] = price;
            if (redeemable) {
                totals.unbacked += unbackedPortion;
                unbackedPortions[index] = unbackedPortion;
            }
        }

        // There’s an unlikely scenario where all the normally redeemable branches (i.e. having TCR > SCR) have 0 unbacked
        // In that case, we redeem proportinally to branch size
        if (totals.unbacked == 0) {
            unbackedPortions = new uint256[](totals.numCollaterals);
            for (uint256 index = 0; index < totals.numCollaterals; index++) {
                ITroveManager troveManager = getTroveManager(index);
                (,, bool redeemable) = troveManager.getUnbackedPortionPriceAndRedeemability();
                if (redeemable) {
                    uint256 unbackedPortion = troveManager.getEntireSystemDebt();
                    totals.unbacked += unbackedPortion;
                    unbackedPortions[index] = unbackedPortion;
                }
            }
        }
HIGH severity: The flashloan protection for Zappers is insufficient - We can operate on Troves we don't own. **Impact**
The code for the Balancer Vault Flashloan is as follows:

https://etherscan.io/address/0xba12222222228d8ba445958a75a0704d566bf2c8#code

Meaning anyone can call this and set the `BalancerFlashloan` as the recipient

This only check done in the Zappers looks as...

Vulnerable code:
function flashLoan(
        IFlashLoanRecipient recipient,
        IERC20[] memory tokens,
        uint256[] memory amounts,
        bytes memory userData
    ) external override nonReentrant whenNotPaused {
        InputHelpers.ensureInputLengthMatch(tokens.length, amounts.length);

        uint256[] memory feeAmounts = new uint256[](tokens.length);
        uint256[] memory preLoanBalances = new uint256[](tokens.length);

        // Used to ensure `tokens` is sorted in ascending order, which ensures token uniqueness.
        IERC20 previousToken = IERC20(0);

        for (uint256 i = 0; i < tokens.length; ++i) {
            IERC20 token = tokens[i];
            uint256 amount = amounts[i];

            _require(token > previousToken, token == IERC20(0) ? Errors.ZERO_TOKEN : Errors.UNSORTED_TOKENS);
            previousToken = token;

            preLoanBalances[i] = token.balanceOf(address(this));
            feeAmounts[i] = _calculateFlashLoanFeeAmount(amount);

            _require(preLoanBalances[i] >= amount, Errors.INSUFFICIENT_FLASH_LOAN_BALANCE);
            token.safeTransfer(address(recipient), amount);
        }

        recipient.receiveFlashLoan(tokens, amounts, feeAmounts, userData);

function receiveFlashLoanOnLeverDownTrove(LeverDownTroveParams calldata _params, uint256 _effectiveFlashLoanAmount)
        external
    {
        require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider");

function receiveFlashLoanOnOpenLeveragedTrove(
        OpenLeveragedTroveParams calldata _params,
        uint256 _effectiveFlashLoanAmount
    ) external {
        require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider");
        require(decodeParams(_params) == currentOperationId, "callback was started by this contract");
        currentOperationId = UNSET_OPERATION_ID; // bytes32(0)
MEDIUM severity: `eth_price_fallback_2` review. **Executive Summary**

**Logic still doesn't solve against Redemption Fee being lower than Oracle Drift**

https://github.com/liquity/bold/blob/3ad11270a22190e77c1e8ef7742d2ebec133a317/contracts/src/PriceFeeds/RETHPriceFeed.sol#L58-L60

Because rETH and stETH/ETH have a higher deviation threshold...

Vulnerable code:
// Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation.
        // NOTE: only needed | /// @audit You should take the max for redemptions
        uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice);

uint256 wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18;

function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) {
        assert(priceSource == PriceSource.ETHUSDxCanonical);
        // Get the underlying_per_LST canonical rate directly from the LST contract
        // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0?
        (uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate();

        // If the exchange rate contract is down, switch to (and return) lastGoodPrice. 
        if (exchangeRateIsDown) {
            priceSource = PriceSource.lastGoodPrice;
            return lastGoodPrice;
        }

        // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * underlying_per_LST
        uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18;

        uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice); /// @audit Downward price
        /// @audit This will keep the lowest possible price "forever", it should instead update since the ETH feed is working
        lastGoodPrice = bestPrice; /// @audit Redemptions may overpay MASSIVELY due to that

        return bestPrice;
    }
MEDIUM severity: `zappers_leverage_exchange_leftovers` review. **Med - Missing Sweeps here**

https://github.com/liquity/bold/blob/3f190ec5d63fa26a64fa5edc9404b92e9e053e03/contracts/src/Zappers/GasCompZapper.sol#L143-L174

As discussed in the original finding, one way to leave dust is to try repaying more than necessary

This will result in a transfer in of...

Vulnerable code:
function adjustTroveWithRawETH(
        uint256 _troveId,
        uint256 _collChange,
        bool _isCollIncrease,
        uint256 _boldChange,
        bool _isDebtIncrease,
        uint256 _maxUpfrontFee
    ) external {
        address receiver = _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease);
        borrowerOperations.adjustTrove(
            _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee
        );
        _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver);
    }

    function adjustUnredeemableTroveWithRawETH(
        uint256 _troveId,
        uint256 _collChange,
        bool _isCollIncrease,
        uint256 _boldChange,
        bool _isDebtIncrease,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external {
        address receiver = _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease);
        borrowerOperations.adjustUnredeemableTrove(
            _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee
        );
        _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver);
    }

function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount, address _zapper) external returns (uint256) {
        ICurvePool curvePoolCached = curvePool;
        IBoldToken boldTokenCached = boldToken;
        uint256 initialBoldBalance = boldTokenCached.balanceOf(address(this));
        boldTokenCached.transferFrom(_zapper, address(this), _boldAmount);
        boldTokenCached.approve(address(curvePoolCached), _boldAmount);

        // TODO: make this work
        //return curvePoolCached.exchange(BOLD_TOKEN_INDEX, COLL_TOKEN_INDEX, _boldAmount, _minCollAmount, false, _zapper);
        uint256 output = curvePoolCached.exchange(BOLD_TOKEN_INDEX, COLL_TOKEN_INDEX, _boldAmount, _minCollAmount);
        collToken.safeTransfer(_zapper, output);

        uint256 currentBoldBalance = boldTokenCached.balanceOf(address(this));
        if (currentBoldBalance > initialBoldBalance) {
            boldTokenCached.transfer(_zapper, currentBoldBalance - initialBoldBalance);
        }

        return output;
    }