Spectra - K42's results

A permissionless interest rate derivatives protocol on Ethereum.

General Information

Platform: Code4rena

Start Date: 23/02/2024

Pot Size: $36,500 USDC

Total HM: 2

Participants: 39

Period: 7 days

Judge: Dravee

Id: 338

League: ETH

Spectra

Findings Distribution

Researcher Performance

Rank: 10/39

Findings: 1

Award: $259.26

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0x11singh99

Also found by: K42, dharma09

Labels

bug
G (Gas Optimization)
grade-a
sponsor acknowledged
sufficient quality report
G-03

Awards

259.2593 USDC - $259.26

External Links

Gas Optimization Report for Spectra by K42

  • Note: I made sure these optimizations are unique in relation to the Bot Report and 4Analy3er Report.

Possible Optimization In AMBeacon.sol

Possible Optimization =

  • A unique optimization for this contract could involve leveraging this beacon contract as a lightweight proxy in certain contexts, reducing the need for separate proxy contracts for some interactions, thereby saving gas. This would involve adding a fallback function that delegates calls to the implementation address, effectively allowing the AMBeacon to serve dual purposes without compromising its primary function as a beacon.

Here is the optimized code snippet:

// Adding a fallback function to delegate calls to the implementation
fallback() external {
    address _impl = implementation();
    require(_impl != address(0), "Implementation not set");

    assembly {
        // Copy msg.data. We take full control of memory in this inline assembly
        // block because it will not return to Solidity code. We overwrite the
        // Solidity scratch pad at memory position 0.
        calldatacopy(0, 0, calldatasize())

        // Call the implementation.
        // out and outsize are 0 because we don't know the size yet.
        let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)

        // Copy the returned data.
        returndatacopy(0, 0, returndatasize())

        switch result
        // delegatecall returns 0 on error.
        case 0 { revert(0, returndatasize()) }
        default { return(0, returndatasize()) }
    }
}
  • Estimated gas saved = This optimization does not directly save gas in the context of the AMBeacon's operations but can reduce the overall gas cost and complexity within the ecosystem it supports by minimizing the number of contracts and transactions required for certain interactions.

Possible Optimizations In AMTransparentUpgradeableProxy.sol

Possible Optimization 1 =

  • The _proxyAdmin() function is called to retrieve the _admin immutable address. Since _admin is immutable, directly using _admin instead of calling _proxyAdmin() can save gas by reducing function call overhead.

Here is the optimized code snippet:

// Before optimization: Using _proxyAdmin() function to access _admin
function _fallback() internal virtual override {
    if (msg.sender == _proxyAdmin()) {
        if (msg.sig != IAMTransparentUpgradeableProxy.upgradeToAndCall.selector) {
            revert ProxyDeniedAdminAccess();
        } else {
            _dispatchUpgradeToAndCall();
        }
    } else {
        super._fallback();
    }
}

// After optimization: Directly using _admin
function _fallback() internal virtual override {
    if (msg.sender == _admin) { // Direct access
        if (msg.sig != IAMTransparentUpgradeableProxy.upgradeToAndCall.selector) {
            revert ProxyDeniedAdminAccess();
        } else {
            _dispatchUpgradeToAndCall();
        }
    } else {
        super._fallback();
    }
}
  • Estimated gas saved = Eliminates the JUMP and JUMPI opcodes associated with internal function calls, streamlining execution.

Possible Optimization 2 =

  • The _dispatchUpgradeToAndCall() function decodes the entire msg.data to extract newImplementation and data. This can be optimized by directly accessing msg.data without decoding it first, which is more efficient for extracting specific parts of the calldata.

Here is the optimized code:

// Before optimization: Decoding entire msg.data
function _dispatchUpgradeToAndCall() private {
    (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
    ERC1967Utils.upgradeToAndCall(newImplementation, data);
}

// After optimization: Use calldata slicing for efficiency
function _dispatchUpgradeToAndCall() private {
    address newImplementation = abi.decode(msg.data[4:36], (address));
    bytes memory data = msg.data.length > 36 ? msg.data[36:] : bytes("");
    ERC1967Utils.upgradeToAndCall(newImplementation, data, msg.value);
}
  • Estimated gas saved = The exact savings depend on the size of data being passed but can range from hundreds to thousands of gas. Reduces the use of CALLDATALOAD and CALLDATACOPY by optimizing data access patterns.

Possible Optimizations In PrincipalToken.sol

Possible Optimization 1 =

  • The contract initializes ibtUnit based on _ibtDecimals, which is set during the initialization and does not change thereafter. If _ibtDecimals is known and constant for all deployments, pre-computing this value can save gas.

Here is the optimized code snippet:

/// Original code uses a variable set in the initializer
uint256 private ibtUnit; // Set in initialize function as 10 ** _ibtDecimals

// Optimized approach with a constant expression
uint256 private constant IBT_UNIT = 10**18; // Assuming _ibtDecimals is always 18
  • Estimated gas saved = This change eliminates the need for runtime computation and storage of ibtUnit if _ibtDecimals is indeed constant. The gas savings occur during contract deployment and any function call that would have computed or read ibtUnit from storage.

Possible Optimization 2 =

  • The contract updates yields for users individually, which could lead to repetitive and inefficient gas usage, especially when multiple users' yields need updating simultaneously. A more efficient approach could involve batch processing of yield updates, reducing the overhead associated with individual transaction costs for each user.

Here is the optimized code:

// Before optimization: Individual yield updates
function updateYield(address _user) public override returns (uint256 updatedUserYieldInIBT) {
    // Logic for updating yield for a single user
}

// After optimization: Batch processing of yield updates
function updateYields(address[] calldata _users) external {
    for (uint i = 0; i < _users.length; i++) {
        address user = _users[i];
        (uint256 _ptRate, uint256 _ibtRate) = _updatePTandIBTRates();
        if (ibtRateOfUser[user] != _ibtRate) {
            ibtRateOfUser[user] = _ibtRate;
        }
        if (ptRateOfUser[user] != _ptRate) {
            ptRateOfUser[user] = _ptRate;
        }
        // Additional logic for batch updating yields...
    }
    // Emit an event or log success as needed
}
  • Estimated gas saved = Batch processing can significantly reduce gas costs by amortizing the fixed costs of transaction execution (such as the base transaction fee and contract execution overhead) across multiple yield updates

Possible Optimization 3 =

  • The contract makes external calls to view functions of another contract (IERC4626(ibt)). Using staticcall for these can save gas.

Here is the optimized code snippet:

// Before optimization: Using a regular call
uint256 ibtRate = IERC4626(ibt).previewRedeem(ibtUnit);

// After optimization: Directly using staticcall for a view function
(bool success, bytes memory data) = address(ibt).staticcall(abi.encodeWithSignature("previewRedeem(uint256)", ibtUnit));
uint256 ibtRate;
if (success) {
    ibtRate = abi.decode(data, (uint256));
}
  • Estimated gas saved = Utilizing staticcall for view functions can reduce the gas cost by avoiding the overhead associated with state-changing calls. The savings are more pronounced when these calls are made frequently.

Possible Optimizations In YieldToken.sol

Possible Optimization 1 =

  • The decimals() function makes an external call to the pt (Principal Token) contract to fetch the decimals. This can be optimized by caching the decimals value during initialization, assuming the decimals of the PT do not change.

Here is the optimized code snippet:

// Adding a state variable to cache decimals
uint8 private _ptDecimals;

// Modify the initialize function to cache the decimals value
function initialize(
    string calldata _name,
    string calldata _symbol,
    address _pt
) external initializer {
    __ERC20_init(_name, _symbol);
    __ERC20Permit_init(_name);
    pt = _pt;
    _ptDecimals = IERC20Metadata(pt).decimals(); // Cache the decimals
}

// Use the cached value for the decimals function
function decimals() public view virtual override returns (uint8) {
    return _ptDecimals;
}
  • Estimated gas saved = This optimization can save gas for each call to decimals() by avoiding an external call to the pt contract. The savings per call can be significant, especially when decimals() is called frequently in transactions, potentially saving thousands of gas. Reduces the DELEGATECALL operations by utilizing cached state variable instead.

Possible Optimization 2 =

  • The transfer() and transferFrom() functions call IPrincipalToken(pt).beforeYtTransfer every time a transfer occurs. This external call can be optimized by checking if the transfer is necessary (i.e., the amount is not zero and from is not equal to to) before making the call.

Here is the optimized code:

// Optimizing transfer to minimize external calls
function transfer(address to, uint256 amount) public virtual override returns (bool success) {
    if (amount > 0 && msg.sender != to) {
        IPrincipalToken(pt).beforeYtTransfer(msg.sender, to);
    }
    return super.transfer(to, amount);
}

// Optimizing transferFrom similarly
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool success) {
    if (amount > 0 && from != to) {
        IPrincipalToken(pt).beforeYtTransfer(from, to);
    }
    return super.transferFrom(from, to, amount);
}
  • Estimated gas saved = This optimization potentially saves gas by avoiding unnecessary external calls when the transfer amount is zero or when transferring tokens to oneself. The exact savings depend on how often these scenarios occur but can significantly reduce unnecessary gas usage in these edge cases.

Possible Optimizations In PrincipalTokenUtil.sol

Possible Optimization 1 =

  • The library frequently calculates fees using a divisor of 1e18 for percentages and fees. Precomputing the result of common fee rates can save gas by reducing runtime division operations.

Here is the optimized code snippet:

// Before optimization: Dynamic fee calculation
function _computeTokenizationFee(
    uint256 _amount,
    address _pt,
    address _registry
) internal view returns (uint256) {
    return
        _amount
            .mulDiv(IRegistry(_registry).getTokenizationFee(), FEE_DIVISOR, Math.Rounding.Ceil)
            .mulDiv(
                FEE_DIVISOR - IRegistry(_registry).getFeeReduction(_pt, msg.sender),
                FEE_DIVISOR,
                Math.Rounding.Ceil
            );
}

// After optimization: Use precomputed fee rates for common scenarios
// Assuming common fee rates are 0.5%, 1%, and 2% - precompute these
uint256 private constant FEE_RATE_05 = FEE_DIVISOR / 200; // 0.5% fee
uint256 private constant FEE_RATE_1 = FEE_DIVISOR / 100; // 1% fee
uint256 private constant FEE_RATE_2 = FEE_DIVISOR / 50; // 2% fee

function _computeTokenizationFeeOptimized(
    uint256 _amount,
    address _pt,
    address _registry
) internal view returns (uint256) {
    uint256 feeRate = IRegistry(_registry).getTokenizationFee();
    uint256 feeReduction = IRegistry(_registry).getFeeReduction(_pt, msg.sender);
    
    // Use precomputed rates for common fee percentages
    if (feeRate == FEE_RATE_05 || feeRate == FEE_RATE_1 || feeRate == FEE_RATE_2) {
        return _amount.mulDiv(feeRate - feeReduction, FEE_DIVISOR, Math.Rounding.Ceil);
    } else {
        return _amount.mulDiv(feeRate, FEE_DIVISOR, Math.Rounding.Ceil)
                      .mulDiv(FEE_DIVISOR - feeReduction, FEE_DIVISOR, Math.Rounding.Ceil);
    }
}
  • Estimated gas saved = This optimization can save gas by avoiding division operations for fee calculations, especially when the fees are among the precomputed rates. The exact savings depend on the frequency of fee calculations but could range from hundreds to thousands of gas per transaction. Reduces the number of DIV and MUL operations by utilizing precomputed constants for common scenarios.

Possible Optimization 2 =

Here is the optimized code:

// Before optimization: Separate utility function for conversion
function _convertToSharesWithRate(
    uint256 _assets,
    uint256 _rate,
    uint256 _ibtUnit,
    Math.Rounding _rounding
) internal pure returns (uint256 shares) {
    if (_rate == 0) {
        revert IPrincipalToken.RateError();
    }
    return _assets.mulDiv(_ibtUnit, _rate, _rounding);
}

// After optimization: Inline conversion in calling function
// Assuming this conversion logic is used in a specific context
function someFunctionUsingConversion(
    uint256 _assets,
    uint256 _rate,
    uint256 _ibtUnit
) internal pure returns (uint256 shares) {
    if (_rate == 0) {
        revert IPrincipalToken.RateError();
    }
    // Inlined conversion logic
    shares = _assets.mulDiv(_ibtUnit, _rate, Math.Rounding.Ceil); // Example rounding
}
  • Estimated gas saved = Inlining small utility functions can save the gas used for function calls, which includes gas for jumping to the function code and returning. This can save approximately 200-500 gas per inlined function call, depending on the EVM's specifics and the function's complexity. Eliminates the JUMP and JUMPI opcodes for function calls, streamlining execution to only use arithmetic and logic opcodes directly in the calling context.

#0 - c4-pre-sort

2024-03-03T14:00:40Z

gzeon-c4 marked the issue as sufficient quality report

#1 - c4-judge

2024-03-11T00:44:29Z

JustDravee marked the issue as grade-a

#2 - c4-sponsor

2024-03-11T18:01:05Z

jeanchambras (sponsor) acknowledged

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