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
Rank: 10/39
Findings: 1
Award: $259.26
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0x11singh99
259.2593 USDC - $259.26
Possible Optimization =
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()) } } }
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 Optimization 1 =
_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(); } }
JUMP
and JUMPI
opcodes associated with internal
function calls, streamlining execution.Possible Optimization 2 =
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); }
CALLDATALOAD
and CALLDATACOPY
by optimizing data access patterns.Possible Optimization 1 =
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
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 =
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 }
Possible Optimization 3 =
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)); }
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 Optimization 1 =
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; }
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 =
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); }
Possible Optimization 1 =
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); } }
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 }
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