Platform: Code4rena
Start Date: 19/01/2024
Pot Size: $36,500 USDC
Total HM: 9
Participants: 113
Period: 3 days
Judge: 0xsomeone
Id: 322
League: ETH
Rank: 50/113
Findings: 1
Award: $38.84
đ Selected for report: 0
đ Solo Findings: 0
đ Selected for report: c3phas
Also found by: 0x11singh99, Raihan, dharma09, hunter_w3b, slvDev
38.8402 USDC - $38.84
bytes.concat()
Over abi.encodePacked
for Non-Hashing Concatenationabi.decode
to Extract Calldata Values More Efficientlymsg.sender
Explanation of Issue:
The gas optimization report addresses the integration of the approveAndCheckIfNative
logic directly into the callBridge
function. This modification aims to reduce redundancy and gas consumption by avoiding separate function calls and optimizing storage mapping accesses.
function callBridge( uint256 amt2Bridge, uint bridgeFee, BridgeInstructions memory instructions ) private returns (bytes memory) { bool native = approveAndCheckIfNative(instructions, amt2Bridge); return IBridgeAdapter(bridgeAdapters[instructions.bridgeId]).bridge{ value: bridgeFee + (native ? amt2Bridge : 0) }( amt2Bridge, instructions.postBridge, instructions.dstChainId, instructions.target, instructions.paymentOperator, instructions.payload, instructions.additionalArgs, instructions.refund ); } function approveAndCheckIfNative( BridgeInstructions memory instructions, uint256 amt2Bridge ) private returns (bool) { IBridgeAdapter bridgeAdapter = IBridgeAdapter(bridgeAdapters[instructions.bridgeId]); address bridgeToken = bridgeAdapter.getBridgeToken( instructions.additionalArgs ); if (bridgeToken != address(0)) { IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge); return false; } return true; }
https://github.com/code-423n4/2024-01-decent/blob/main//src/UTB.sol#L282-L301
https://github.com/code-423n4/2024-01-decent/blob/main//src/UTB.sol#L207-L220
function callBridge( uint256 amt2Bridge, uint bridgeFee, BridgeInstructions memory instructions ) private returns (bytes memory) { address adapter = bridgeAdapters[instructions.bridgeId]; IBridgeAdapter bridgeAdapter = IBridgeAdapter(adapter); address bridgeToken = bridgeAdapter.getBridgeToken(instructions.additionalArgs); if (bridgeToken != address(0)) { IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge); return bridgeAdapter.bridge{ value: bridgeFee + amt2Bridge }( amt2Bridge, instructions.postBridge, instructions.dstChainId, instructions.target, instructions.paymentOperator, instructions.payload, instructions.additionalArgs, instructions.refund ); } else { return bridgeAdapter.bridge{ value: bridgeFee }( amt2Bridge, instructions.postBridge, instructions.dstChainId, instructions.target, instructions.paymentOperator, instructions.payload, instructions.additionalArgs, instructions.refund ); } }
Reason:
The optimization involves integrating the logic of approveAndCheckIfNative
directly into the callBridge
function. The reasons for this optimization are:
Redundancy Reduction:
Combining the logic eliminates the need for a separate function call (approveAndCheckIfNative
), reducing redundancy and avoiding unnecessary overhead associated with additional function invocations.
Storage Access Optimization:
The adapter
variable is used to cache the result of bridgeAdapters[instructions.bridgeId]
directly within the callBridge
function, optimizing storage access by avoiding redundant mappings.
Code Readability: Consolidating the logic into a single function enhances code readability by providing a more straightforward and self-contained view of the bridge calling process. This contributes to better code maintainability.
By integrating the logic directly into the callBridge
function, gas efficiency is improved, resulting in potential savings during contract execution. The modified code remains clear and concise while adhering to best practices in smart contract development.
Explanation of Issue:
In Solidity versions prior to 0.8.10, the compiler inserted additional code, including the EXTCODESIZE
operation (costing 100 gas), to check for the existence of a contract before making an external function call. However, in more recent Solidity versions, these existence checks are skipped if the external call has a return value. To achieve similar gas-efficient behavior in earlier versions, low-level calls can be used since they inherently bypass contract existence checks.
Examples of Code:
Non-optimized Code:
contract NonOptimizedContract { function externalCallWithExistenceCheck(address _target) public view returns (uint256) { // Non-optimized: External call with compiler-inserted contract existence check return SomeExternalContract(_target).someFunction(); } }
Optimized Code:
contract OptimizedContract { function externalCallWithoutExistenceCheck(address _target) public view returns (uint256) { // Optimized: External call using low-level call, bypassing existence check (bool success, bytes memory result) = _target.staticcall(abi.encodeWithSignature("someFunction()")); require(success, "External call failed"); return abi.decode(result, (uint256)); } }
Reason:
The non-optimized code relies on the compiler-inserted contract existence check, which can incur an additional gas cost. In contrast, the optimized code uses a low-level call (staticcall
) to directly invoke the external function without a contract existence check.
By utilizing low-level calls, the optimized code achieves the same behavior as the recent Solidity versions without existence checks, making it more gas-efficient. This optimization is particularly relevant in scenarios where external calls are made frequently and the contract existence is guaranteed.
There are 21 instances of this issue:
2100
File: src/bridge_adapters/DecentBridgeAdapter.sol 109 IERC20(bridgeToken).transferFrom( 114 IERC20(bridgeToken).approve(address(router), amt2Bridge); 139 IERC20(swapParams.tokenIn).transferFrom( 145 IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn); 147 IUTB(utb).receiveFromBridge(
File: src/bridge_adapters/StargateBridgeAdapter.sol 88 IERC20(bridgeToken).transferFrom(msg.sender, address(this), amt2Bridge); 89 IERC20(bridgeToken).approve(address(router), amt2Bridge);
File: src/swappers/UniSwapper.sol 44 IERC20(token).transfer(user, amount); 55 IERC20(token).transfer(recipient, amount); 83 IERC20(swapParams.tokenIn).transferFrom( 91 IWETH(wrapped).deposit{value: swapParams.amountIn}(); 137 IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 138 amountOut = IV3SwapRouter(uniswap_router).exactInput(params) 158 IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 159 amountIn = IV3SwapRouter(uniswap_router).exactOutput(params);
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L44
File: src/UTB.sol 152 IERC20(tokenOut).approve(address(executor), amountOut); 216 IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTB.sol#L152
File: src/UTBExecutor.sol 61 IERC20(token).transferFrom(msg.sender, address(this), amount); 62 IERC20(token).approve(paymentOperator, amount); 73 uint remainingBalance = IERC20(token).balanceOf(address(this)) - 80 IERC20(token).transfer(refund, remainingBalance);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBExecutor.sol#L61
Explanation of Issue: The current state variables in the contract have public visibility, which exposes them to potential security risks, increases complexity, and incurs higher gas costs. It is recommended to change the visibility to private unless there is a specific need for public access.
Examples of Code:
contract ExampleContract { // Non-optimized: Public state variable uint256 public publicVariable; // Additional code... }
contract ExampleContract { // Optimized: Private state variable uint256 private privateVariable; // Additional code... }
Reason:
Security: Public state variables are susceptible to unauthorized access and modification, posing security risks to the contract. Changing the visibility to private limits access to these variables, reducing the potential for malicious attacks.
Encapsulation: Private state variables encapsulate the internal workings of the contract, promoting better code organization and readability. By exposing only necessary interfaces, the contract becomes easier to understand and maintain.
Gas Costs: Public state variables come with higher gas costs for reading and writing, as Solidity generates additional getter and setter functions. Using private state variables helps reduce gas costs, making the contract more efficient in terms of execution on the Ethereum blockchain.
There are 13 instance(s) of this issue:
File: src/DcntEth.sol 6 address public router;
File: src/DecentBridgeExecutor.sol 10 bool public gasCurrencyIsEth; // for chains that use ETH as gas currency
File: src/DecentEthRouter.sol IWETH public weth; IDcntEth public dcntEth; IDecentBridgeExecutor public executor; uint8 public constant MT_ETH_TRANSFER = 0; uint8 public constant MT_ETH_TRANSFER_WITH_PAYLOAD = 1; uint16 public constant PT_SEND_AND_CALL = 1; bool public gasCurrencyIsEth; // for chains that use ETH as gas currency
File: src/bridge_adapters/BaseAdapter.sol 7 address public bridgeExecutor;
https://github.com/code-423n4/2024-01-decent/blob/main/src/bridge_adapters/BaseAdapter.sol#L7
File: src/bridge_adapters/StargateBridgeAdapter.sol 24 address public stargateEth;
File: src/swappers/UniSwapper.sol 17 address public uniswap_router; 18 address payable public wrapped;
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L17-L18
Explanation of Issue:
In certain scenarios, opting for a hardcoded address over the use of address(this)
can lead to more gas-efficient smart contracts. This recommendation is particularly relevant when the same contract address is required multiple times within the contract code.
Examples of Code:
Non-optimized Code:
contract MyContract { #L function doSomething() public { // Using address(this) require(msg.sender == address(this), "Caller is not authorized"); // Do something } }
Optimized Code:
contract MyContract { address public immutable myAddress = 0x1234567890123456789012345678901234567890; #L function doSomething() public { // Using hardcoded address require(msg.sender == myAddress, "Caller is not authorized"); // Do something } }
Reason:
The optimization stems from the fact that utilizing address(this)
necessitates an additional EXTCODESIZE operation to retrieve the contract's address from its bytecode. This additional operation contributes to increased gas costs. By pre-calculating and incorporating a hardcoded address, the need for the extra operation is eliminated, resulting in a reduction in the overall gas cost of the contract.
This optimization becomes particularly impactful when the contract address is referenced multiple times throughout the code. In such cases, the use of a hardcoded address can significantly enhance the gas efficiency of the smart contract. References
There are 20 instance(s) of this issue:
File: src/DecentBridgeExecutor.sol 30 uint256 balanceBefore = weth.balanceOf(address(this)); 41 (balanceBefore - weth.balanceOf(address(this))); 75 weth.transferFrom(msg.sender, address(this), amount);
File: src/DecentEthRouter.sol 51 require(weth.balanceOf(address(this)) > amount, "not enough reserves"); 181 weth.transferFrom(msg.sender, address(this), _amount); 186 address(this), // from address that has dcntEth (so DecentRouter) 186 if (weth.balanceOf(address(this)) < _amount) { 288 dcntEth.transferFrom(msg.sender, address(this), amount); 297 dcntEth.transferFrom(msg.sender, address(this), amount); 309 dcntEth.mint(address(this), msg.value); 316 dcntEth.burn(address(this), amount); 325 weth.transferFrom(msg.sender, address(this), amount); 326 dcntEth.mint(address(this), amount); 333 dcntEth.burn(address(this), amount);
File: src/UTBExecutor.sol 59 uint initBalance = IERC20(token).balanceOf(address(this)); 61 IERC20(token).transferFrom(msg.sender, address(this), amount); 73 uint remainingBalance = IERC20(token).balanceOf(address(this)) -
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBExecutor.sol#L59
File: src/swappers/UniSwapper.sol 85 address(this), 132 recipient: address(this), 152 recipient: address(this),
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L85
Explanation of Issue:
When dealing with structs or arrays and fetching data from a storage location, opting for memory variables can result in higher gas costs. This is because assigning the data to a memory variable causes all fields of the struct or array to be read from storage, incurring a Gcoldsload (2100 gas) for each field.
Instead of declaring the variable with the memory keyword, it is more gas-efficient to declare it with the storage keyword. Additionally, caching any fields that need to be re-read in stack variables can further optimize gas usage. This approach ensures that only the fields actually read incur the Gcoldsload, resulting in substantial gas savings.
Examples of Code:
Non-optimized Code:
contract MyContract { struct MyStruct { uint256 field1; uint256 field2; // ... more fields } function fetchData() public view returns (uint256) { MyStruct memory myData = storageData; // Gas-inefficient return myData.field1; } MyStruct storage storageData; // Assume storageData is assigned elsewhere }
Optimized Code:
contract MyContract { struct MyStruct { uint256 field1; uint256 field2; // ... more fields } function fetchData() public view returns (uint256) { MyStruct storage myData = storageData; // Gas-optimized return myData.field1; } MyStruct storage storageData; // Assume storageData is assigned elsewhere }
Reason:
The optimization lies in minimizing gas costs associated with storage reads. When declaring variables with the memory keyword, the gas cost increases due to Gcoldsload operations for each field of the struct or array. By utilizing the storage keyword and selectively caching fields in stack variables, unnecessary Gcoldsload operations are avoided, leading to more gas-efficient smart contracts. Reading the whole struct or array into a memory variable makes sense only when the
There are 3 instances of this issue:
File: src/DecentEthRouter.sol 170 ICommonOFT.LzCallParams memory callParams = ICommonOFT.LzCallParams({
File: src/swappers/UniSwapper.sol 129 IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter 149 IV3SwapRouter.ExactOutputParams memory params = IV3SwapRouter
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L129
Explanation of Issue: If the data can fit in 32 bytes, the bytes32 data type can be used instead of bytes or strings, as it is less expensive in terms of gas consumption.
Examples of Code:
string public constant myString = "Hello, world!";
bytes32 public constant myBytes = bytes32("Hello, world!");
Reason:
Using bytes32 instead of strings for constants that fit within 32 bytes can lead to gas savings. Here's why:
Gas Cost: The storage and memory costs of working with bytes32 are lower compared to strings. Strings are dynamic arrays in Solidity, requiring additional gas for storage and memory allocation. Using bytes32 for small constant values eliminates these extra costs.
Efficiency: bytes32 is a fixed-size data type, while strings are variable-length. By using bytes32, you ensure that the storage and memory requirements are constant, resulting in more efficient contract execution.
As a smart contract Solidity auditor and gas optimizer, it is important to consider the size and nature of the data when choosing between bytes32, bytes, and strings. Opting for bytes32 instead of strings for small constant values can lead to gas savings without compromising functionality. However, it's crucial to verify that the data can indeed fit within 32 bytes to avoid truncation or unexpected behavior.
There are 1 instances of this issue:
File: src/UTBFeeCollector.sol 10 string constant BANNER = "\x19Ethereum Signed Message:\n32";
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L10
Explanation of Issue:
When hashing values, employing assembly instead of Solidity can lead to significant gas savings. This optimization is particularly evident when using the keccak256 function.
Examples of Code:
Non-optimized Code:
contract NonOptimizedContract { function solidityHash(uint256 a, uint256 b) public view returns (bytes32) { // Unoptimized return keccak256(abi.encodePacked(a, b)); } }
Optimized Code:
contract OptimizedContract { function assemblyHash(uint256 a, uint256 b) public view returns (bytes32) { // Optimized bytes32 hashedVal; assembly { mstore(0x00, a) mstore(0x20, b) hashedVal := keccak256(0x00, 0x40) } return hashedVal; } }
Reason:
The optimization involves using assembly to directly hash values, resulting in substantial gas savings. In the non-optimized code, the Solidity approach using keccak256(abi.encodePacked(a, b))
incurs a higher gas cost.
In the optimized code, the assembly approach utilizes the mstore
instruction to efficiently load values into memory and then applies the keccak256 function. This results in a more gas-efficient hashing operation, as demonstrated by the reduced gas cost in the optimized code.
Utilizing assembly for hashing is particularly advantageous when optimizing for gas costs in smart contracts, making it a valuable technique for gas-conscious developers.
There are 1 instances of this issue:
File: src/UTBFeeCollector.sol 49 bytes32 constructedHash = keccak256( abi.encodePacked(BANNER, keccak256(packedInfo)) );
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L49-L51
Explanation of Issue: When a stack variable is employed as a transient cache for a state variable, and this stack variable is only accessed once, it leads to unnecessary gas consumption. In such cases, it is more gas-efficient to use the state variable directly, avoiding the additional gas cost associated with the redundant stack assignment.
Examples of Code: Non-optimized Code:
uint256 public totalBalance; function updateBalance(uint256 newAmount) external { uint256 tempBalance = totalBalance; // Redundant stack assignment // Operations on tempBalance totalBalance = tempBalance + newAmount; }
Optimized Code:
uint256 public totalBalance; function updateBalance(uint256 newAmount) external { // Operations directly on totalBalance totalBalance = totalBalance + newAmount; // Gas-efficient, eliminates redundant stack assignment }
Reason:
In the non-optimized example, a stack variable (tempBalance
) is introduced to temporarily hold the value of the state variable (totalBalance
). However, since tempBalance
is only accessed once, it is more gas-efficient to perform the operations directly on the state variable, eliminating the need for an unnecessary stack assignment. This optimization reduces gas consumption, contributing to a more economical contract execution.
There are 1 instances of this issue:
the balance
is use only once in the userIsWithdrawing
modifier.
File: src/DecentEthRouter.sol 61 uint256 balance = balanceOf[msg.sender];
Fix code:
modifier userIsWithdrawing(uint256 amount) { - uint256 balance = balanceOf[msg.sender]; - require(balance >= amount, "not enough balance"); + require(balanceOf[msg.sender] >= amount, "not enough balance"); _; balanceOf[msg.sender] -= amount; }
Explanation of Issue: Common math operations, such as min and max, may have more gas-efficient alternatives. In scenarios where unoptimized code uses conditional operators, like the ternary operator, the resulting conditional jumps in opcodes can incur higher gas costs. Exploring gas-efficient alternatives for these operations is crucial for optimizing smart contract performance.
Examples of Code: Non-optimized Code:
function max(uint256 x, uint256 y) public pure returns (uint256 z) { z = x > y ? x : y; // Unoptimized conditional operator }
Optimized Code:
function max(uint256 x, uint256 y) public pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, y), gt(y, x))) // Gas-efficient alternative } }
Reason: In the non-optimized example, the ternary operator is used for the max function, introducing conditional jumps in the opcodes, which can be gas-costly. The optimized code provides a gas-efficient alternative using assembly code. By avoiding conditional operators, the gas consumption is reduced, leading to improved contract efficiency. The Solady Library offers additional gas-efficient math operations, making it valuable for developers seeking optimization opportunities.
There are 2 instance(s) of this issue:
File: src/bridge_adapters/StargateBridgeAdapter.sol 109 bridgeToken == stargateEth ? (lzBridgeData.fee + amt2Bridge) : lzBridgeData.fee;
File: src/swappers/UniSwapper.sol 115 uint amt2Recipient = swapParams.direction == SwapDirection.EXACT_OUT ? swapParams.amountOut : swapParams.amountIn;
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L115-L117
Explanation of Issue: By structuring conditional flows in a positive manner, you can save gas by avoiding the use of the NOT opcode.
Examples of Code:
bool public isTrue; function checkCondition() public { if (!isTrue) { // Code to be executed if condition is false } else { // Code to be executed if condition is true } }
bool public isTrue; function checkCondition() public { if (isTrue) { // Code to be executed if condition is true } else { // Code to be executed if condition is false } }
Reason:
The optimization revolves around avoiding the use of the NOT opcode (!) in the conditional flow. Here's the reason behind this optimization:
There are 1 instance(s) of this issue:
File: src/DecentBridgeExecutor.sol 77 if (!gasCurrencyIsEth || !deliverEth) { _executeWeth(from, target, amount, callPayload); } else { _executeEth(from, target, amount, callPayload); }
Fix code:
- if (!gasCurrencyIsEth || !deliverEth) { + if (gasCurrencyIsEth || deliverEth) { - _executeWeth(from, target, amount, callPayload); + _executeEth(from, target, amount, callPayload); } else { - _executeEth(from, target, amount, callPayload); + _executeWeth(from, target, amount, callPayload); }
Explanation of Issue:
Performing consecutive external calls with similar function signatures can result in redundant operations and increased gas costs. The use of assembly provides an opportunity to optimize gas consumption by reusing function signatures, parameters, and memory space.
Examples of Code:
Non-optimized Code:
contract NonOptimizedContractA { function nonOptimizedCalls(uint256 param) public { // Non-optimized back-to-back calls ExternalContractB.externalFunctionB(param); ExternalContractC.externalFunctionC(param); } } contract ExternalContractB { function externalFunctionB(uint256 param) external { // Perform external call B // ... } } contract ExternalContractC { function externalFunctionC(uint256 param) external { // Perform external call C // ... } }
Optimized Code:
contract OptimizedContractD { address public externalContractBAddress; address public externalContractCAddress; constructor(address _externalContractBAddress, address _externalContractCAddress) { externalContractBAddress = _externalContractBAddress; externalContractCAddress = _externalContractCAddress; } function optimizedCalls(uint256 param) public { // Optimized back-to-back calls using assembly assembly { // Cache the free memory pointer let freeMemoryPointer := mload(0x40) // Perform external call to ExternalContractB.externalFunctionB let successB := call(gas(), sload(externalContractBAddress.slot), 0, 0, param, 0, 0) // Restore the free memory pointer mstore(0x40, freeMemoryPointer) // Reset the zero slot if necessary if eq(successB, 0) { sstore(0, 0) } // Cache the free memory pointer again freeMemoryPointer := mload(0x40) // Perform external call to ExternalContractC.externalFunctionC let successC := call(gas(), sload(externalContractCAddress.slot), 0, 0, param, 0, 0) // Restore the free memory pointer mstore(0x40, freeMemoryPointer) // Reset the zero slot if necessary if eq(successC, 0) { sstore(0, 0) } } } }
Reason:
The non-optimized code involves separate external calls within NonOptimizedContractA
, resulting in redundant operations and potential memory expansion costs.
In the optimized code (OptimizedContractD
), assembly is used to efficiently reuse function signatures, parameters, and memory space for back-to-back external calls to ExternalContractB.externalFunctionB
and ExternalContractC.externalFunctionC
. The free memory pointer is cached and restored, reducing potential memory expansion costs. This optimization results in a more gas-efficient implementation when similar external calls are performed consecutively.
There are 4 instance(s) of this issue:
File: src/DecentBridgeExecutor.sol 30 uint256 balanceBefore = weth.balanceOf(address(this)); 31 weth.approve(target, amount);
File: src/bridge_adapters/DecentBridgeAdapter.sol 139 IERC20(swapParams.tokenIn).transferFrom( msg.sender, address(this), swapParams.amountIn ); IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn);
File: src/bridge_adapters/StargateBridgeAdapter.sol 88 IERC20(bridgeToken).transferFrom(msg.sender, address(this), amt2Bridge); IERC20(bridgeToken).approve(address(router), amt2Bridge);
File: src/UTBExecutor.sol 61 IERC20(token).transferFrom(msg.sender, address(this), amount); 62 IERC20(token).approve(paymentOperator, amount);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBExecutor.sol#L61-L62
Explanation of Issue: The issue lies in the non-optimized code of ContractB, where unnecessary external calls are made to functions of ContractA. This results in increased gas consumption, as the same external calls are redundantly invoked within the internal functions.
Examples of Code:
Non-optimized Code:
// Contract B contract ContractB { // External reference to ContractA function AB(uint256 _valueR, uint256 _valueT,address _token) public { contractA(_token).R(_valueR); // External call to T function of ContractA contractA(_token).T(_valueT); } }
Optimized Code:
// Contract B contract ContractB { // External reference to ContractA function AB(uint256 _valueR, uint256 _valueT,address _token) public { ContractA contractA = ContractA(_token); contractA.R(_valueR); // External call to T function of ContractA contractA.T(_valueT); } }
Reason:
In the non-optimized code, unnecessary external calls to ContractA's functions are made separately in the AB
function of ContractB. The optimized code combines these external calls, avoiding redundant calls and improving gas efficiency. By caching the reference to ContractA, we eliminate redundant external function invocations, resulting in reduced gas costs during contract execution.
There are 7 instance(s) of this issue:
IERC20(swapParams.tokenIn)
in an IERC20
type variable, and then call the transferFrom
and approve
functions in side UTB.performSwap
function.File: src/UTB.sol 83 IERC20(swapParams.tokenIn).transferFrom( 90 IERC20(swapParams.tokenIn).approve(
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTB.sol#L83
IERC20(fees.feeToken)
in an IERC20
type variable, and then call the transferFrom
and approve
functions inside UTB.retrieveAndCollectFees
modifier.File: src/UTB.sol IERC20(fees.feeToken).transferFrom( msg.sender, address(this), fees.feeAmount ); IERC20(fees.feeToken).approve( address(feeCollector), fees.feeAmount );
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTB.sol#L236-L244
IERC20(token)
in an IERC20
type variable, and then call the transferFrom
and approve
and transfer
functions inside UTBExecutor.execute
function.File: src/UTBExecutor.sol 61 IERC20(token).transferFrom(msg.sender, address(this), amount); 62 IERC20(token).approve(paymentOperator, amount); 80 IERC20(token).transfer(refund, remainingBalance);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBExecutor.sol#L61-L62
IERC20(bridgeToken)
in an IERC20
type variable, and then call the transferFrom
and approve
functions inside DecentBridgeAdapter.bridge
function.File: src/bridge_adapters/DecentBridgeAdapter.sol 109 IERC20(bridgeToken).transferFrom( msg.sender, address(this), amt2Bridge ); IERC20(bridgeToken).approve(address(router), amt2Bridge);
IERC20(swapParams.tokenIn)
in an IERC20
type variable, and then call the transferFrom
and approve
functions inside DecentBridgeAdapter.receiveFromBridge
function.File: src/bridge_adapters/DecentBridgeAdapter.sol 139 IERC20(swapParams.tokenIn).transferFrom( msg.sender, address(this), swapParams.amountIn ); IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn);
IERC20(bridgeToken)
in an IERC20
type variable, and then call the transferFrom
and approve
functions inside StargateBridgeAdapter.bridge
function.File: src/bridge_adapters/StargateBridgeAdapter.sol 88 IERC20(bridgeToken).transferFrom(msg.sender, address(this), amt2Bridge); 89 IERC20(bridgeToken).approve(address(router), amt2Bridge);
Explanation of Issue:
Using a ternary operation instead of an if-else statement can result in reduced gas costs, making it a more gas-efficient choice for conditional assignments.
Examples of Code:
Non-optimized Code:
contract NonOptimizedContract { function conditionallyAssign(uint256 value) public pure returns (uint256) { // Non-optimized: Using if-else statement for conditional assignment if (value > 0) { return value; } else { return 0; } } }
Optimized Code:
contract OptimizedContract { function conditionallyAssign(uint256 value) public pure returns (uint256) { // Optimized: Using ternary operation for conditional assignment return (value > 0) ? value : 0; } }
Reason:
The non-optimized code utilizes an if-else statement for a simple conditional assignment. While this approach is readable, it involves additional opcodes, potentially leading to higher gas costs. In the optimized code, a ternary operation is used for the same conditional assignment. Ternary operations generally result in fewer opcodes and are therefore more gas-efficient. This optimization is particularly valuable in scenarios where gas-conscious development is a priority, and the conditionals involve simple assignments.
There are 2 instance(s) of this issue:
File: src/DecentBridgeExecutor.sol if (!gasCurrencyIsEth || !deliverEth) { _executeWeth(from, target, amount, callPayload); } else { _executeEth(from, target, amount, callPayload); }
File: src/UTBFeeCollector.sol if (token == address(0)) { payable(owner).transfer(amount); } else { IERC20(token).transfer(owner, amount); }
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L70-L74
Explanation of Issue: When calling an external function without specifying a gas limit, the called contract may consume all the remaining gas, causing the transaction to be reverted. To mitigate this risk, it is recommended to explicitly set a gas limit when making low-level external calls.
Examples of Code:
contract A { function callExternalContract(address externalContract, uint256 amount, bytes memory payload) external { (bool success, ) = externalContract.call{value: amount}(payload); require(success, "External call failed"); } }
contract A { function callExternalContract(address externalContract, uint256 amount, bytes memory payload) external { (bool success, ) = externalContract.call{value: amount, gas: 200000}(payload); require(success, "External call failed"); } }
Reason:
The optimization involves explicitly setting a gas limit when making low-level external calls to mitigate the risk of unlimited gas consumption. Here's the reasoning behind this optimization, considering the provided code snippet:
Gas Consumption Risk: When an external function is called without specifying a gas limit, the called contract has the potential to consume all the remaining gas. This can cause the transaction to be reverted and result in unexpected behavior.
Gas Limit Mitigation: By explicitly setting a gas limit when making low-level external calls, such as externalContract.call{value: amount, gas: 200000}(payload)
, you can control the maximum amount of gas that can be consumed by the called contract. This helps mitigate the risk of unlimited gas consumption and ensures that the transaction remains within the desired gas boundaries.
Value and Payload: The provided code also demonstrates passing an amount of Ether (value
) and a data payload (payload
) to the external contract. These considerations are essential when making external calls, but the gas optimization mainly focuses on mitigating the gas consumption risk.
There are 3 instances of this issue:
File: src/DecentBridgeExecutor.sol 61 (bool success, ) = target.call{value: amount}(callPayload);
File: src/UTBExecutor.sol 52 (success, ) = target.call{value: amount}(payload); 65 (success, ) = target.call{value: extraNative}(payload);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBExecutor.sol#L52
bytes.concat()
Over abi.encodePacked
for Non-Hashing ConcatenationExplanation of Issue:
When concatenating bytes and not intending to use the result for hashing, bytes.concat()
is a more gas-efficient alternative to abi.encodePacked
. Using bytes.concat()
can lead to reduced gas costs in scenarios where concatenation is performed on bytes variables.
Examples of Code:
Non-optimized Code:
contract NonOptimizedContract { function concatenateBytes(bytes memory a, bytes memory b) public pure returns (bytes memory) { // Non-optimized: Using abi.encodePacked for concatenation return abi.encodePacked(a, b); } }
Optimized Code:
contract OptimizedContract { function concatenateBytes(bytes memory a, bytes memory b) public pure returns (bytes memory) { // Optimized: Using bytes.concat for concatenation return bytes.concat(a, b); } }
Reason:
The non-optimized code uses abi.encodePacked
for concatenating bytes. While this approach is valid, it may result in higher gas costs, especially when concatenating large amounts of data.
In the optimized code, bytes.concat()
is utilized for non-hashing concatenation. This method is specifically designed for concatenating bytes variables and is generally more gas-efficient than abi.encodePacked
. By adopting this optimization, gas consumption can be reduced, making the smart contract more cost-effective in scenarios where byte concatenation is a common operation.
There are 1 instances of this issue:
File: src/UTBFeeCollector.sol bytes32 constructedHash = keccak256( abi.encodePacked(BANNER, keccak256(packedInfo)) );
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L49-L51
Explanation of Issue: Using interfaces to make external contract calls in Solidity can be inefficient in terms of memory utilization. Each such call involves creating a new memory location to store the data being passed, resulting in memory expansion costs. This can impact gas consumption, especially for contracts that make frequent external calls. However, using inline assembly allows for optimized memory usage by reusing already allocated memory spaces or utilizing scratch space for smaller datasets. This optimization can lead to notable gas savings. Furthermore, inline assembly enables important safety checks, such as verifying if the target address has code deployed to it using extcodesize(addr)
before making the call, mitigating risks associated with contract interactions.
Examples of Code:
interface ExternalContract { function someFunction(uint256 data) external returns (uint256); } contract MyContract { ExternalContract externalContract; function callExternalContract(uint256 data) external returns (uint256) { return externalContract.someFunction(data); } }
contract MyContract { function callExternalContract(address externalContract, uint256 data) external returns (uint256) { uint256 result; bool success; assembly { let freeMemoryPointer := mload(0x40) // Get the current free memory pointer // Verify if the target address has code deployed to it let codeSize := extcodesize(externalContract) if iszero(codeSize) { revert(0, 0) } // Prepare the data to be passed to the external contract mstore(freeMemoryPointer, data) let inputData := freeMemoryPointer let inputSize := 32 // Make the external call success := call(gas(), externalContract, 0, inputData, inputSize, freeMemoryPointer, 32) // Retrieve the result from the external call result := mload(freeMemoryPointer) } require(success, "External call failed"); return result; } }
Reason:
The optimization involves using inline assembly to optimize memory usage and improve gas efficiency when making external contract calls. Here's the reasoning behind this optimization:
Memory Efficiency: Using interfaces for external calls in Solidity incurs memory expansion costs, as each call involves creating a new memory location to store the data being passed. By utilizing inline assembly, memory can be allocated and reused more efficiently, resulting in reduced gas consumption.
Gas Savings: Optimized memory usage through inline assembly can lead to significant gas savings, especially for contracts that make frequent external calls. Reusing already allocated memory spaces or utilizing scratch space for smaller datasets avoids unnecessary memory expansion costs.
Safety Checks: Inline assembly allows for important safety checks before making external calls. For example, using extcodesize(addr)
verifies if the target address has code deployed to it, mitigating risks associated with calling non-existent contracts.
There are 16 instances of this issue:
File: src/DecentBridgeExecutor.sol 30 uint256 balanceBefore = weth.balanceOf(address(this)); 31 weth.approve(target, amount); 36 weth.transfer(from, amount); 44 weth.transfer(from, remainingAfterCall); 60 weth.withdraw(amount); 75 weth.transferFrom(msg.sender, address(this), amount);
File: src/swappers/UniSwapper.sol 137 IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 138 amountOut = IV3SwapRouter(uniswap_router).exactInput(params); 158 IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 159 amountIn = IV3SwapRouter(uniswap_router).exactOutput(params);
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L137
File: src/UTB.sol 83 IERC20(swapParams.tokenIn).transferFrom( 90 IERC20(swapParams.tokenIn).approve( 152 IERC20(tokenOut).approve(address(executor), amountOut); 211 IBridgeAdapter bridgeAdapter = IBridgeAdapter(bridgeAdapters[instructions.bridgeId]); 216 IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTB.sol#L83
File: src/UTBFeeCollector.sol 73 IERC20(token).transfer(owner, amount);
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L73
Explanation of Issue: The issue involves optimizing the sequencing of Solidity operations using short-circuit mode. Short-circuiting utilizes OR/AND logic to arrange operations based on their gas costs. By placing low gas cost operations at the front and high gas cost operations at the back, subsequent high-cost Ethereum virtual machine operations can be skipped (short-circuited) if the initial low-cost operation evaluates to true.
Examples of Code:
// Operations not sorted by gas cost function performOperations(uint256 x, uint256 y) public { // High gas cost operation if (g(y) || f(x)) { // Code to execute if the condition is true } }
// Operations sorted by gas cost using short-circuit mode function performOperations(uint256 x, uint256 y) public { // Low gas cost operation first if (f(x) || g(y)) { // Code to execute if the condition is true } }
Reason:
The reason for this optimization is to improve gas efficiency by leveraging short-circuiting. By sorting operations based on their gas costs, with low-cost operations placed before high-cost operations, subsequent high-cost Ethereum virtual machine operations can be skipped if the preceding low-cost operation evaluates to true. In the non-optimized code example, the g(y)
operation is executed first, and if it returns true (or a non-zero value), the f(x)
operation is not executed due to the short-circuit behavior of the ||
operator. However, this does not follow the intended gas optimization technique. In the optimized code example, the f(x)
operation is executed first, and if it returns true, the g(y)
operation is not executed. This optimization reduces unnecessary gas consumption and results in cost savings during contract execution.
There are 2 instances of this issue:
gasCurrencyIsEth
consumes more gas compared to accessing the function parameter deliverEth
.File: src/DecentBridgeExecutor.sol 77 if (!gasCurrencyIsEth || !deliverEth) {
fix code :
- if (!gasCurrencyIsEth || !deliverEth) { + if (!deliverEth || !gasCurrencyIsEth) {
File: src/DecentEthRouter.sol 272 if (!gasCurrencyIsEth || !deliverEth) {
note
: All instances are not found by bot raceExplanation of Issue: Gas optimization is a critical aspect of smart contract development, and identifying areas for improvement is essential. In this case, it is observed that using assembly to check for address(0) can result in significant gas savings.
Examples of Code:
// Solidity code without assembly optimization if (_addr == address(0)) { revert("zero address"); }
// Solidity code with assembly optimization assembly { if iszero(_addr) { mstore(0x00, "zero address"); revert(0x00, 0x20); } }
Reason:
The gas optimization is achieved by using assembly to explicitly check for the zero address condition. In the non-optimized code, the equality check is performed in high-level Solidity, which may result in higher gas consumption. By leveraging assembly, a direct comparison with the zero address can be made, and if the condition is met, gas-efficient operations such as mstore
and revert
are utilized.
There are 2 instances of this issue:
File: src/UTBFeeCollector.sol 55 if (fees.feeToken != address(0)) { 70 if (token == address(0)) {
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L55
abi.decode
to Extract Calldata Values More Efficientlynote
: All instances are not found by bot raceExplanation of Issue:
The current gas optimization report highlights the use of assembly over abi.decode
for extracting calldata values. By leveraging assembly, we can enhance the efficiency of decoding calldata values, ensuring that only the necessary values are decoded. This optimization is particularly beneficial in scenarios where there is a need to minimize gas consumption during contract execution.
Examples of Code:
// Solidity code using abi.decode for calldata decoding function extractData() external { (uint256 value1, uint256 value2) = abi.decode(msg.data, (uint256, uint256)); // Further code logic using value1 and value2 }
// Solidity code using assembly for efficient calldata decoding function extractData() external { uint256 value1; uint256 value2; assembly { // Offset of calldata values to be extracted (assuming 4-byte values) let offset := add(msg.data, 4) // Load values directly from calldata value1 := mload(offset) value2 := mload(add(offset, 32)) } // Further code logic using value1 and value2 }
Reason:
The optimization revolves around using assembly to directly extract calldata values instead of relying on the higher-level abi.decode
function. The non-optimized code utilizes abi.decode
to extract values, potentially decoding more information than needed. In contrast, the optimized code in assembly allows for precise extraction, loading only the necessary values directly from calldata.
There are 8 instances of this issue:
File: src/bridge_adapters/DecentBridgeAdapter.sol 51 SwapParams memory swapParams = abi.decode( 96 uint64 dstGas = abi.decode(additionalArgs, (uint64)); 103 SwapParams memory swapParams = abi.decode( 134 SwapParams memory swapParams = abi.decode(
File: src/bridge_adapters/StargateBridgeAdapter.sol 202 SwapParams memory swapParams = abi.decode(
File: src/DecentEthRouter.sol 251 (, , , , callPayload) = abi.decode(
File: src/UTB.sol 69 SwapParams memory swapParams = abi.decode( 181 SwapParams memory newPostSwapParams = abi.decode(
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTB.sol#L69
note
: All instances are not found by bot raceExplanation of Issue:
In Solidity, when an external function does not modify function parameters of type bytes memory
, string memory
, or [] memory
, it is recommended to use calldata
instead of memory
for these parameters. calldata
is a non-modifiable, non-persistent area where function arguments are stored, and it is generally cheaper in terms of gas compared to memory
. This optimization is particularly beneficial for arrays and complex data types when used in external function calls.
Examples of Code:
function processArray(uint256[] memory data) external { // Function logic using the data array }
function processArray(uint256[] calldata data) external { // Function logic using the data array }
Reason:
Using calldata
 instead of memory for function parameters in external functions saves gas by avoiding the need for copying data to memory
. This approach provides direct read-only access to function arguments from the input data, resulting in reduced gas consumption. It is particularly beneficial for large arrays or complex data types, as it eliminates the gas cost associated with copying data to memory. This optimization improves contract efficiency, reduces costs for users, and enhances overall performance.
There are 5 instance(s) of this issue:
File: src/DecentEthRouter.sol 243 bytes memory _payload
File: src/bridge_adapters/StargateBridgeAdapter.sol 185 bytes memory, // _srcAddress 189 bytes memory payload
File: src/swappers/UniSwapper.sol 33 SwapParams memory newSwapParams, 59 bytes memory swapPayload
https://github.com/code-423n4/2024-01-decent/blob/main/src/swappers/UniSwapper.sol#L33
note
: All instances are not found by bot raceExplanation of Issue: Using custom errors instead of revert()/require() strings can save gas. Custom errors are available from Solidity version 0.8.4.
Examples of Code:
function foo() public { require(msg.sender == owner, "Only the owner can call this function."); // ... }
error OnlyOwner(); function foo() public { if (msg.sender != owner) { revert OnlyOwner(); } // ... }
Reason: Custom errors are more efficient than revert()/require() strings because they consume less gas. They also provide more flexibility in error handling and can be used to provide more detailed error messages to users.
There are 2 instance(s) of this issue:
File: src/UTBFeeCollector.sol 29 require(signature.length == 65, "Invalid signature length"); 54 require(recovered == signer, "Wrong signature");
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L29
note
: All instances are not found by bot raceExplanation of Issue: The gas optimization report highlights the opportunity to mark constructors as payable in Solidity contracts. Payable functions, including constructors, incur lower gas costs compared to non-payable functions. Marking constructors as payable can result in gas savings, as the compiler eliminates extra checks that ensure payments are not provided. In the case of constructors, this optimization is safe because only the deployer has the ability to pass funds during contract deployment.
Examples of Code:
contract MyContract { uint256 public value; // Non-payable constructor constructor(uint256 _initialValue) { value = _initialValue; } }
contract MyContract { uint256 public value; // Payable constructor constructor(uint256 _initialValue) payable { value = _initialValue; } }
Reason:
The optimization involves marking the constructor as payable. The reasons for this optimization are:
Gas Cost Reduction: Payable functions, including constructors, have lower gas costs because the compiler does not need to add extra checks to ensure that a payment wasn't provided. By marking the constructor as payable, unnecessary gas-consuming checks are eliminated.
Deployer Exclusivity: Constructors are executed only during contract deployment, and only the deployer has the ability to pass funds. As a result, marking the constructor as payable is safe, as it aligns with the nature of constructor execution.
Avoidance of Extra Checks: By explicitly marking the constructor as payable, the developer signals to the compiler that no additional checks for non-payability are required. This leads to cleaner and more efficient contract code.
This optimization is particularly relevant in scenarios where gas efficiency is a critical consideration, contributing to a more cost-effective deployment and initialization of smart contracts on the blockchain.
There are 2 instance(s) of this issue:
File: src/DcntEth.sol 13 constructor(
File: src/UTBFeeCollector.sol 12 constructor() UTBOwned() {}
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L12
note
: All instances are not found by bot raceExplanation of Issue: The issue is related to unnecessary storage updates when the old value is equal to the new value. This results in unnecessary gas consumption.
Examples of Code:
// Storage update without checking for value change function updateValue(uint256 newValue) public { value = newValue; }
// Avoiding storage update when the value hasn't changed function updateValue(uint256 newValue) public { if (value != newValue) { value = newValue; } }
Reason: The reason for this optimization is to save gas by avoiding unnecessary storage updates. If the old value is equal to the new value, there is no need to update the storage, which saves the gas cost of a Gsreset operation (2900 gas). However, it's important to note that avoiding the storage update may result in a Gcoldsload operation (2100 gas) or a Gwarmaccess operation (100 gas) depending on the context of the contract execution.
There are 2 instance(s) of this issue:
File: src/DcntEth.sol 21 router = _router;
File: src/UTBFeeCollector.sol 19 signer = _signer;
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTBFeeCollector.sol#L19
msg.sender
note
: All instances are not found by bot raceExplanation of Issue:
When validating msg.sender
, using assembly can significantly reduce the number of opcodes required, leading to a more gas-efficient implementation.
Examples of Code:
Non-optimized Code:
contract NonOptimizedContract { function validateSender() public view { // Non-optimized: Standard Solidity validation of msg.sender require(msg.sender == address(0x123...), "Unauthorized"); } }
Optimized Code:
contract OptimizedContract { function validateSender() public view { // Optimized: Using assembly for efficient validation of msg.sender assembly { // Compare msg.sender directly in assembly if iszero(eq(sload(msg.sender.slot), 0x123...)) { // Revert if the comparison fails revert(0, 0) } } } }
Reason:
The non-optimized code performs a standard Solidity validation of msg.sender
, which involves multiple opcodes and can contribute to higher gas costs.
In the optimized code, assembly is used to efficiently validate msg.sender
with fewer opcodes. The iszero
and eq
assembly functions directly compare the msg.sender
value, resulting in a more gas-efficient implementation. This optimization is valuable when frequent and efficient validation of the sender's address is required in smart contracts.
There are 1 instance(s) of this issue:
File: src/DcntEth.sol 9 require(msg.sender == router);
#0 - raymondfam
2024-01-26T16:42:09Z
All findings except G-14 are generically known to complement the bot report.
#1 - c4-pre-sort
2024-01-26T16:42:16Z
raymondfam marked the issue as sufficient quality report
#2 - alex-ppg
2024-02-04T18:02:09Z
The following findings were penalized:
storage
unchecked
for a simple assignment in its second instance which is invalid#3 - c4-judge
2024-02-04T18:02:22Z
alex-ppg marked the issue as grade-b