Badger eBTC Audit + Certora Formal Verification Competition - Sathish9098'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: 21/52

Findings: 1

Award: $162.76

Gas:
grade-a

๐ŸŒŸ Selected for report: 0

๐Ÿš€ Solo Findings: 0

Awards

162.763 USDC - $162.76

Labels

bug
G (Gas Optimization)
grade-a
sufficient quality report
edited-by-warden
G-17

External Links

GAS OPTIMIZATIONS

[G-1] Optimizing storage usage with packed structs

Saves 8000 GAS , 4 SLOT

Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct.

Subsequent reads as well as writes have smaller gas savings.

[G-1.1] user,isCollIncrease,isDebtIncrease can be packed with same slot : Saves 2 SLOT , 4000 GAS

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/BorrowerOperations.sol#L97-L104

FILE: Breadcrumbs2023-10-badger/packages/contracts/contracts/BorrowerOperations.sol

 struct MoveTokensParams {
        address user;
+        bool isCollIncrease;
+        bool isDebtIncrease;
        uint256 collSharesChange;
        uint256 collAddUnderlying; // ONLY for isCollIncrease=true
-        bool isCollIncrease;
        uint256 netDebtChange;
-        bool isDebtIncrease;
    }

[G-1.2] roundEthBtcId , roundStEthEthId , success can be packed with same slot : Saves 1 SLOT , 2000 GAS

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/Interfaces/IPriceFeed.sol#L17-L24

FILE: 2023-10-badger/packages/contracts/contracts/Interfaces/IPriceFeed.sol

 struct ChainlinkResponse {
        uint80 roundEthBtcId;
        uint80 roundStEthEthId;
+        bool success;
        uint256 answer;
        uint256 timestampEthBtc;
        uint256 timestampStEthEth;
-        bool success;
    }

[G-1.3] timestamp , success can be packed with same slot : Saves 1 SLOT , 2000 GAS

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/Interfaces/IPriceFeed.sol#L26-L30

FILE: 2023-10-badger/packages/contracts/contracts/Interfaces/IPriceFeed.sol

  struct FallbackResponse {
        uint256 answer;
-        uint256 timestamp;
+        uint248 timestamp;
        bool success;
    }

[G-2] State variables can be packed into fewer storage slots

Saves 2000 GAS , 1 SLOT

The EVM works with 32 byte words. Variables less than 32 bytes can be declared next to eachother in storage and this will pack the values together into a single 32 byte storage slot (if the values combined are <= 32 bytes). If the variables packed together are retrieved together in functions we will effectively save ~2000 gas with every subsequent SLOAD for that storage slot. This is due to us incurring a Gwarmaccess (100 gas) versus a Gcoldsload (2100 gas).

[G-2.1] minuteDecayFactor and redemptionsPaused can be packed within same slot : Saves 2000 GAS , 1 SLOT

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/CdpManagerStorage.sol#L143-L148

minuteDecayFactor value is not exceeds the MAX_MINUTE_DECAY_FACTOR = 999999999999999999 because this check _minuteDecayFactor <= MAX_MINUTE_DECAY_FACTOR in setMinuteDecayFactor() function. So we can downcast this to uint248 so down casting is safe.

        require(
            _minuteDecayFactor <= MAX_MINUTE_DECAY_FACTOR,
            "CDPManager: new minute decay factor out of range"
        );
FILE: 2023-10-badger/packages/contracts/contracts/CdpManagerStorage.sol

 bool public redemptionsPaused;
    /*
     * Half-life of 12h. 12h = 720 min
     * (1/2) = d^720 => d = (1/2)^(1/720)
     */
-    uint256 public minuteDecayFactor = 999037758833783000;
+    uint248 public minuteDecayFactor = 999037758833783000;
    uint256 public constant MIN_MINUTE_DECAY_FACTOR = 1; // Non-zero
    uint256 public constant MAX_MINUTE_DECAY_FACTOR = 999999999999999999; // Corresponds to a very fast decay rate, but not too extreme

[G-3] Remove or replace unused state variables

Saves 40000 GAS with 2 instances

Saves a storage slot. If the variable is assigned a non-zero value, saves Gsset (20000 gas). If it's assigned a zero value, saves Gsreset (2900 gas). If the variable remains unassigned, there is no gas savings unless the variable is public, in which case the compiler-generated non-payable getter deployment cost is saved. If the state variable is overriding an interface's public function, mark the variable as constant or immutable so that it does not use a storage slot.

FILE: 2023-10-badger/packages/contracts/contracts/Dependencies/RolesAuthority.sol

30: EnumerableSet.Bytes32Set internal enabledFunctionSigsPublic;

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/Dependencies/RolesAuthority.sol#L30

FILE: 2023-10-badger/packages/contracts/contracts/Governor.sol

17: bytes32 NO_ROLES = bytes32(0);

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/Governor.sol#L17

[G-4] systemCollShares should be cached

Saves 100 GAS ,1 SLOD

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/ActivePool.sol#L294-L300

FILE: Breadcrumbs2023-10-badger/packages/contracts/contracts/ActivePool.sol

+ uint256 systemCollShares_ = systemCollShares;
 require(
-            collateral.balanceOf(address(this)) >= collateral.getPooledEthByShares(systemCollShares),
+            collateral.balanceOf(address(this)) >= collateral.getPooledEthByShares(systemCollShares_ ),
            "ActivePool: Must repay Balance"
        );
        require(
-            collateral.sharesOf(address(this)) >= systemCollShares,
+            collateral.sharesOf(address(this)) >= systemCollShares_ ,
            "ActivePool: Must repay Share"
        );

[G-5] lastRedemptionTimestamp should be cached

Saves 400 GAS , 4 SLOD

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/CdpManager.sol#L689-L699

FILE: 2023-10-badger/packages/contracts/contracts/CdpManager.sol

 function _updateLastRedemptionTimestamp() internal {
+  uint256 lastRedemptionTimestamp_ = lastRedemptionTimestamp ;
-        uint256 timePassed = block.timestamp > lastRedemptionTimestamp
+        uint256 timePassed = block.timestamp > lastRedemptionTimestamp_ 
-            ? block.timestamp - lastRedemptionTimestamp
+            ? block.timestamp - lastRedemptionTimestamp_ 
            : 0;

        if (timePassed >= SECONDS_IN_ONE_MINUTE) {
            // Using the effective elapsed time that is consumed so far to update lastRedemptionTimestamp
            // instead block.timestamp for consistency with _calcDecayedBaseRate()
-            lastRedemptionTimestamp += _minutesPassedSinceLastRedemption() * SECONDS_IN_ONE_MINUTE;
+            lastRedemptionTimestamp = lastRedemptionTimestamp_ + _minutesPassedSinceLastRedemption() * SECONDS_IN_ONE_MINUTE;
            emit LastRedemptionTimestampUpdated(block.timestamp);
        }
    }


 function _minutesPassedSinceLastRedemption() internal view returns (uint256) {

+  uint256 lastRedemptionTimestamp_ = lastRedemptionTimestamp ;
        return
-            block.timestamp > lastRedemptionTimestamp
+            block.timestamp > lastRedemptionTimestamp_ 
-                ? ((block.timestamp - lastRedemptionTimestamp) / SECONDS_IN_ONE_MINUTE)
+                ? ((block.timestamp - lastRedemptionTimestamp_ ) / SECONDS_IN_ONE_MINUTE)
                : 0;
    }

[G-6] Cdps[_redeemColFromCdp.cdpId].debt should be cached

Saves 100 GAS

FILE: 2023-10-badger/packages/contracts/contracts/CdpManager.sol

 + uint256 debt_ = Cdps[_redeemColFromCdp.cdpId].debt ;
singleRedemption.debtToRedeem = EbtcMath._min(
            _redeemColFromCdp.maxEBTCamount,
-            Cdps[_redeemColFromCdp.cdpId].debt /// @audit Redeem everything
+            debt_  /// @audit Redeem everything
        );

        singleRedemption.collSharesDrawn = collateral.getSharesByPooledEth(
            (singleRedemption.debtToRedeem * DECIMAL_PRECISION) / _redeemColFromCdp.price
        );

        // Repurposing this struct here to avoid stack too deep.
        CdpDebtAndCollShares memory _oldDebtAndColl = CdpDebtAndCollShares(
-            Cdps[_redeemColFromCdp.cdpId].debt,
+            debt_,
            Cdps[_redeemColFromCdp.cdpId].coll
        );

[G-7] getUserRoles[user] should be cached

Saves 100 GAS

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/Dependencies/RolesAuthority.sol#L154-L160

FILE: Breadcrumbs2023-10-badger/packages/contracts/contracts/Dependencies/RolesAuthority.sol

} else {
+  bytes32 user_ = getUserRoles[user] ;
-            getUserRoles[user] &= ~bytes32(1 << role);
+            user_ &= ~bytes32(1 << role);

            // Remove user if no more roles
-            if (getUserRoles[user] == bytes32(0)) {
+            if (user_ == bytes32(0)) {
                users.remove(user);
            }

[G-8] The result of sortedCdps.nonExistId() call should be cached rather than re-calling the function

The instances below point to the second+ call of the function within a single function

Saves 1 external call and 2100 GAS

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/CdpManager.sol#L277-L285


function _isValidFirstRedemptionHint(
        bytes32 _firstRedemptionHint,
        uint256 _price
    ) internal view returns (bool) {
+  bytes32 nonExistId_ = sortedCdps.nonExistId() ;
        if (
-            _firstRedemptionHint == sortedCdps.nonExistId() ||
+            _firstRedemptionHint == nonExistId_ ||
            !sortedCdps.contains(_firstRedemptionHint) ||
            getSyncedICR(_firstRedemptionHint, _price) < MCR
        ) {
            return false;
        }

        bytes32 nextCdp = sortedCdps.getNext(_firstRedemptionHint);
-        return nextCdp == sortedCdps.nonExistId() || getSyncedICR(nextCdp, _price) < MCR;
+        return nextCdp == nonExistId_ || getSyncedICR(nextCdp, _price) < MCR;
    }

[G-9] Emit memory or constant variables instead of state variables

Emitting memory or constant variables instead of state variables in Solidity events can be a useful optimization technique, particularly in the context of gas efficiency and contract optimization

FILE: Breadcrumbs2023-10-badger/packages/contracts/contracts/CdpManager.sol

 53:  stakingRewardSplit = STAKING_REWARD_SPLIT;
54:        // Emit initial value for analytics
- 55:        emit StakingRewardSplitSet(stakingRewardSplit);
+ 55:        emit StakingRewardSplitSet(STAKING_REWARD_SPLIT);

[G-10] Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate

Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the keyโ€™s keccak256 hash (Gkeccak256 - 30 gas) and that calculationโ€™s associated stack operations.

FILE: 2023-10-badger/packages/contracts/contracts/Dependencies/RolesAuthority.sol

32: mapping(address => bytes32) public getUserRoles;
36: mapping(address => mapping(bytes4 => bytes32)) public getRolesWithCapability;

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/Dependencies/RolesAuthority.sol#L32-L36

#0 - c4-pre-sort

2023-11-17T14:43:20Z

bytes032 marked the issue as sufficient quality report

#1 - jhsagd76

2023-11-28T02:52:36Z

How to measure the increase in gas cost when decode

#2 - c4-judge

2023-11-28T02:52:43Z

jhsagd76 marked the issue as grade-c

#3 - sathishpic22

2023-12-06T08:53:48Z

Hi @jhsagd76

All of my findings have already been proven, and not increase any gas after optimization.

[G-1] Optimizing storage usage with packed structs

This is already proven finding accepted many old contest. This will not increase any gas cost.

[G-1.1] user,isCollIncrease,isDebtIncrease can be packed with same slot : Saves 2 SLOT , 4000 GAS

[G-1.2] roundEthBtcId , roundStEthEthId , success can be packed with same slot : Saves 1 SLOT , 2000 GAS

Without struct optimization bool will occupy the 1 SLOT and incurs the gas overhead

[G-1.3] timestamp , success can be packed with same slot : Saves 1 SLOT , 2000 GAS

Usually for timestamp uint32 mostly used in many cases and many protocols.

[G-2] State variables can be packed into fewer storage slots

[G-2.1] minuteDecayFactor and redemptionsPaused can be packed within same slot : Saves 2000 GAS , 1 SLOT

This state variable optimization is safe. because minuteDecayFactor is not exceed the MAX_MINUTE_DECAY_FACTOR this constant value (999999999999999999). In safe hand my suggestion is uint248 . This will not harm protocol and not increase any gas after optimizations .

[G-3] Remove or replace unused state variables

Unused state variables removed or replaced to reduce the gas cost

[G-4] systemCollShares should be cached [G-5] lastRedemptionTimestamp should be cached

Instead of calling storage variables repeatedly when caching this with memory variable saves the gas.

[G-6] Cdps[_redeemColFromCdp.cdpId].debt should be cached [G-7] getUserRoles[user] should be cached

Mappings should be cached instead of calling repeatedly

[G-8] The result of sortedCdps.nonExistId() call should be cached rather than re-calling the function

Repeated function calls should be cached intead calling same function again and again

[G-9] Emit memory or constant variables instead of state variables

Emitting contract variable consumes less gas than state variable.

[G-10] Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate

I am perplexed by the contradiction in my findings. Although I have submitted a greater number of low-impact findings than other wardens, my findings have contributed to significantly higher gas savings than any other reports. While it is true that some wardens have submitted a higher total number of findings, these findings are predominantly classified as low-impact. All of my findings fall under the low-impact category based on the established criteria.

Despite implementing gas optimizations, the gas consumption did not increase but rather decreased more effectively. My findings have been thoroughly validated and widely accepted in previous competitions.

Certainly, I'll be happy to provide further clarifications if needed.

Thank you

#4 - jhsagd76

2023-12-06T09:14:19Z

Upon reviewing the gas report again, I realized that I missed it while filtering out a significant amount of bot output. I will elevate this report.

#5 - c4-judge

2023-12-06T09:14:38Z

jhsagd76 marked the issue as grade-a

#6 - jhsagd76

2023-12-06T20:07:32Z

3 * 2 + 2 * 1 + 5 * 4

28

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