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: 23/113
Findings: 1
Award: $249.55
π Selected for report: 1
π Solo Findings: 0
π Selected for report: c3phas
Also found by: 0x11singh99, Raihan, dharma09, hunter_w3b, slvDev
249.5525 USDC - $249.55
GAS_FOR_RELAY
While we try our best to maintain readability in the provided code snippets, some functions have been truncated to highlight the affected portions.
It's important to note that during the implementation of these suggested changes, developers must exercise caution to avoid introducing vulnerabilities. Although the optimizations have been tested prior to these recommendations, it is the responsibility of the developers to conduct thorough testing again.
Code reviews and additional testing are strongly advised to minimize any potential risks associated with the refactoring process.
Note: some titles may be similar to the bot but the issues identified were missed by bots
Use immutable if you want to assign a permanent value at construction. Use constants if you already know the permanent value. Both get directly embedded in bytecode, saving SLOAD. Variables only set in the constructor and never edited afterwards should be marked as immutable, as it would avoid the expensive storage-writing operation in the constructor (around 20 000 gas per variable) and replace the expensive storage-reading operations (around 2100 gas per reading) to a less expensive value reading (3 gas)
Total Instances: 4 Gas per instance: 2100 Total Saved: 8400 Gas
bridgeToken
should be immutableFile: /src/bridge_adapters/DecentBridgeAdapter.sol 19: address bridgeToken;
diff --git a/src/bridge_adapters/DecentBridgeAdapter.sol b/src/bridge_adapters/DecentBridgeAdapter.sol index 69bf19e..daaf4f8 100644 --- a/src/bridge_adapters/DecentBridgeAdapter.sol +++ b/src/bridge_adapters/DecentBridgeAdapter.sol @@ -16,7 +16,7 @@ contract DecentBridgeAdapter is BaseAdapter, IBridgeAdapter { mapping(uint256 => uint16) lzIdLookup; mapping(uint16 => uint256) chainIdLookup; bool gasIsEth; - address bridgeToken; + address immutable bridgeToken;
executor
should be immutableFile: /src/DecentEthRouter.sol 15: IDecentBridgeExecutor public executor;
weth
and gasCurrencyIsEth
should be immutableFile: /src/DecentBridgeExecutor.sol 9: IWETH weth; 10: bool public gasCurrencyIsEth; // for chains that use ETH as gas currency
File: /src/UTB.sol 282: function callBridge( 283: uint256 amt2Bridge, 284: uint bridgeFee, 285: BridgeInstructions memory instructions 286: ) private returns (bytes memory) { 287: bool native = approveAndCheckIfNative(instructions, amt2Bridge); 288: return 289: IBridgeAdapter(bridgeAdapters[instructions.bridgeId]).bridge{ 290: value: bridgeFee + (native ? amt2Bridge : 0) 291: }( 292: amt2Bridge, 293: instructions.postBridge, 294: instructions.dstChainId, 295: instructions.target, 296: instructions.paymentOperator, 297: instructions.payload, 298: instructions.additionalArgs, 299: instructions.refund 300: ); 301: }
The above function makes a call to function approveAndCheckIfNative
whose implementation is as follows
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L207-L220
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; }
Now, note that in both functions we are reading the state variable IBridgeAdapter(bridgeAdapters[instructions.bridgeId])
If we in line the functionality of approveAndCheckIfNative
inside the function callBridge
we can avoid making this unnecessary sloads
diff --git a/src/UTB.sol b/src/UTB.sol index 3d05b38..c864b81 100644 --- a/src/UTB.sol +++ b/src/UTB.sol @@ -284,9 +284,20 @@ contract UTB is Owned { uint bridgeFee, BridgeInstructions memory instructions ) private returns (bytes memory) { - bool native = approveAndCheckIfNative(instructions, amt2Bridge); + + bool native; + IBridgeAdapter bridgeAdapter = IBridgeAdapter(bridgeAdapters[instructions.bridgeId]); + address bridgeToken = bridgeAdapter.getBridgeToken( + instructions.additionalArgs + ); + if (bridgeToken != address(0)) { + IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge); + native = false; + } + native = true; + return - IBridgeAdapter(bridgeAdapters[instructions.bridgeId]).bridge{ + bridgeAdapter.bridge{ value: bridgeFee + (native ? amt2Bridge : 0) }( amt2Bridge,
The function approveAndCheckIfNative
is no longer needed and we can simply delete the code
Note, if we implement the immutable finding, then we do not have to do this https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L68-L82
File: /src/DecentBridgeExecutor.sol 68: function execute( 69: address from, 70: address target, 71: bool deliverEth, 72: uint256 amount, 73: bytes memory callPayload 74: ) public onlyOwner { 75: weth.transferFrom(msg.sender, address(this), amount); 77: if (!gasCurrencyIsEth || !deliverEth) { 78: _executeWeth(from, target, amount, callPayload); 79: } else { 80: _executeEth(from, target, amount, callPayload); 81: } 82: }
The if condition checks the status of gasCurrencyIsEth
which is a state variable(SLOAD) then depending on the result, it will check deliverEth
which is a function parameter(mload)
The second check is way cheaper and thus we can use the rules of short circuiting to our advantage
diff --git a/src/DecentBridgeExecutor.sol b/src/DecentBridgeExecutor.sol index 1a47cb5..f33e301 100644 --- a/src/DecentBridgeExecutor.sol +++ b/src/DecentBridgeExecutor.sol @@ -74,12 +74,12 @@ contract DecentBridgeExecutor is IDecentBridgeExecutor, Owned { ) public onlyOwner { weth.transferFrom(msg.sender, address(this), amount); - if (!gasCurrencyIsEth || !deliverEth) { + if (!deliverEth || !gasCurrencyIsEth ) { _executeWeth(from, target, amount, callPayload); } else { _executeEth(from, target, amount, callPayload); } - }
File: /src/swappers/UniSwapper.sol 79: function _receiveAndWrapIfNeeded( 80: SwapParams memory swapParams 81: ) private returns (SwapParams memory _swapParams) { 82: if (swapParams.tokenIn != address(0)) { 83: IERC20(swapParams.tokenIn).transferFrom( 84: msg.sender, 85: address(this), 86: swapParams.amountIn 87: ); 88: return swapParams; 89: } 90: swapParams.tokenIn = wrapped; 91: IWETH(wrapped).deposit{value: swapParams.amountIn}(); 92: return swapParams; 93: }
diff --git a/src/swappers/UniSwapper.sol b/src/swappers/UniSwapper.sol index 929f710..c47e998 100644 --- a/src/swappers/UniSwapper.sol +++ b/src/swappers/UniSwapper.sol @@ -88,7 +88,7 @@ contract UniSwapper is UTBOwned, ISwapper { return swapParams; } swapParams.tokenIn = wrapped; - IWETH(wrapped).deposit{value: swapParams.amountIn}(); + IWETH(swapParams.tokenIn).deposit{value: swapParams.amountIn}(); return swapParams; }
File: /src/bridge_adapters/StargateBridgeAdapter.sol 154: function getDestAdapter(uint chainId) private view returns (address dstAddr) { 155: dstAddr = destinationBridgeAdapter[chainId]; 157: require( 158: dstAddr != address(0), 159: string.concat("dst chain address not set ") 160: ); 161: }
diff --git a/src/bridge_adapters/StargateBridgeAdapter.sol b/src/bridge_adapters/StargateBridgeAdapter.sol index c0339aa..b6b06fd 100644 --- a/src/bridge_adapters/StargateBridgeAdapter.sol +++ b/src/bridge_adapters/StargateBridgeAdapter.sol @@ -155,8 +155,7 @@ contract StargateBridgeAdapter is dstAddr = destinationBridgeAdapter[chainId]; require( - dstAddr != address(0), - string.concat("dst chain address not set ") + dstAddr != address(0),"dst chain address not set " ); }
File: /src/bridge_adapters/DecentBridgeAdapter.sol 81: function bridge( 82: uint256 amt2Bridge, 83: SwapInstructions memory postBridge, 84: uint256 dstChainId, 85: address target, 86: address paymentOperator, 87: bytes memory payload, 88: bytes calldata additionalArgs, 89: address payable refund 90: ) public payable onlyUtb returns (bytes memory bridgePayload) { 91: require( 92: destinationBridgeAdapter[dstChainId] != address(0), 93: string.concat("dst chain address not set ") 94: );
diff --git a/src/bridge_adapters/DecentBridgeAdapter.sol b/src/bridge_adapters/DecentBridgeAdapter.sol index 69bf19e..b32abe3 100644 --- a/src/bridge_adapters/DecentBridgeAdapter.sol +++ b/src/bridge_adapters/DecentBridgeAdapter.sol @@ -90,7 +90,7 @@ contract DecentBridgeAdapter is BaseAdapter, IBridgeAdapter { ) public payable onlyUtb returns (bytes memory bridgePayload) { require( destinationBridgeAdapter[dstChainId] != address(0), - string.concat("dst chain address not set ") + "dst chain address not set " );
GAS_FOR_RELAY
File: /src/DecentEthRouter.sol 96: uint256 GAS_FOR_RELAY = 100000; 97: uint256 gasAmount = GAS_FOR_RELAY + _dstGasForCall;
@@ -18,8 +18,9 @@ contract DecentEthRouter is IDecentEthRouter, IOFTReceiverV2, Owned { uint8 public constant MT_ETH_TRANSFER_WITH_PAYLOAD = 1; uint16 public constant PT_SEND_AND_CALL = 1; + uint256 public constant GAS_FOR_RELAY = 100000; @@ -93,7 +94,7 @@ contract DecentEthRouter is IDecentEthRouter, IOFTReceiverV2, Owned { bytes memory payload ) { - uint256 GAS_FOR_RELAY = 100000; + uint256 gasAmount = GAS_FOR_RELAY + _dstGasForCall; adapterParams = abi.encodePacked(PT_SEND_AND_CALL, gasAmount); destBridge = bytes32(abi.encode(destinationBridges[_dstChainId]));
File: /src/DecentEthRouter.sol 60: modifier userIsWithdrawing(uint256 amount) { 61: uint256 balance = balanceOf[msg.sender]; 62: require(balance >= amount, "not enough balance"); 63: _; 64: balanceOf[msg.sender] -= amount; 65: }
We are caching the value of balanceOf[msg.sender]
on line 61 but due to how we perform the arithmetic operation on line 64, we cannot take advantage of the cached variable(using the short form -=
utilizes the first variable). We can change this to the long form (x = x - y
) which would allow us to use the variable balance
which saves us an entire SLOAD(100 Gas)
uint256 balance = balanceOf[msg.sender]; require(balance >= amount, "not enough balance"); _; - balanceOf[msg.sender] -= amount; + balanceOf[msg.sender] = balance - amount; }
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isnβt possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block see resource
File: /src/DecentEthRouter.sol 60: modifier userIsWithdrawing(uint256 amount) { 61: uint256 balance = balanceOf[msg.sender]; 62: require(balance >= amount, "not enough balance"); 63: _; 64: balanceOf[msg.sender] -= amount; 65: }
The check on line 62 ensures that the arithmetic operation on line 64 would never overflow since it would only be performed if balanceOf[msg.sender]
is greater than amount
@@ -61,7 +61,10 @@ contract DecentEthRouter is IDecentEthRouter, IOFTReceiverV2, Owned { uint256 balance = balanceOf[msg.sender]; require(balance >= amount, "not enough balance"); _; - balanceOf[msg.sender] -= amount; + unchecked{ + balanceOf[msg.sender] -= amount; + } + }
The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
File: /src/bridge_adapters/DecentBridgeAdapter.sol 55: return 56: router.estimateSendAndCallFee( 57: router.MT_ETH_TRANSFER_WITH_PAYLOAD(), 58: lzIdLookup[dstChainId], 59: target, 60: swapParams.amountIn, 61: dstGas, 62: false, 63: payload 64: );
File: /src/bridge_adapters/StargateBridgeAdapter.sol 100: function getValue( 101: bytes calldata additionalArgs, 102: uint256 amt2Bridge 103: ) private view returns (uint value) { 104: (address bridgeToken, LzBridgeData memory lzBridgeData) = abi.decode( 105: additionalArgs, 106: (address, LzBridgeData) 107: ); 108: return 109: bridgeToken == stargateEth 110: ? (lzBridgeData.fee + amt2Bridge) 111: : lzBridgeData.fee; 112: }
It is important to emphasize that the provided recommendations aim to enhance the efficiency of the code without compromising its readability. We understand the value of maintainable and easily understandable code to both developers and auditors.
As you proceed with implementing the suggested optimizations, please exercise caution and be diligent in conducting thorough testing. It is crucial to ensure that the changes are not introducing any new vulnerabilities and that the desired performance improvements are achieved. Review code changes, and perform thorough testing to validate the effectiveness and security of the refactored code.
Should you have any questions or need further assistance, please don't hesitate to reach out.
#0 - raymondfam
2024-01-26T16:44:22Z
All findings are generically known to complement the bot report.
#1 - c4-pre-sort
2024-01-26T16:44:29Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-02-04T17:58:02Z
alex-ppg marked the issue as grade-a
#3 - c4-judge
2024-02-04T17:58:04Z
alex-ppg marked the issue as selected for report
#4 - alex-ppg
2024-02-04T17:59:13Z
One of the two reports with no penalties (i.e. incorrect findings), and shows a lot of manual effort was put into this.
The majority of the gas reports in this contest are statically generated with minimal manual effort, and this particular submission is a welcome exception. I do not have an issue with users relying on automated tools and then post-processing their outputs manually (i.e. like this Warden who may or may not have used tools but has zero invalids).