Farseen0/scar-pairs huggingface dataset:
HIGH severity: Withdrawing collateral and fees and bypassing trust safety mechanism. **Severity**

**Impact:** High

**Likelihood:** High

**Description**

From the project specification:

        Each project has a dedicated beneficiary address associated with it that enjoys very special rights during the next states and hence is enforced to be a (Safe) multisignature wallet with.....

Vulnerable code:
function checkIfBeneficiaryIsATrustedSafe(address beneficiary) public view {
    if (protocolTrustee == address(0)) {
      return; //when no trustee is configured, we're not checking for Safe accounts
    }
    IOwnerManager ownerManager = IOwnerManager(beneficiary);

    if (
      beneficiary.codehash != SAFE_PROXY_130_CODEHASH || ownerManager.getThreshold() != 2 // @audit check codehash = SAFE_PROXY_130_CODEHASH not ensure the Gnosis Safe is trusted.
        || ownerManager.getOwners().length != 2 || !ownerManager.isOwner(protocolTrustee)
    ) {
      revert BeneficiaryIsNotTrustful();
    }
  }

contract MaliciousGnosisSafe is GnosisSafe {
    address public fakeOwner;
    address public realOwner;

    function setReturnOwner(address _fakeOwner, address _realOwner) public {
        fakeOwner = _fakeOwner;
        realOwner = _realOwner;
    }

    function getOwners() public view override returns (address[] memory) {
        address[] memory array = new address[](2);
        array[0] = fakeOwner;
        array[1] = realOwner;
        return array;
    }

    function getThreshold() public view override returns (uint256) {
        return 2;
    }

    function isOwner(address owner) public view override returns (bool) {
        if (owner == fakeOwner) {
            return true;
        } else {
            return super.isOwner(owner);
        }
    }
}

+  interface ISafe {
+     function getStorageAt(uint256, uint256) external view returns (bytes memory);
+  }

+  address SAFE_SINGLETON_CONTRACT = 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552;

  function checkIfBeneficiaryIsATrustedSafe(address beneficiary) public view {
    if (protocolTrustee == address(0)) {
      return; //when no trustee is configured, we're not checking for Safe accounts
    }
    IOwnerManager ownerManager = IOwnerManager(beneficiary);

+   bytes memory data = ISafe(beneficiary).getStorageAt(0, 1);
+   address singletonAddress;
+
+   assembly {
+        singletonAddress := mload(add(data, 32))
+   }

-   if (
+   if ( singletonAddress != SAFE_SINGLETON_CONTRACT ||
      beneficiary.codehash != SAFE_PROXY_130_CODEHASH || ownerManager.getThreshold() != 2
        || ownerManager.getOwners().length != 2 || !ownerManager.isOwner(protocolTrustee)
    ) {
      revert BeneficiaryIsNotTrustful();
    }
  }
HIGH severity: Broken mint if market pre-mint less than `p`. **Severity**

**Impact:** Medium

**Likelihood:** High

**Description**

When market is created, it is allowed that `params.premint` and the `p` configured inside `params.curveParameters` is different, as long as `params.premint` is lower and result in 0 when provided to `getBuyPrice`.

However,...

Vulnerable code:
function checkParameters(MarketParameters memory params) public view virtual {
    if (!trustedCurves[address(params.priceCurve)]) {
      revert UntrustedCurve();
    }

    // reverts if parameters are out of sanity range
    params.priceCurve.checkParameters(params.curveParameters, params.fundingGoal);

    //the "premint" parameter can be different from the curve's premint parameter
    if (params.priceCurve.getBuyPrice(0, params.premint, params.curveParameters) != 0) {
      revert ParameterMismatch();
    }

    // check that the deadline isn't too far in the future (eg millisecond issues)
    if (params.deadline > block.timestamp + 10 * 365 days) {
      revert ParameterMismatch();
    }
  }

function mint(uint256 tokenId, uint256 amount)
    external
    payable
    nonReentrant
    returns (uint256 gross, uint256 net, uint256 tokensToMint)
  {
    MarketData storage market = markets[tokenId];
    if (market.state != MarketState.OPEN) {
      revert BadState(MarketState.OPEN, market.state);
    }

    MarketParameters memory _marketParams = marketParams[tokenId];

    if (trySettle(tokenId) > MarketState.OPEN) {
      //this allows settling the market implicitly without reverting
      Address.sendValue(payable(_msgSender()), msg.value);
      return (0, 0, 0);
    }

    uint256 tradingFee;
    //when buying, gross > net
    (gross, net, tradingFee) = getBuyPrice(tokenId, amount);

    // revert trades that require too low volume but still allow premints that require 0 value
    if (
>>>   totalSupply(tokenId) + amount > _marketParams.premint // @audit - premint here can be lower than the actual `p`
        && (amount < MINIMUM_TRADE_SIZE || net == 0)
    ) {
      revert TradeSizeOutOfRange();
    }
   // ...
}

+  (, uint256 p) = _marketParams.priceCurve.decodeParameters(_marketParams.curveParameters);
    if (
-      totalSupply(tokenId) + amount > _marketParams.premint
+      totalSupply(tokenId) + amount > p
        && (amount < MINIMUM_TRADE_SIZE || net == 0)
    ) {
      revert TradeSizeOutOfRange();
    }
MEDIUM severity: Revert if supply is less than premint parameter. **Severity**

**Impact:** Medium

**Likelihood:** Medium

**Description**

The `getTokensNeededToAddEthValue` function reverts if the `supply` is less than the `curve`'s `premint` parameter.

I will describe the scenario where the `supply` is smaller than the `curve`'s `premint` parameter.

The...

Vulnerable code:
function getTokensNeededToAddEthValue(uint256 supply, uint256 ethAmount, bytes32 curveParameters) external pure returns (uint256 tokenAmount) {
    (uint256 m, uint256 p) = decodeParameters(curveParameters);
    UD60x18 pmins = ud(supply - p);
}

function checkParameters(MarketParameters memory params) public view virtual {
    //the "premint" parameter can be different from the curve's premint parameter
    if (params.priceCurve.getBuyPrice(0, params.premint, params.curveParameters) != 0) {
      revert ParameterMismatch();
    }
}

function spawn(MarketParameters memory params) external nonReentrant {
    _mint(params.sourcer, params.tokenId, params.premint, "");
}
MEDIUM severity: 10% maximum pre-mint check can be bypassed. **Severity**

**Impact:** Medium

**Likelihood:** Medium

**Description**

When users create market, it will need to be checked by `LinearCurve.checkParameters` if the provided configuration is correct. `m` must within the predefined range and `p` (pre-mint) amount must not exceed 10% of total...

Vulnerable code:
function checkParameters(bytes32 curveParameters, uint256 fundingGoal) external pure override {
    (uint256 m, uint256 p) = decodeParameters(curveParameters);
    if ((m < 1e4) || (m >= 1e19)) {
      revert CurveParametersOutOfRange();
    }

    uint256 fundingGoalInTokens = supplyAtCollateral(curveParameters, fundingGoal);

    //shouldnt allow more than 10% premints
    if (((100 * p) / fundingGoalInTokens) > 10) {
      revert CurveParametersOutOfRange();
    }
  }

function testPreMintExceed10Percent() public {
      uint256 slope = curve.computeSlope(10 ether, 9000 ether, 990 ether);
      uint256 fundingGoalInTokens =
      curve.supplyAtCollateral(bytes32(abi.encodePacked(uint128(slope), uint128(990 ether))), 10 ether);

      (uint256 tokenId,) = defaultMarket(ophelia);

      userMint(alice, tokenId, 9000 ether);


      MarketData memory _marketData = ipSeed.getMarketData(tokenId);

      console.log("market collateral collected : ");
      console.log(_marketData.collateral);

      console.log("supplyAtCollateral result : ");
      console.log(fundingGoalInTokens);
      assertTrue(_marketData.state == MarketState.FUNDED);
    }

function defaultParams(uint256 tokenId, address sourcer, address beneficiary, LinearCurve curve)
    public
    view
    returns (MarketParameters memory params)
  {
    uint256 slope = curve.computeSlope(10 ether, 9000 ether, 990 ether);

    params = MarketParameters({
      tokenId: tokenId,
      projectId: projectId,
      sourcer: sourcer,
      beneficiary: beneficiary,
      priceCurve: curve,
      curveParameters: bytes32(abi.encodePacked(uint128(slope), uint128(990 ether))),
      fundingGoal: 10 ether,
      premint: 990 ether,
      deadline: uint64(block.timestamp + defaultDeadline)
    });
  }
LOW severity: Collateral not returned if there is a disagreement. When the negotition is failed, the beneficiary can call `negotiationFailed` function to returns the collateral back. The function has the `onlyBeneficiary` modifier, which means only the beneficiary can call this function. However, if the beneficiary is a Gnosis Safe multisig with threshold 2 and...

Vulnerable code:
function negotiationFailed(uint256 tokenId)
    external
    payable
    nonReentrant
    onlyBeneficiary(tokenId) // @audit Need both sourcer and prtocol trustee to sign
  {
    ...
  }
LOW severity: Dust amount left after all refund is made. When the market is expired, the `refund` function calculates the refund amount based on the user's contribution to the funding goal. The refund amount is calculated by dividing the user's contribution by the accrued capital and multiplying it by the claimable capital. The result is rounded down,...

Vulnerable code:
function refund(uint256 tokenId, address contributor, uint16 feeBps)
    private
    returns (uint256 payout, uint256 tradingFee)
  {
    uint256 contribution = contributions[tokenId][contributor];
    MarketData storage market = markets[tokenId];
    //in an open market users simply get their contribution back
    //in expired markets they receive a share of the claimable collateral proportional to their original contribution to the funding goal
    if (market.state > MarketState.OPEN) {
      payout = (contribution * market.claimableCapital) / market.accruedCapital; // @audit result is rounded down
    } else {
    ...
  }
MEDIUM severity: Some orders can be executed multiple times. **Severity**

**Impact:** High

**Likelihood:** Low

**Description**

Functions `_deduplicateOrder()` and `verifyNonce()` are in charge of deduplicating orders and making sure that the same nonce can't be used twice. The issue is that in `verifyNonce()` code converts `invalidatorBit` to `uint128`...

Vulnerable code:
uint128 invalidatorSlot = uint64(nonce) >> 8;
 uint128 invalidatorBit = uint128(1 << uint8(nonce));
 uint128 invalidator = _orderBitmaps[sender][invalidatorSlot];
 if (invalidator & invalidatorBit != 0) revert InvalidNonce();
LOW severity: Missing sanity checks when setting the `tokenConfig`. The `addSupportedAsset()` function allows the admin to add new tokens with built-in sanity checks to ensure token validity:

However, during deployment, new tokens are added using the internal `_setTokenConfig()` function, which lacks these validations:

To prevent errors during deployment,...

Vulnerable code:
if (tokenConfig[asset].isActive || asset == address(0) || asset == address(usde)) {
      revert InvalidAssetAddress();
    }

for (uint128 k = 0; k < _tokenConfig.length;) {
      _setTokenConfig(_assets[k], _tokenConfig[k]);
      unchecked {
        ++k;
      }
    }

for (uint128 k = 0; k < _tokenConfig.length;) {
      if (_assets[k] == address(0) || _assets[k] == address(usde)) revert InvalidAssetAddress();
      _setTokenConfig(_assets[k], _tokenConfig[k]);
      unchecked {
        ++k;
      }
    }
LOW severity: ETH and WETH redemption limits can be combined. The `EthenaMinting.sol` contract introduced asset based limits for minting and redemption in addition to global limits.

In one case, these limits can be bypassed. When redeeming USDe for ETH and WETH tokens, the different `totalPerBlockPerAsset[block.number]` accumulators are incremented, even...

Vulnerable code:
modifier belowMaxMintPerBlock(uint128 mintAmount, address asset) {
    TokenConfig memory _config = tokenConfig[asset];
    if (!_config.isActive) revert UnsupportedAsset();
 >> if (totalPerBlockPerAsset[block.number][asset].mintedPerBlock + mintAmount > _config.maxMintPerBlock) {
      revert MaxMintPerBlockExceeded();
    }
    _;
  }
  modifier belowMaxRedeemPerBlock(uint128 redeemAmount, address asset) {
    TokenConfig memory _config = tokenConfig[asset];
    if (!_config.isActive) revert UnsupportedAsset();
>>  if (totalPerBlockPerAsset[block.number][asset].redeemedPerBlock + redeemAmount > _config.maxRedeemPerBlock) {
      revert MaxRedeemPerBlockExceeded();
    }
    _;
  }

function redeem(Order calldata order, Signature calldata signature)
    external
    override
    nonReentrant
    onlyRole(REDEEMER_ROLE)
>>  belowMaxRedeemPerBlock(order.usde_amount, order.collateral_asset)
    belowGlobalMaxRedeemPerBlock(order.usde_amount)
  {
    ---SNIP---
>>  totalPerBlockPerAsset[block.number][order.collateral_asset].redeemedPerBlock += order.usde_amount;
HIGH severity: `updateSystemSnapshots_excludeCollRemainder` lacks access control. **Impact**

The function `updateSystemSnapshots_excludeCollRemainder` doesn't have access control meaning that stakes can be manipulated

I found this by running invariant tests, with the simple check that no call should ever succeed

**Mitigation**

Add a check to ensure that the caller is...

Vulnerable code:
=> [call] CryticTester.troveManager_updateSystemSnapshots_excludeCollRemainder((address,uint256)[])([]) (addr=0xA647ff3c36cFab592509E13860ab8c4F28781a66, value=0, sender=0x0000000000000000000000000000000000030000)
         => [call] TroveManager.updateSystemSnapshots_excludeCollRemainder((address,uint256)[])([]) (addr=0x48E4F3f3daE11341ff2eDF60Af6857Ae08C871C5, value=0, sender=0xA647ff3c36cFab592509E13860ab8c4F28781a66)
                 => [event] SystemSnapshotsUpdated([], [])
                 => [return ()]
         => [event] Log("should never be possible")
         => [panic: assertion failed]
HIGH severity: `BorrowerOperations` Inconsistent IMCR logic could allow risky collaterals to have a higher CollerateralRatio during Recovery Mode. **Impact**

`_requireValidAdjustmentInCurrentMode` is a key part of the security checks for adjusting and opening Troves

From discussing with the team, I was made aware that some Collaterals may have a IMCR that would be above CCR (e.g. 175% CR)

Given this fact, we can check that the logic in...

Vulnerable code:
if (_vars.isInRecoveryMode) {
      // BorrowerOps: Collateral withdrawal not permitted Recovery Mode
      if (_isCollWithdrawal) revert CollWithdrawPermittedInRM();
      if (_isDebtIncrease) _requireICRisAboveCCR(_vars.newICR);
    } else {
      // if Normal Mode

      // check if the individual minimum collateral ratio is met (based on the used coll types)
      if (_isCollWithdrawal || _isDebtIncrease) _requireICRisAboveIMCR(_vars.newICR, _vars.newIMCR);

_requireICRisAboveIMCR(_vars.newICR, _vars.newIMCR);
HIGH severity: `StakingOperations` Token Transfer is updating the total supply before accruing rewards to users causing loss of rewards. **Impact**

-> Transfer
-> New Balance
-> Check points (user uses old balance, but total supply is the new one)
-> User lost rewards
-> New total Supply and correct total debt

In fixing this be careful not to cause a division by zero (as the initial deposit would)

This can only happen on...

Vulnerable code:
function updatePool(ISwapPair _pid) public {
    PoolInfo storage pool = poolInfo[_pid];

    // check
    if (block.timestamp <= pool.lastRewardTime) return;

    // update
    uint tokenSupply = _pid.balanceOf(address(this));
    if (tokenSupply == 0 || totalAllocPoint == 0) {
      pool.lastRewardTime = block.timestamp;
      return;
    }
    uint multiplier = block.timestamp - pool.lastRewardTime;
    uint reward = (multiplier * rewardsPerSecond * pool.allocPoint) / totalAllocPoint;
    pool.accRewardPerShare += (reward * REWARD_DECIMALS) / tokenSupply;
    pool.lastRewardTime = block.timestamp;
  }
HIGH severity: `StakingOperations.claim` doesn't update reward debt, allowing multiple claims. **Impact**

User rewards are computed as `((user.amount * accRewardPerShare) / REWARD_DECIMALS) - user.rewardDebt;`

`user.rewardDebt = (user.amount * pool.accRewardPerShare) / REWARD_DECIMALS;` is set in deposit and withdraw

but it is not update on `claim` and `batchClaim`

Due to this, an...

Vulnerable code:
function claim(ISwapPair _pid) external override {
    requireValidPool(_pid);
    _claim(_pid, msg.sender);
  }

  function batchClaim(ISwapPair[] memory _pids) external override {
    uint length = _pids.length;
    for (uint n = 0; n < length; n++) {
      requireValidPool(_pids[n]);
      _claim(_pids[n], msg.sender);
    }
MEDIUM severity: `RedemptionOperations` Redemptions should be disabled during Recovery Mode. **Impact**

`RedemptionOperations` checks the `TCR < MCR`, but should most likely check for `TCR < CCR`

https://github.com/blkswnStudio/ap/blob/8fab2b32b4f55efd92819bd1d0da9bed4b339e87/packages/contracts/contracts/RedemptionOperations.sol#L101-L103

This is because during Recovery Mode, minting...

Vulnerable code:
(, uint TCR, , ) = storagePool.checkRecoveryMode(vars.priceCache); /// @audit High? not checking RM -> Mint for free, Inflate total supply (pay no redemption), redeem at small fee
    if (TCR < MCR) revert LessThanMCR(); /// @audit-ok force to liquidate if all system is insolvent
MEDIUM severity: `_exchangeRate` value validation looks incorrect. **Impact**

By definition a value for `_exchangeRate` should be within the bounds of `STOCK_SPLIT_PRECISION`

**Mitigation**

It's unclear if this is incorrect

Either way I highly recommend simplifying the logic for stock splits to allow the owner to set a scalar multiplier and a divisor, this...

Vulnerable code:
if (_exchangeRate >= -STOCK_SPLIT_PRECISION && _exchangeRate < STOCK_SPLIT_PRECISION) revert InvalidExchangeRate();
MEDIUM severity: `RedemptionOperations` allows redemption against stale prices. **Impact**

The code for `_calculateTroveRedemption` doesn't validate that collaterals nor debts prices are not stale

https://github.com/blkswnStudio/ap/blob/8fab2b32b4f55efd92819bd1d0da9bed4b339e87/packages/contracts/contracts/RedemptionOperations.sol#L231-L259

This opens up to arbitrage...

Vulnerable code:
function _calculateTroveRedemption(
    PriceCache memory _priceCache,
    address _borrower,
    uint _redeemMaxAmount,
    bool _includePendingRewards
  ) internal view returns (SingleRedemptionVariables memory vars) {
    address stableCoinAddress = address(tokenManager.getStableCoin());

    // stable coin debt should always exists because of the gas comp
    TokenAmount[] memory troveDebt = _includePendingRewards
      ? troveManager.getTroveRepayableDebts(_priceCache, _borrower, true) // with pending rewards
      : troveManager.getTroveDebt(_borrower); // without pending rewards
    if (troveDebt.length == 0) revert InvalidRedemptionHint();
    for (uint i = 0; i < troveDebt.length; i++) {
      TokenAmount memory debtEntry = troveDebt[i];

      if (debtEntry.tokenAddress == stableCoinAddress) vars.stableCoinEntry = debtEntry;
      vars.troveDebtInUSD += priceFeed.getUSDValue(_priceCache, debtEntry.tokenAddress, debtEntry.amount);
    }

    vars.collLots = _includePendingRewards
      ? troveManager.getTroveWithdrawableColls(_borrower)
      : troveManager.getTroveColl(_borrower);
    for (uint i = 0; i < vars.collLots.length; i++) {
      uint p = priceFeed.getUSDValue(_priceCache, vars.collLots[i].tokenAddress, vars.collLots[i].amount);
      vars.troveCollInUSD += p;
      if (!tokenManager.isDebtToken(vars.collLots[i].tokenAddress)) vars.redeemableTroveCollInUSD += p;
    } /// @audit can change the ratio of a trove to have mostly `isDebtToken` debt | How does this relate to collateral?
MEDIUM severity: `LiquidationOperations.batchLiquidateTroves` redistributes bad debt and collateral after all operations, meaning it will allow skipping bad debt redistribution during liquidations. **Impact**

The code for `batchLiquidateTroves` is as follows:

https://github.com/blkswnStudio/ap/blob/8fab2b32b4f55efd92819bd1d0da9bed4b339e87/packages/contracts/contracts/LiquidationOperations.sol#L106-L137

Where in the loop, liquidations are done on Troves for which their Debt and Coll is...

Vulnerable code:
function batchLiquidateTroves(address[] memory _troveArray, bytes[] memory _priceUpdateData) public payable override {
    if (!troveManager.enableLiquidationAndRedeeming()) revert LiquidationDisabled(); /// @audit MUST be separate
    if (_troveArray.length == 0) revert EmptyArray();

    LocalVariables_OuterLiquidationFunction memory vars;

    // update prices and build price cache
    priceFeed.updatePythPrices{ value: msg.value }(_priceUpdateData); /// @audit custom update on oracle prices, doesn't guarantee everything will be valid
    vars.priceCache = priceFeed.buildPriceCache();

    (vars.isRecoveryMode, vars.TCR, vars.entireSystemCollInUSD, vars.entireSystemDebtInUSD) = storagePool
      .checkRecoveryMode(vars.priceCache);
    vars.remainingStabilities = stabilityPoolManager.getRemainingStability(vars.priceCache);
    _initializeEmptyTokensToRedistribute(vars); // all set to 0 (nothing to redistribute)

    bool atLeastOneTroveLiquidated = false;
    for (uint i = 0; i < _troveArray.length; i++) {
      address trove = _troveArray[i];
      if (!troveManager.isTroveActive(trove)) continue; // Skip non-active troves | // @audit TODO: Are these ones with 0 debt?
      if (troveManager.getTroveOwnersCount() <= 1) continue; // don't liquidate if last trove /// @audit Break?

      bool liquidated = _executeTroveLiquidation(vars, trove); /// @audit Out of order Sorting based on Risk, to maximize profit
      if (liquidated && !atLeastOneTroveLiquidated) atLeastOneTroveLiquidated = true;
    }
    if (!atLeastOneTroveLiquidated) revert NoLiquidatableTrove();

    // move tokens into the stability pools
    stabilityPoolManager.offset(vars.priceCache, vars.remainingStabilities); /// @audit Redistribute, but pay from reserve?

    // and redistribute the rest (which could not be handled by the stability pool)
    troveManager.redistributeDebtAndColl(vars.priceCache, vars.tokensToRedistribute); /// @audit But computed here ??

(
      vars.troveAmountsIncludingRewards,
      vars.IMCR,
      vars.troveCollInUSD,
      vars.troveDebtInUSD,
      vars.troveDebtInUSDWithoutGasCompensation
    ) = troveManager.getEntireDebtAndColl(outerVars.priceCache, trove);
MEDIUM severity: Decay Coefficient could round down and have an effective slower decay. **Impact**

`calcDecayedStableCoinBaseRate` calls `_minutesPassedSinceLastFeeOp` which rounds down by up to 1 minute - 1

https://github.com/blkswnStudio/ap/blob/8fab2b32b4f55efd92819bd1d0da9bed4b339e87/packages/contracts/contracts/TroveManager.sol#L987-L997

This, in conjunction with the logic...

Vulnerable code:
function calcDecayedStableCoinBaseRate() public view override returns (uint) {
    uint minutesPassed = _minutesPassedSinceLastFeeOp();
    uint decayFactor = LiquityMath._decPow(MINUTE_DECAY_FACTOR, minutesPassed);

    return (stableCoinBaseRate * decayFactor) / DECIMAL_PRECISION;
  }

  function _minutesPassedSinceLastFeeOp() internal view returns (uint) {
    return (block.timestamp - lastFeeOperationTime) / 1 minutes;
  }

uint timePassed = block.timestamp - lastFeeOperationTime;
    if (timePassed >= 1 minutes) { /// @audit Can we abuse this in some way? | See ETHOS and eBTC findings
      lastFeeOperationTime = block.timestamp;
      emit LastFeeOpTimeUpdated(block.timestamp);
    }
MEDIUM severity: `TroveManager``_calcBorrowingRate` always returns `borrowingFeeFloor`. **Impact**

`_calcBorrowingRate` is using `min(X + Y, X)` meaning it will always return `X` in this case `borrowingFeeFloor`

**Mitigation**

Change 

To

Vulnerable code:
function _calcBorrowingRate(uint _stableCoinBaseRate) internal view returns (uint) {
    return LiquityMath._min(borrowingFeeFloor + _stableCoinBaseRate, borrowingFeeFloor);
  }

function _calcBorrowingRate(uint _stableCoinBaseRate) internal view returns (uint) {
    return LiquityMath._min(borrowingFeeFloor + _stableCoinBaseRate, 1e18);
  }