Platform: Code4rena
Start Date: 22/09/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 175
Period: 14 days
Judge: alcueca
Total Solo HM: 4
Id: 287
League: ETH
Rank: 28/175
Findings: 2
Award: $261.04
š Selected for report: 0
š Solo Findings: 0
š Selected for report: rvierdiiev
Also found by: 0x11singh99, 0xAnah, 0xta, Aamir, DavidGiladi, DevABDee, Eurovickk, JCK, K42, MrPotatoMagic, Pessimistic, Raihan, Rolezn, SM3_SS, SY_S, Sathish9098, Udsen, ayo_dev, blutorque, c3phas, clara, dharma09, hihen, hunter_w3b, jamshed, koxuan, lsaudit, marqymarq10, oualidpro, pfapostol, sivanesh_808, tabriz, wahedtalash77, zabihullahazadzoi, ziyou-
17.7101 USDC - $17.71
Gas optimization techniques are critical in smart contract development to reduce transaction costs and make contracts more efficient. The Maia DAO - Ulysses protocol, as observed in the provided contract, can benefit from various gas-saving strategies to enhance its performance. Below is a summary of gas optimization techniques followed by categorized findings within the contracts.
Finding | Occurrences |
---|---|
Use mappings instead of arrays | 42 |
Using Storage instead of memory for structs/arrays saves gas | 14 |
UseĀ assemblyĀ to writeĀ address storage values | 40 |
A modifier used only once and not being inherited should be inlined to save gas | 12 |
ERC1155 is a cheaper non-fungible token than ERC721 | 3 |
Use one ERC1155 or ERC6909 token instead of several ERC20 tokens | 9 |
Using assembly to revert with an error message | 177 |
Split revert statements | 1 |
Understand the trade-offs when choosing between internal functions and modifiers | 27 |
Using > 0 costs more gas than != 0 | 17 |
Do-While loops are cheaper than for loops | 15 |
Use assembly to reuse memory space when making more than one external call | 44 |
Missing Zero address checks in the constructor | 14 |
Short-circuit booleans | 4 |
15 Pre-increment and pre-decrement are cheaper thanĀ +1 ,-1 | 4 |
Use hardcode address instead address(this) | 33 |
Use bytes.concat() instead of abi.encodePacked(), since this is preferred since 0.8.4 | 46 |
Shorten the array rather than copying to a new one | 14 |
Initializers can be marked as payable to save deployment gas | 9 |
Use assembly to validate msg.sender | 22 |
Use selfdestruct in the constructor if the contract is one-time use | |
Avoid contract calls by making the architecture monolithic | 8 |
It is sometimes cheaper to cache calldata | 7 |
Use ++i instead of i++ to increment | 4 |
Always use Named Returns | 16 |
Use bitmaps instead of bools when a significant amount of booleans are used | 10 |
Empty blocks should be removed or emit something | 5 |
Using mappings instead of arrays to avoid length checks
When storing a list or group of items that you wish to organize in a specific order and fetch with a fixed key/index, itās common practice to use an array data structure. This works well, but did you know that a trick can be implemented to save 2,000+ gas on each read using a mapping?
See the example below
/// get(0) gas cost: 4860 contract Array { uint256[] a; constructor() { a.push() = 1; a.push() = 2; a.push() = 3; } function get(uint256 index) external view returns(uint256) { return a[index]; } } /// get(0) gas cost: 2758 contract Mapping { mapping(uint256 => uint256) a; constructor() { a[0] = 1; a[1] = 2; a[2] = 3; } function get(uint256 index) external view returns(uint256) { return a[index]; } }
Just by using a mapping, we get a gas saving of 2102 gas. Why? Under the hood when you read the value of an index of an array, solidity adds bytecode that checks that you are reading from a valid index (i.e an index strictly less than the length of the array) else it reverts with a panic error (Panic(0x32) to be precise). This prevents from reading unallocated or worse, allocated storage/memory locations.
Due to the way mappings are (simply a key => value pair), no check like that exists and we are able to read from the a storage slot directly. Itās important to note that when using mappings in this manner, your code should ensure that you are not reading an out of bound index of your canonical array.
File: src/BranchPort.sol 35 address[] public bridgeAgents; 45 address[] public bridgeAgentFactories; 55 address[] public strategyTokens; 71 address[] public portStrategies;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L35
File: src/MulticallRootRouter.sol 32 address[] outputTokens; 34 uint256[] amountsOut; 36 uint256[] depositsOut;
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L32
File: src/RootPort.sol 67 address[] public bridgeAgents; 80 address[] public bridgeAgentFactories;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L67
File: src/factories/ERC20hTokenBranchFactory.sol 23 ERC20hTokenBranch[] public hTokens;
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L23
File: src/factories/ERC20hTokenRootFactory.sol 23 ERC20hTokenRoot[] public hTokens;
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol#L23
File: src/BranchBridgeAgent.sol 507 address[] memory _hTokens = new address[](numOfAssets); 508 address[] memory _tokens = new address[](numOfAssets); 509 uint256[] memory _amounts = new uint256[](numOfAssets); 510 uint256[] memory _deposits = new uint256[](numOfAssets); 827 address[] memory addressArray = new address[](1); 828 uint256[] memory uintArray = new uint256[](1);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L507-L510
File: src/interfaces/BridgeAgentStructs.sol 16 address[] hTokens; 17 address[] tokens; 18 uint256[] amounts; 19 uint256[] deposits; 32 address[] hTokens; //Input Local hTokens Address. 33 address[] tokens; //Input Native / underlying Token Address. 34 uint256[] amounts; //Amount of Local hTokens deposited for interaction. 35 uint256[] deposits; //Amount of native tokens deposited for interaction. 42 address[] hTokens; //Input Local hTokens Address. 43 address[] tokens; //Input Native / underlying Token Address. 44 uint256[] amounts; //Amount of Local hTokens deposited for interaction. 45 uint256[] deposits; //Amount of native tokens deposited for interaction. 62 address[] hTokens; //Input Local hTokens Addresses. 63 address[] tokens; //Input Native / underlying Token Addresses. 64 uint256[] amounts; //Amount of Local hTokens deposited for interaction. 65 uint256[] deposits; //Amount of native tokens deposited for interaction. 75 address[] globalAddresses; //Input Global hTokens Addresses. 76 uint256[] amounts; //Amount of Local hTokens deposited for interaction. 77 uint256[] deposits; //Amount of native tokens deposited for interaction. 93 address[] hTokens; // Input Local hTokens Addresses. 94 address[] tokens; // Input Native / underlying Token Addresses. 95 uint256[] amounts; // Amount of Local hTokens deposited for interaction. 96 uint256[] deposits; // Amount of native tokens deposited for interaction.
https://github.com/code-423n4/2023-09-maia/blob/main/src/interfaces/BridgeAgentStructs.sol#L16-L19
File: src/RootBridgeAgent.sol 1007 address[] memory addressArray = new address[](1); 1008 uint256[] memory uintArray = new uint256[](1); 1067 address[] memory hTokens = new address[](_globalAddresses.length); 1068 address[] memory tokens = new address[](_globalAddresses.length);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1007
File: src/RootBridgeAgentExecutor.sol 276 address[] memory hTokens = new address[](numOfAssets); 277 address[] memory tokens = new address[](numOfAssets); 278 uint256[] memory amounts = new uint256[](numOfAssets); 279 uint256[] memory deposits = new uint256[](numOfAssets);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L276C1-L279C64
When fetching data from a storage location, assigning the data to aĀ memoryĀ variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) forĀ eachĀ field of the struct/array.
If the fields are read from the new memory variable, they incur an additionalĀ MLOADĀ rather than a cheap stack read.
Instead of declearing the variable with theĀ memoryĀ keyword, declaring the variable with theĀ storageĀ keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read.
The only time it makes sense to read the whole struct/array into aĀ memoryĀ variable, is if the full struct/array is being returned by the function, is being passed to a function that requiresĀ memory, or if the array/struct is being read from anotherĀ memoryĀ array/struct
File: src/RootBridgeAgent.s 1007 address[] memory addressArray = new address[](1); 1008 uint256[] memory uintArray = new uint256[](1); 1067 address[] memory hTokens = new address[](_globalAddresses.length); 1068 address[] memory tokens = new address[](_globalAddresses.length);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1007
File: src/RootBridgeAgentExecutor.sol 276 address[] memory hTokens = new address[](numOfAssets); 277 address[] memory tokens = new address[](numOfAssets); 278 uint256[] memory amounts = new uint256[](numOfAssets); 279 uint256[] memory deposits = new uint256[](numOfAssets);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L276C1-L279C64
File: src/BranchBridgeAgent.sol 507 address[] memory _hTokens = new address[](numOfAssets); 508 address[] memory _tokens = new address[](numOfAssets); 509 uint256[] memory _amounts = new uint256[](numOfAssets); 510 uint256[] memory _deposits = new uint256[](numOfAssets); 827 address[] memory addressArray = new address[](1); 828 uint256[] memory uintArray = new uint256[](1);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L507-L510
File: src/ArbitrumBranchPort.sol 26 address public immutable rootPortAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchPort.sol#L26
File: src/MulticallRootRouter.sol 65 uint256 public immutable localChainId; 68 address public immutable localPortAddress; 71 address public immutable multicallAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L65-L71
File: src/CoreRootRouter.sol 51 address public immutable rootPortAddress; /// @notice Bridge Agent to manage remote execution and cross-chain assets. 54 address payable public bridgeAgentAddress; /// @notice Bridge Agent Executor Address. 57 address public bridgeAgentExecutorAddress; /// @notice ERC20 hToken Root Factory Address. 60 address public hTokenFactoryAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L51C1-L60C41
File: src/CoreBranchRouter.sol 31 hTokenFactoryAddress = _hTokenFactoryAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L31
File: src/RootBridgeAgent.sol 40 uint16 public immutable localChainId; /// @notice Bridge Agent Factory Address. 43 address public immutable factoryAddress; /// @notice Local Core Root Router Address 46 address public immutable localRouterAddress; /// @notice Local Port Address where funds deposited from this chain are stored. 49 address public immutable localPortAddress; /// @notice Local Layer Zero Endpoint Address for cross-chain communication. 52 address public immutable lzEndpointAddress; /// @notice Address of Root Bridge Agent Executor. 55 address public immutable bridgeAgentExecutorAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L40C1-L55C57
File: src/VirtualAccount.sol 21 address public immutable override userAddress; 24 address public immutable override localPortAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L21
File: src/BranchPort.sol 25 address public coreBranchRouterAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L25
File: src/BranchBridgeAgent.sol 60 address public immutable rootBridgeAgentAddress; 67 address public immutable lzEndpointAddress; 70 address public immutable localRouterAddress; 74 address public immutable localPortAddress; 77 address public immutable bridgeAgentExecutorAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L60
File: src/BaseBranchRouter.sol 33 address public localPortAddress; 36 address public override localBridgeAgentAddress; 39 address public override bridgeAgentExecutorAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L33
File: src/RootPort.sol 37 address public localBranchPortAddress; 40 address public coreRootRouterAddress; 43 address public coreRootBridgeAgentAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L37
File: src/token/ERC20hTokenRoot.sol 17 address public immutable override factoryAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/token/ERC20hTokenRoot.sol#L17
File: src/factories/BranchBridgeAgentFactory.sol 27 address public immutable rootBridgeAgentFactoryAddress; /// @notice Local Core Branch Router Address. 30 address public immutable localCoreBranchRouterAddress; /// @notice Root Port Address. 33 address public immutable localPortAddress; /// @notice Local Layer Zero Endpoint for cross-chain communication. 36 address public immutable lzEndpointAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/BranchBridgeAgentFactory.sol#L27
File: src/factories/RootBridgeAgentFactory.sol 16 address public immutable rootPortAddress; 19 address public immutable lzEndpointAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/RootBridgeAgentFactory.sol#L16
File: src/factories/ERC20hTokenBranchFactory.sol 17 address public immutable localPortAddress; 20 address public immutable localCoreRouterAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L17
File: src/factories/ERC20hTokenRootFactory.sol 17 address public immutable rootPortAddress; 20 address public immutable coreRootRouterAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol
requiresEndpoint
modifier is only used in this function lzReceiveNonBlocking
it should be save some gas that inlined.File: src/BranchBridgeAgent.sol 930 modifier requiresEndpoint(address _endpoint, bytes calldata _srcAddress) { 931 _requiresEndpoint(_endpoint, _srcAddress); 932 _; 933 } 947 modifier requiresRouter() { 948 if (msg.sender != localRouterAddress) revert UnrecognizedRouter(); 949 _; 950 }
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L930-L934
requiresBridgeAgentFactory
modifier is only used in this function addBridgeAgent
it should be save some gas that inlined.File: src/BranchPort.sol 553 modifier requiresBridgeAgentFactory() { 554 if (!isBridgeAgentFactory[msg.sender]) revert UnrecognizedBridgeAgentFactory(); 555 _; 556 } 559 modifier requiresPortStrategy(address _token) { 560 if (!isStrategyToken[_token]) revert UnrecognizedStrategyToken(); 561 if (!isPortStrategy[msg.sender][_token]) revert UnrecognizedPortStrategy(); 562 _; 563 }
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L553C1-L556C6
File: src/RootBridgeAgent.sol 1204 modifier requiresEndpoint(address _endpoint, uint16 _srcChain, bytes calldata _srcAddress) virtual { 1226 modifier requiresPort() { 1232 modifier requiresManager() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1204
File: src/RootPort.sol 557 modifier requiresBridgeAgentFactory() { 563 modifier requiresBridgeAgent() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L557
File: src/factories/ERC20hTokenBranchFactory.sol 112 modifier requiresCoreRouter() { 113 if (msg.sender != localCoreRouterAddress) revert UnrecognizedCoreRouter(); 114 _; 115 }
File: src/factories/ERC20hTokenRootFactory.sol 96 modifier requiresCoreRouterOrPort() { 97 if (msg.sender != coreRootRouterAddress) { 98 if (msg.sender != rootPortAddress) { 99 revert UnrecognizedCoreRouterOrPort(); 100 } 101 } 102 _; 103 }
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol#L96
The ERC721 balanceOf function is rarely used in practice but adds a storage overhead whenever a mint and transfer happens. ERC1155 tracks balance per id, and also uses the same balance to track ownership of the id. If the maximum supply for each id is one, then the token becomes non-fungible per each id.
File: src/VirtualAccount.sol 7 import {ERC721} from "solmate/tokens/ERC721.sol"; 11 import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L7
File: src/interfaces/IVirtualAccount.sol 4 import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/interfaces/IVirtualAccount.sol#L4
This was the original intent of the ERC1155 token. Each individual token behaves like and ERC20, but only one contract needs to be deployed.
The drawback of this approach is that the tokens will not be compatible with most DeFi swapping primitives.
ERC1155 uses callbacks on all of the transfer methods. If this is not desired, ERC6909 can be used instead.
File: src/ArbitrumCoreBranchRouter.sol 4 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumCoreBranchRouter.sol#L4
File: src/CoreRootRouter.sol 6 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L6
File: src/CoreBranchRouter.sol 4 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L4
File: src/BranchPort.sol 8 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L8
File: src/BaseBranchRouter.sol 7 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L7
File: src/token/ERC20hTokenRoot.sol 6 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/token/ERC20hTokenRoot.sol#L6
File: src/token/ERC20hTokenBranch.sol 6 import {ERC20} from "solmate/tokens/ERC20.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/token/ERC20hTokenBranch.sol#L6
File: src/factories/ERC20hTokenBranchFactory.sol 6 import {ERC20} from "../token/ERC20hTokenBranch.sol";
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L6
File: src/factories/ERC20hTokenRootFactory.sol 6 import {ERC20} from "solmate/tokens/ERC20.sol";
When reverting in solidity code, it is common practice to use a require or revert statement to revert execution with an error message. This can in most cases be further optimized by using assembly to revert with the error message.
Hereās an example;
/// calling restrictedAction(2) with a non-owner address: 24042 contract SolidityRevert { address owner; uint256 specialNumber = 1; constructor() { owner = msg.sender; } function restrictedAction(uint256 num) external { require(owner == msg.sender, "caller is not owner"); specialNumber = num; } } /// calling restrictedAction(2) with a non-owner address: 23734 function restrictedAction(uint256 num) external { assembly { if sub(caller(), sload(owner.slot)) { mstore(0x00, 0x20) // store offset to where length of revert message is stored mstore(0x20, 0x13) // store length (19) mstore( 0x40, 0x63616c6c6572206973206e6f74206f776e657200000000000000000000000000 ) // store hex representation of message revert(0x00, 0x60) // revert with data } } specialNumber = num; } }
From the example above we can see that we get a gas saving of over 300
gas when reverting wth the same error message with assembly against doing so in solidity. This gas savings come from the memory expansion costs and extra type checks the solidity compiler does under the hood.
File: src/ArbitrumBranchBridgeAgent.sol 126 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); 127 if (_endpoint != rootBridgeAgentAddress) revert LayerZeroUnauthorizedEndpoint();
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L126
File: src/ArbitrumBranchPort.sol 39 require(_rootPortAddress != address(0), "Root Port Address cannot be 0"); 63 if (_globalToken == address(0)) revert UnknownGlobalToken(); 83 if (!IRootPort(_rootPortAddress).isGlobalToken(_globalAddress, localChainId)) revert UnknownGlobalToken(); 90 if (_underlyingAddress == address(0)) revert UnknownUnderlyingToken();
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchPort.sol
File: src/ArbitrumCoreBranchRouter.sol 98 revert UnrecognizedBridgeAgentFactory(); 108 revert UnrecognizedBridgeAgent(); 169 revert UnrecognizedFunctionId();
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumCoreBranchRouter.sol
File: src/CoreRootRouter.sol 84 require(_setup, "Contract is already initialized"); 113 revert UnauthorizedCallerNotManager(); 117 if (!IPort(rootPortAddress).isChainId(_dstChainId)) revert InvalidChainId(); 120 if (IBridgeAgent(_rootBridgeAgent).getBranchBridgeAgent(_dstChainId) != address(0)) revert InvalidChainId(); 123 if (!IBridgeAgent(_rootBridgeAgent).isBranchBridgeAgentAllowed(_dstChainId)) revert UnauthorizedChainId(); 164 revert UnrecognizedBridgeAgentFactory(); 278 require(msg.sender == rootPortAddress, "Only root port can call"); 327 revert UnrecognizedFunctionId(); 245 revert UnrecognizedFunctionId(); 412 if (_dstChainId == rootChainId) revert InvalidChainId(); 415 revert UnrecognizedGlobalToken(); 420 revert TokenAlreadyAdded(); 461 if (IPort(rootPortAddress).isGlobalAddress(_underlyingAddress)) revert TokenAlreadyAdded(); 462 if (IPort(rootPortAddress).isLocalToken(_underlyingAddress, _srcChainId)) revert TokenAlreadyAdded(); 463 if (IPort(rootPortAddress).isUnderlyingToken(_underlyingAddress, _srcChainId)) revert TokenAlreadyAdded(); 483 if (IPort(rootPortAddress).isLocalToken(_localAddress, _dstChainId)) revert TokenAlreadyAdded(); 512 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor();
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L6
File: src/CoreBranchRouter.sol 212 revert UnrecognizedBridgeAgentFactory(); 222 revert UnrecognizedBridgeAgent(); 268 if (!IPort(_localPortAddress).isBridgeAgent(_branchBridgeAgent)) revert UnrecognizedBridgeAgent();
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L212
File: src/RootBridgeAgent.sol 244 if (settlementReference.owner == address(0)) revert SettlementRetryUnavailable(); 249 revert NotSettlementOwner(); 282 if (settlementOwner == address(0)) revert SettlementRetrieveUnavailable(); 287 revert NotSettlementOwner(); 307 if (settlement.status == STATUS_SUCCESS) revert SettlementRedeemUnavailable(); 308 if (settlementOwner == address(0)) revert SettlementRedeemUnavailable(); 313 revert NotSettlementOwner(); 357 if (_dParams.amount < _dParams.deposit) revert InvalidInputParams(); 365 revert InvalidInputParams(); 372 revert InvalidInputParams(); 396 if (length > MAX_TOKENS_LENGTH) revert InvalidInputParams(); 430 if (!success) if (msg.sender == getBranchBridgeAgent[localChainId]) revert ExecutionFailure(); 450 revert AlreadyExecutedTransaction(); 473 revert AlreadyExecutedTransaction(); 496 revert AlreadyExecutedTransaction(); 519 revert AlreadyExecutedTransaction(); 545 revert AlreadyExecutedTransaction(); 583 revert AlreadyExecutedTransaction(); 623 revert AlreadyExecutedTransaction(); 670 if (settlementReference.owner == address(0)) revert SettlementRetryUnavailable(); 675 revert NotSettlementOwner(); 705 revert AlreadyExecutedTransaction(); 757 if (!success) revert ExecutionFailure(); 818 if (callee == address(0)) revert UnrecognizedBridgeAgent(); 871 if (_hTokens.length == 0) revert SettlementRetryUnavailableUseCallout(); 910 if (callee == address(0)) revert UnrecognizedBridgeAgent(); 1057 if (_globalAddresses.length > MAX_TOKENS_LENGTH) revert InvalidInputParamsLength(); 1060 if (_globalAddresses.length != _amounts.length) revert InvalidInputParamsLength(); 1061 if (_amounts.length != _deposits.length) revert InvalidInputParamsLength(); 1141 if (_amount == 0 && _deposit == 0) revert InvalidInputParams(); 1142 if (_amount < _deposit) revert InvalidInputParams(); 1145 if (_localAddress == address(0)) revert UnrecognizedLocalAddress(); 1146 if (_underlyingAddress == address(0)) if (_deposit > 0) revert UnrecognizedUnderlyingAddress(); 1171 if (getBranchBridgeAgent[_branchChainId] != address(0)) revert AlreadyAddedBridgeAgent(); 1199 if (msg.sender != localRouterAddress) revert UnrecognizedRouter(); 1205 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); 1208 if (_endpoint != lzEndpointAddress) revert LayerZeroUnauthorizedEndpoint(); 1210 if (_srcAddress.length != 40) revert LayerZeroUnauthorizedCaller(); 1221 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedExecutor(); 1227 if (msg.sender != localPortAddress) revert UnrecognizedPort();
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L244C1-L245C1
File: src/VirtualAccount.sol 76 if (!success) revert CallFailed(); 103 if (!success) revert CallFailed(); 111 if (msg.value != valAccumulator) revert CallFailed(); 163 revert UnauthorizedCaller();
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L76C18-L76C25
File: src/BranchPort.sol 109 require(_owner != address(0), "Owner is zero address"); 123 require(coreBranchRouterAddress == address(0), "Contract already initialized"); 124 require(!isBridgeAgentFactory[_bridgeAgentFactory], "Contract already initialized"); 126 require(_coreBranchRouter != address(0), "CoreBranchRouter is zero address"); 127 require(_bridgeAgentFactory != address(0), "BridgeAgentFactory is zero address"); 181 require(ERC20(_token).balanceOf(address(this)) - currBalance == _amount, "Port Strategy Withdraw Failed"); 213 require( 299 if (length > 255) revert InvalidInputArrays(); 300 if (length != _underlyingAddresses.length) revert InvalidInputArrays(); 301 if (_underlyingAddresses.length != _amounts.length) revert InvalidInputArrays(); 302 if (_amounts.length != _deposits.length) revert InvalidInputArrays(); 320 if (isBridgeAgent[_bridgeAgent]) revert AlreadyAddedBridgeAgent(); 339 if (isBridgeAgentFactory[_newBridgeAgentFactory]) revert AlreadyAddedBridgeAgentFactory(); 364 revert InvalidMinimumReservesRatio(); 387 if (!isStrategyToken[_token]) revert UnrecognizedStrategyToken(); 332 require(coreBranchRouterAddress != address(0), "CoreRouter address is zero"); 333 require(_newCoreRouter != address(0), "New CoreRouter address is zero"); 542 if (msg.sender != coreBranchRouterAddress) revert UnrecognizedCore(); 548 if (!isBridgeAgent[msg.sender]) revert UnrecognizedBridgeAgent(); 554 if (!isBridgeAgentFactory[msg.sender]) revert UnrecognizedBridgeAgentFactory();
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L299C1-L302C78
File: src/BranchBridgeAgent.sol 125 require(_rootBridgeAgentAddress != address(0), "Root Bridge Agent Address cannot be the zero address."); 126 require( 130 require(_localRouterAddress != address(0), "Local Router Address cannot be the zero address."); 131 require(_localPortAddress != address(0), "Local Port Address cannot be the zero address."); 354 if (deposit.owner != msg.sender) revert NotDepositOwner(); 412 if (payload.length == 0) revert DepositRetryUnavailableUseCallout(); 424 if (getDeposit[_depositNonce].owner != msg.sender) revert NotDepositOwner(); 439 if (deposit.status == STATUS_SUCCESS) revert DepositRedeemUnavailable(); 440 if (deposit.owner == address(0)) revert DepositRedeemUnavailable(); 441 if (deposit.owner != msg.sender) revert NotDepositOwner(); 604 if (executionState[nonce] != STATUS_READY) revert AlreadyExecutedTransaction(); 624 if (executionState[nonce] != STATUS_READY) revert AlreadyExecutedTransaction(); 646 if (executionState[nonce] != STATUS_READY) revert AlreadyExecutedTransaction(); 672 revert AlreadyExecutedTransaction(); 720 if (!success) revert ExecutionFailure(); 869 if (_hTokens.length > MAX_TOKENS_LENGTH) revert InvalidInput(); 870 if (_hTokens.length != _tokens.length) revert InvalidInput(); 871 if (_tokens.length != _amounts.length) revert InvalidInput(); 872 if (_amounts.length != _deposits.length) revert InvalidInput(); 938 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); 939 if (_endpoint != lzEndpointAddress) revert LayerZeroUnauthorizedEndpoint(); 942 if (_srcAddress.length != 40) revert LayerZeroUnauthorizedCaller(); 943 if (rootBridgeAgentAddress != address(uint160(bytes20(_srcAddress[20:])))) revert LayerZeroUnauthorizedCaller(); 948 if (msg.sender != localRouterAddress) revert UnrecognizedRouter(); 954 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor();
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L354C1-L355C1
File: src/RootPort.sol 130 require(_bridgeAgentFactory != address(0), "Bridge Agent Factory cannot be 0 address."); 131 require(_coreRootRouter != address(0), "Core Root Router cannot be 0 address."); 132 require(_setup, "Setup ended."); 152 require(_coreRootBridgeAgent != address(0), "Core Root Bridge Agent cannot be 0 address."); 153 require(_coreLocalBranchBridgeAgent != address(0), "Core Local Branch Bridge Agent cannot be 0 address."); 154 require(_localBranchPortAddress != address(0), "Local Branch Port Address cannot be 0 address."); 155 require(isBridgeAgent[_coreRootBridgeAgent], "Core Bridge Agent doesn't exist."); 156 require(_setupCore, "Core Setup ended."); 245 if (_globalAddress == address(0)) revert InvalidGlobalAddress(); 246 if (_localAddress == address(0)) revert InvalidLocalAddress(); 247 if (_underlyingAddress == address(0)) revert InvalidUnderlyingAddres 264 if (_localAddress == address(0)) revert InvalidLocalAddress(); 282 if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); 290 if (_deposit > 0) if (!ERC20hTokenRoot(_hToken).mint(_recipient, _deposit, _srcChainId)) revert UnableToMint(); 309 if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); 320 if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); 330 if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); 341 if (!isGlobalAddress[_hToken]) revert UnrecognizedToken(); 342 if (!ERC20hTokenRoot(_hToken).mint(_to, _amount, localChainId)) revert UnableToMint(); 360 if (_user == address(0)) revert InvalidUserAddress(); 383 if (isBridgeAgent[_bridgeAgent]) revert AlreadyAddedBridgeAgent(); 422 if (isBridgeAgentFactory[_bridgeAgentFactory]) revert AlreadyAddedBridgeAgentFactory(); 448 if (isChainId[_chainId]) revert AlreadyAddedChain(); 485 if (isGlobalAddress[_ecoTokenGlobalAddress]) revert AlreadyAddedEcosystemToken(); 510 if (_coreRootRouter == address(0)) revert InvalidCoreRootRouter(); 511 if (_coreRootBridgeAgent == address(0)) revert InvalidCoreRootBridgeAgent(); 528 if (_coreBranchRouter == address(0)) revert InvalidCoreBranchRouter(); 529 if (_coreBranchBridgeAgent == address(0)) revert InvalidCoreBrancBridgeAgent(); 544 if (_coreBranchRouter == address(0)) revert InvalidCoreBranchRouter(); 545 if (_coreBranchBridgeAgent == address(0)) revert InvalidCoreBrancBridgeAgent();
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L130C1-L132C41
Similar to splitting require statements, you will usually save some gas by not having a boolean operator in the if statement.
contract CustomErrorBoolLessEfficient { error BadValue(); function requireGood(uint256 x) external pure { if (x < 10 || x > 20) { revert BadValue(); } } } contract CustomErrorBoolEfficient { error TooLow(); error TooHigh(); function requireGood(uint256 x) external pure { if (x < 10) { revert TooLow(); } if (x > 20) { revert TooHigh(); } } }
File: src/BranchPort.sol 363 if (_minimumReservesRatio >= DIVISIONER || _minimumReservesRatio < MIN_RESERVE_RATIO) { 364 revert InvalidMinimumReservesRatio(); 365 }
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L363C1-L365C10
Modifiers inject its implementation bytecode where it is used while internal functions jump to the location in the runtime code where the its implementation is. This brings certain trade-offs to both options.
Using modifiers more than once means repetitiveness and increase in size of the runtime code but reduces gas cost because of the absence of jumping to the internal function execution offset and jumping back to continue. This means that if runtime gas cost matter most to you, then modifiers should be your choice but if deployment gas cost and/or reducing the size of the creation code is most important to you then using internal functions will be best.
However, modifiers have the tradeoff that they can only be executed at the start or end of a functon. This means executing it at the middle of a function wouldnāt be directly possible, at least not without internal functions which kill the original purpose. This affects itās flexibility. Internal functions however can be called at any point in a function.
Example showing difference in gas cost using modifiers and an internal function
// SPDX-License-Identifier: MIT pragma solidity 0.8.19; /\*_ deployment gas cost: 195435 gas per call: restrictedAction1: 28367 restrictedAction2: 28377 restrictedAction3: 28411 _/ contract Modifier { address owner; uint256 val; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner); _; } function restrictedAction1() external onlyOwner { val = 1; } function restrictedAction2() external onlyOwner { val = 2; } function restrictedAction3() external onlyOwner { val = 3; } } /\*_ deployment gas cost: 159309 gas per call: restrictedAction1: 28391 restrictedAction2: 28401 restrictedAction3: 28435 _/ contract InternalFunction { address owner; uint256 val; constructor() { owner = msg.sender; } function onlyOwner() internal view { require(msg.sender == owner); } function restrictedAction1() external { onlyOwner(); val = 1; } function restrictedAction2() external { onlyOwner(); val = 2; } function restrictedAction3() external { onlyOwner(); val = 3; } }
Operation | Deployment | restrictedAction1 | restrictedAction2 | restrictedAction3 |
---|---|---|---|---|
Modifiers | 195,435 | 28,367 | 28,377 | 28,411 |
Internal Functions | 159,309 | 28,391 | 28,401 | 28,435 |
From the table above, we can see that the contract that uses modifiers cost more than 35k gas more than the contract using internal functions when deploying it due to repetition of the onlyOwner functionality in 3 functions.
During runtime, we can see that each function using modifiers cost a fixed 24 gas less than the functions using internal functions.
File: src/MulticallRootRouter.sol // @audit this lock modifier use 4 times 590 modifier lock() { 598 modifier requiresExecutor() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L590C1-L591C1
File: src/CoreRootRouter.sol 511 modifier requiresExecutor() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L511
File: src/RootBridgeAgent.sol 1190 modifier lock() { 1198 modifier requiresRouter() { 1204 modifier requiresEndpoint(address _endpoint, uint16 _srcChain, bytes calldata _srcAddress) virtual { 1220 modifier requiresAgentExecutor() { 1226 modifier requiresPort() { 1232 modifier requiresManager() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1220
File: src/VirtualAccount.sol 160 modifier requiresApprovedCaller() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L160
File: src/BranchPort.sol 541 modifier requiresCoreRouter() { 547 modifier requiresBridgeAgent() { 553 modifier requiresBridgeAgentFactory() { 559 modifier requiresPortStrategy(address _token) { 566 modifier lock() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L553
File: src/BranchBridgeAgent.sol 922 modifier lock() { 930 modifier requiresEndpoint(address _endpoint, bytes calldata _srcAddress) { 947 modifier requiresRouter() { 953 modifier requiresAgentExecutor() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L922
File: src/BaseBranchRouter.sol 206 modifier requiresAgentExecutor() { 212 modifier lock() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L206
File: src/RootPort.sol 557 modifier requiresBridgeAgentFactory() { 563 modifier requiresBridgeAgent() { 569 modifier requiresCoreRootRouter() { 575 modifier requiresLocalBranchPort() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L557
File: src/factories/ERC20hTokenBranchFactory.sol 112 modifier requiresCoreRouter() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L112
File: src/factories/ERC20hTokenRootFactory.sol 96 modifier requiresCoreRouterOrPort() {
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol#L96
File: src/ArbitrumBranchPort.sol 127 if (_deposit > 0) { 132 if (_amount - _deposit > 0) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchPort.sol#L127
File: src/RootBridgeAgent.sol 363 if (_dParams.amount > 0) { 370 if (_dParams.deposit > 0) { 1146 if (_underlyingAddress == address(0)) if (_deposit > 0) revert UnrecognizedUnderlyingAddress(); 1149 if (_amount - _deposit > 0) { 1156 if (_deposit > 0) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L363
File: src/BranchPort.sol 259 if (_amounts[i] - _deposits[i] > 0) { 266 if (_deposits[i] > 0) { 525 if (_hTokenAmount > 0) { 531 if (_deposit > 0) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L259
File: src/BranchBridgeAgent.sol 906 if (_amount - _deposit > 0) { 912 if (_deposit > 0) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L906
File: src/BaseBranchRouter.sol 165 if (_amount - _deposit > 0) { 173 if (_deposit > 0) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L165
File: src/RootPort.sol 284 if (_amount - _deposit > 0) { 290 if (_deposit > 0) if (!ERC20hTokenRoot(_hToken).mint(_recipient, _deposit, _srcChainId)) revert UnableToMint();
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L284
If you want to push optimization at the expense of creating slightly unconventional code, Solidity do-while loops are more gas efficient than for loops, even if you add an if-condition check for the case where the loop doesnāt execute at all.
// SPDX-License-Identifier: MIT pragma solidity 0.8.20; // times == 10 in both tests contract Loop1 { function loop(uint256 times) public pure { for (uint256 i; i < times; ) { unchecked { ++i; } } } } contract Loop2 { function loop(uint256 times) public pure { if (times == 0) { return; } uint256 i; do { unchecked { ++i; } } while (i < times); } }
File: src/MulticallRootRouter.sol 278 for (uint256 i = 0; i < outputParams.outputTokens.length;) { 367 for (uint256 i = 0; i < outputParams.outputTokens.length;) { 455 for (uint256 i = 0; i < outputParams.outputTokens.length;) { 557 for (uint256 i = 0; i < outputTokens.length;) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L278
File: src/RootBridgeAgentExecutor.sol 281 for (uint256 i = 0; i < uint256(uint8(numOfAssets));) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L281
File: src/RootBridgeAgent.sol 318 for (uint256 i = 0; i < settlement.hTokens.length;) { 399 for (uint256 i = 0; i < length;) { 1070 for (uint256 i = 0; i < hTokens.length;) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L318
File: src/VirtualAccount.sol 70 for (uint256 i = 0; i < length;) { 90 for (uint256 i = 0; i < length;) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L70
File: src/BranchPort.sol 257 for (uint256 i = 0; i < length;) { 305 for (uint256 i = 0; i < length;) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L257
File: src/BranchBridgeAgent.sol 447 for (uint256 i = 0; i < deposit.tokens.length;) { 513 for (uint256 i = 0; i < numOfAssets;) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L447
File: src/BaseBranchRouter.sol 192 for (uint256 i = 0; i < _hTokens.length;) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L192
An operation that causes the solidity compiler to expand memory is making external calls. When making external calls the compiler has to encode the function signature of the function it wishes to call on the external contract alongside itās arguments in memory. As we know, solidity does not clear or reuse memory memory so itāll have to store these data in the next free memory pointer which expands memory further.
With inline assembly, we can either use the scratch space and free memory pointer offset to store this data (as above) if the function arguments do not take up more than 96 bytes in memory. Better still, if we are making more than one external call we can reuse the same memory space as the first calls to store the new arguments in memory without expanding memory unnecessarily. Solidity in this scenario would expand memory by as much as the returned data length is. This is because the returned data is stored in memory (in most cases). If the return data is less than 96 bytes, we can use the scratch space to store it to prevent expanding memory.
See the example below;
contract Called { function add(uint256 a, uint256 b) external pure returns (uint256) { return a + b; } } contract Solidity { // cost: 7262 function call(address calledAddress) external pure returns (uint256) { Called called = Called(calledAddress); uint256 res1 = called.add(1, 2); uint256 res2 = called.add(3, 4); uint256 res = res1 + res2; return res; } } contract Assembly { // cost: 5281 function call(address calledAddress) external view returns (uint256) { assembly { // check that calledAddress has code deployed to it if iszero(extcodesize(calledAddress)) { revert(0x00, 0x00) } // first call mstore(0x00, hex"771602f7") mstore(0x04, 0x01) mstore(0x24, 0x02) let success := staticcall( gas(), calledAddress, 0x00, 0x44, 0x60, 0x20 ) if iszero(success) { revert(0x00, 0x00) } let res1 := mload(0x60) // second call mstore(0x04, 0x03) mstore(0x24, 0x4) success := staticcall(gas(), calledAddress, 0x00, 0x44, 0x60, 0x20) if iszero(success) { revert(0x00, 0x00) } let res2 := mload(0x60) // add results let res := add(res1, res2) // return data mstore(0x60, res) return(0x60, 0x20) } } }
We save approximately 2,000 gas by using the scratch space to store the function selector and itās arguments and also reusing the same memory space for the second call while storing the returned data in the zero slot thus not expanding memory.
If the arguments of the external function you wish to call is above 64 bytes and if you are making one external call, it wouldnāt save any significant gas writing it in assembly. However, if making more than one call. You can still save gas by reusing the same memory slot for the 2 calls using inline assembly.
Note: Always remember to update the free memory pointer if the offset it points to is already used, to avoid solidity overriding the data stored there or using the value stored there in an unexpected way.
Also note to avoid overwriting the zero slot (0x60 memory offset) if you have undefined dynamic memory values within that call stack. An alternative is to explicitly define dynamic memory values or if used, to set the slot back to 0x00 before exiting the assembly block.
File: src/factories/ERC20hTokenBranchFactory.sol 66 ERC20(_wrappedNativeTokenAddress).name(), 67 ERC20(_wrappedNativeTokenAddress).symbol(), 68 ERC20(_wrappedNativeTokenAddress).decimals(),
File: src/ArbitrumBranchPort.sol 83 if (!IRootPort(_rootPortAddress).isGlobalToken(_globalAddress, localChainId)) revert UnknownGlobalToken(); 87 IRootPort(_rootPortAddress).getUnderlyingTokenFromLocal(_globalAddress, localChainId); 92 IRootPort(_rootPortAddress).burnFromLocalBranch(_depositor, _globalAddress, _amount);
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchPort.sol#L83
File: src/ArbitrumCoreBranchRouter.sol 97 if (!IPort(_localPortAddress).isBridgeAgentFactory(_branchBridgeAgentFactory)) { 102 address newBridgeAgent = IBridgeAgentFactory(_branchBridgeAgentFactory).createBridgeAgent( 107 if (!IPort(_localPortAddress).isBridgeAgent(newBridgeAgent)) { 118 IBridgeAgent(localBridgeAgentAddress).callOutSystem(payable(_refundee), payload, _gParams);
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumCoreBranchRouter.sol#L97
File: src/BaseBranchRouter.sol 168 ERC20(_hToken).approve(_localPortAddress, _amount - _deposit); 175 ERC20(_token).approve(_localPortAddress, _deposit);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L168
File: src/BranchBridgeAgent.sol 908 IPort(localPortAddress).bridgeIn(_recipient, _hToken, _amount - _deposit); 913 IPort(localPortAddress).withdraw(_recipient, _token, _deposit);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L908
File: src/CoreBranchRouter.sol 211 if (!IPort(_localPortAddress).isBridgeAgentFactory(_branchBridgeAgentFactory)) { 221 if (!IPort(_localPortAddress).isBridgeAgent(newBridgeAgent)) { 216 address newBridgeAgent = IBridgeAgentFactory(_branchBridgeAgentFactory).createBridgeAgent( 232 IBridgeAgent(localBridgeAgentAddress).callOutSystem{value: msg.value}(payable(_refundee), payload, _gParams); 249 if (IPort(_localPortAddress).isBridgeAgentFactory(_newBridgeAgentFactoryAddress)) { 251 IPort(_localPortAddress).toggleBridgeAgentFactory(_newBridgeAgentFactoryAddress); 254 IPort(_localPortAddress).addBridgeAgentFactory(_newBridgeAgentFactoryAddress); 268 if (!IPort(_localPortAddress).isBridgeAgent(_branchBridgeAgent)) revert UnrecognizedBridgeAgent(); 271 IPort(_localPortAddress).toggleBridgeAgent(_branchBridgeAgent); 289 if (IPort(_localPortAddress).isStrategyToken(_underlyingToken)) { 291 IPort(_localPortAddress).toggleStrategyToken(_underlyingToken); 394 IPort(_localPortAddress).addStrategyToken(_underlyingToken, _minimumReservesRatio); 317 if (!IPort(_localPortAddress).isPortStrategy(_portStrategy, _underlyingToken)) { 319 IPort(_localPortAddress).addPortStrategy(_portStrategy, _underlyingToken, _dailyManagementLimit); 322 IPort(_localPortAddress).updatePortStrategy(_portStrategy, _underlyingToken, _dailyManagementLimit); 325 IPort(_localPortAddress).togglePortStrategy(_portStrategy, _underlyingToken);
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L317
File: src/CoreRootRouter.sol 112 if (msg.sender != IPort(rootPortAddress).getBridgeAgentManager(_rootBridgeAgent)) { 117 if (!IPort(rootPortAddress).isChainId(_dstChainId)) revert InvalidChainId(); 120 if (IBridgeAgent(_rootBridgeAgent).getBranchBridgeAgent(_dstChainId) != address(0)) revert InvalidChainId(); 123 if (!IBridgeAgent(_rootBridgeAgent).isBranchBridgeAgentAllowed(_dstChainId)) revert UnauthorizedChainId(); 461 if (IPort(rootPortAddress).isGlobalAddress(_underlyingAddress)) revert TokenAlreadyAdded(); 462 if (IPort(rootPortAddress).isLocalToken(_underlyingAddress, _srcChainId)) revert TokenAlreadyAdded(); 463 if (IPort(rootPortAddress).isUnderlyingToken(_underlyingAddress, _srcChainId)) revert TokenAlreadyAdded(); 469 IPort(rootPortAddress).setAddresses(
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L461
File: src/MulticallRootRouter.sol 239 IVirtualAccount(userAccount).call(calls); 248 IVirtualAccount(userAccount).call(calls); 251 IVirtualAccount(userAccount).withdrawERC20(outputParams.outputToken, outputParams.amountOut); 255 IVirtualAccount(userAccount).userAddress(), 275 IVirtualAccount(userAccount).call(calls);
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L239
Missing checks for zero-addresses may lead to infunctional protocol, if the variable addresses are updated incorrectly. It also wast gas as it requires the redeployment of the contract.
File: src/CoreRootRouter.sol 71 constructor(uint256 _rootChainId, address _rootPortAddress) { 72 rootChainId = _rootChainId; 73 rootPortAddress = _rootPortAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L73
File: src/CoreBranchRouter.sol 30 constructor(address _hTokenFactoryAddress) BaseBranchRouter() { 31 hTokenFactoryAddress = _hTokenFactoryAddress; 32 }
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L31
File: src/VirtualAccount.sol 35 constructor(address _userAddress, address _localPortAddress) { 36 userAddress = _userAddress; 37 localPortAddress = _localPortAddress; 38 }
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L36
In Solidity, when you evaluate a boolean expression (e.g the || (logical or) or && (logical and) operators), in the case of || the second expression will only be evaluated if the first expression evaluates to false and in the case of && the second expression will only be evaluated if the first expression evaluates to true. This is called short-circuiting.
For example, the expression require(msg.sender == owner || msg.sender == manager) will pass if the first expression msg.sender == owner evaluates to true. The second expression msg.sender == manager will not be evaluated at all.
However, if the first expression msg.sender == owner evaluates to false, the second expression msg.sender == manager will be evaluated to determine whether the overall expression is true or false. Here, by checking the condition that is most likely to pass firstly, we can avoid checking the second condition thereby saving gas in majority of successful calls.
This is similar for the expression require(msg.sender == owner && msg.sender == manager). If the first expression msg.sender == owner evaluates to false, the second expression msg.sender == manager will not be evaluated because the overall expression cannot be true. For the overall statement to be true, both side of the expression must evaluate to true. Here, by checking the condition that is most likely to fail firstly, we can avoid checking the second condition thereby saving gas in majority of call reverts.
Short-circuiting is useful and itās recommended to place the less expensive expression first, as the more costly one might be bypassed. If the second expression is more important than the first, it might be worth reversing their order so that the cheaper one gets evaluated first.
File: src/BranchPort.sol 363 if (_minimumReservesRatio >= DIVISIONER || _minimumReservesRatio < MIN_RESERVE_RATIO) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L363
File: src/BranchBridgeAgent.sol 127 _lzEndpointAddress != address(0) || _rootChainId == _localChainId,
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L127
File: src/factories/BranchBridgeAgentFactory.sol 63 _lzEndpointAddress != address(0) || _rootChainId == _localChainId,
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/BranchBridgeAgentFactory.sol#L63
File: src/RootBridgeAgent.sol 1141 if (_amount == 0 && _deposit == 0) revert InvalidInputParams();
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1141
File: src/RootBridgeAgent.sol 978 settlementNonce = _settlementNonce + 1; 1064 settlementNonce = _settlementNonce + 1;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L978
File: src/BranchBridgeAgent.sol 821 depositNonce = _depositNonce + 1; 875 depositNonce = _depositNonce + 1;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L821
It can be more gas-efficient to use a hardcoded address instead of the address(this) expression, especially if you need to use the same address multiple times in your contract.
The reason for this, is that using address(this) requires an additional EXTCODESIZE operation to retrieve the contractās address from its bytecode, which can increase the gas cost of your contract. By pre-calculating and using a hardcoded address, you can avoid this additional operation and reduce the overall gas cost of your contract.
Hereās an example of how you can use a hardcoded address instead of address(this):
contract MyContract { address public myAddress = 0x1234567890123456789012345678901234567890; function doSomething() public { // Use myAddress instead of address(this) require(msg.sender == myAddress, "Caller is not authorized"); // Do something } }
In the above example, we have a contract (MyContract) with a public address variable myAddress. Instead of using address(this) to retrieve the contractās address, we have pre-calculated and hardcoded the address in the variable. This can help to reduce the gas cost of our contract and make our code more efficient.
File: src/BranchPort.sol 175 uint256 currBalance = ERC20(_token).balanceOf(address(this)); 178 IPortStrategy(msg.sender).withdraw(address(this), _token, _amount); 181 require(ERC20(_token).balanceOf(address(this)) - currBalance == _amount, "Port Strategy Withdraw Failed"); 193 uint256 currBalance = ERC20(_token).balanceOf(address(this)); 210 IPortStrategy(_strategy).withdraw(address(this), _token, amountToWithdraw); 214 ERC20(_token).balanceOf(address(this)) - currBalance == amountToWithdraw, "Port Strategy Withdraw Failed" 436 uint256 currBalance = ERC20(_token).balanceOf(address(this)); 526 _localAddress.safeTransferFrom(_depositor, address(this), _hTokenAmount); 532 _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L175
File: src/ArbitrumBranchBridgeAgent.sol 126 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint();
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L126
File: src/ArbitrumBranchPort.sol 66 _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit); 128 _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit);
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchPort.sol#L66
File: src/RootBridgeAgent.sol 120 bridgeAgentExecutorAddress = DeployRootBridgeAgentExecutor.deploy(address(this)); 148 address(this), 424 (bool success,) = address(this).excessivelySafeCall( 695 address(this).balance 754 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 779 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 940 ILayerZeroEndpoint(lzEndpointAddress).send{value: address(this).balance}( 1182 getBranchBridgeAgentPath[_branchChainId] = abi.encodePacked(_newBranchBridgeAgent, address(this)); 1205 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); 1233 if (msg.sender != IPort(localPortAddress).getBridgeAgentManager(address(this))) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L120
File: src/BranchBridgeAgent.sol 142 rootBridgeAgentPath = abi.encodePacked(_rootBridgeAgentAddress, address(this)); 168 address(this), 579 address(this).excessivelySafeCall( 717 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 737 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 787 ILayerZeroEndpoint(lzEndpointAddress).send{value: address(this).balance}( 938 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint();
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L142
File: src/BaseBranchRouter.sol 167 _hToken.safeTransferFrom(msg.sender, address(this), _amount - _deposit); 174 _token.safeTransferFrom(msg.sender, address(this), _deposit);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L167
File: src/RootPort.sol 301 _hToken.safeTransferFrom(_from, address(this), _amount); 362 newAccount = new VirtualAccount{salt: keccak256(abi.encode(_user))}(_user, address(this));
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L301
File:
It's recommended to use the bytes.concat() function instead of abi.encodePacked() for concatenating byte arrays. bytes.concat() is a more gas-efficient and safer way to concatenate byte arrays, and it's considered a best practice in newer Solidity versions.
File: src/ArbitrumBranchBridgeAgent.sol 115 rootChainId, "", 0, abi.encodePacked(bytes1(0x09), _settlementNonce)
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L115
File: src/ArbitrumCoreBranchRouter.sol 62 bytes memory payload = abi.encodePacked(bytes1(0x02), params); 115 bytes memory payload = abi.encodePacked(bytes1(0x04), data);
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumCoreBranchRouter.sol#L62
File: src/CoreRootRouter.sol 136 bytes memory payload = abi.encodePacked(bytes1(0x02), params); 171 bytes memory payload = abi.encodePacked(bytes1(0x03), params); 196 bytes memory payload = abi.encodePacked(bytes1(0x04), params); 223 bytes memory payload = abi.encodePacked(bytes1(0x05), params); 254 bytes memory payload = abi.encodePacked(bytes1(0x06), params); 284 bytes memory payload = abi.encodePacked(bytes1(0x07), params); 434 bytes memory payload = abi.encodePacked(bytes1(0x01), params);
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L136
File: src/CoreBranchRouter.sol 51 bytes memory payload = abi.encodePacked(bytes1(0x01), params); 75 bytes memory payload = abi.encodePacked(bytes1(0x02), params); 181 bytes memory payload = abi.encodePacked(bytes1(0x03), params); 229 bytes memory payload = abi.encodePacked(bytes1(0x04), params);
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L51
File: src/RootBridgeAgent.sol 151 abi.encodePacked(uint16(2), _gasLimit, _remoteBranchExecutionGas, getBranchBridgeAgent[_dstChainId]) 168 bytes memory payload = abi.encodePacked(bytes1(0x00), _recipient, settlementNonce++, _params); 292 bytes memory payload = abi.encodePacked(bytes1(0x03), settlementOwner, _settlementNonce); 829 abi.encodePacked(uint16(2), _gParams.gasLimit, _gParams.remoteBranchExecutionGas, callee) 879 payload = abi.encodePacked( 893 payload = abi.encodePacked( 921 abi.encodePacked(uint16(2), _gParams.gasLimit, _gParams.remoteBranchExecutionGas, callee) 943 abi.encodePacked(bytes1(0x04), _depositNonce), 992 _payload = abi.encodePacked( 1089 _payload = abi.encodePacked( 1182 getBranchBridgeAgentPath[_branchChainId] = abi.encodePacked(_newBranchBridgeAgent, address(this));
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L151
File: src/BranchBridgeAgent.sol 142 rootBridgeAgentPath = abi.encodePacked(_rootBridgeAgentAddress, address(this)); 171 abi.encodePacked(uint16(2), _gasLimit, _remoteBranchExecutionGas, rootBridgeAgentAddress) 188 bytes memory payload = abi.encodePacked(bytes1(0x00), depositNonce++, _params); 202 bytes memory payload = abi.encodePacked(bytes1(0x01), depositNonce++, _params); 219 bytes memory payload = abi.encodePacked( 241 bytes memory payload = abi.encodePacked( 269 bytes memory payload = abi.encodePacked(bytes1(0x04), msg.sender, depositNonce++, _params); 287 bytes memory payload = abi.encodePacked( 317 bytes memory payload = abi.encodePacked( 362 payload = abi.encodePacked( 373 payload = abi.encodePacked( 386 payload = abi.encodePacked( 398 payload = abi.encodePacked( 427 bytes memory payload = abi.encodePacked(bytes1(0x08), msg.sender, _depositNonce); 474 bytes memory payload = abi.encodePacked(_hasFallbackToggled ? bytes1(0x87) : bytes1(0x07), params); 776 abi.encodePacked(uint16(2), _gParams.gasLimit, _gParams.remoteBranchExecutionGas, rootBridgeAgentAddress) 790 abi.encodePacked(bytes1(0x09), _settlementNonce),
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L142
Inline-assembly can be used to shorten the array by changing the length slot, so that the entries don't have to be copied to a new, shorter array
Shorten the array rather than copying to a new one:
This suggests that instead of creating a new array with fewer elements (by copying data from the original array to the new one), it's more efficient to modify the original array to have a shorter length.
Inline-assembly can be used to shorten the array by changing the length slot:
Use inline assembly to directly manipulate the length of an array.
So that the entries don't have to be copied to a new, shorter array:
When you shorten an array by changing its length directly using inline assembly, you avoid the overhead of copying the array's elements to a new one. This is more gas-efficient, especially when dealing with large arrays, as it reduces the computational and gas costs associated with copying data.
File: src/RootBridgeAgentExecutor.sol 276 address[] memory hTokens = new address[](numOfAssets); 277 address[] memory tokens = new address[](numOfAssets); 278 uint256[] memory amounts = new uint256[](numOfAssets); 279 uint256[] memory deposits = new uint256[](numOfAssets);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L276C1-L279C64
File: src/RootBridgeAgent.sol 1007 address[] memory addressArray = new address[](1); 1008 uint256[] memory uintArray = new uint256[](1); 1067 address[] memory hTokens = new address[](_globalAddresses.length); 1068 address[] memory tokens = new address[](_globalAddresses.length);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1007C1-L1009C1
File: src/BranchBridgeAgent.sol 507 address[] memory _hTokens = new address[](numOfAssets); 508 address[] memory _tokens = new address[](numOfAssets); 509 uint256[] memory _amounts = new uint256[](numOfAssets); 510 uint256[] memory _deposits = new uint256[](numOfAssets); 827 address[] memory addressArray = new address[](1); 828 uint256[] memory uintArray = new uint256[](1);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L507C1-L510C65
File: src/MulticallRootRouter.sol 109 function initialize(address _bridgeAgentAddress) external onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L109
File: src/CoreRootRouter.sol 83 function initialize(address _bridgeAgentAddress, address _hTokenFactory) external onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L83
File: src/BranchPort.sol 122 function initialize(address _coreBranchRouter, address _bridgeAgentFactory) external virtual onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L122
File: src/BaseBranchRouter.sol 60 function initialize(address _localBridgeAgentAddress) external onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L60
File: src/RootPort.sol 129 function initialize(address _bridgeAgentFactory, address _coreRootRouter) external onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L129
File: src/factories/BranchBridgeAgentFactory.sol 87 function initialize(address _coreRootBridgeAgent) external virtual onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/BranchBridgeAgentFactory.sol#L87
File: src/factories/ERC20hTokenBranchFactory.sol 60 function initialize(address _wrappedNativeTokenAddress, address _coreRouter) external onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L60
File: src/factories/ERC20hTokenRootFactory.sol 49 function initialize(address _coreRouter) external onlyOwner {
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol#L49
File: src/factories/ArbitrumBranchBridgeAgentFactory.sol 56 function initialize(address _coreRootBridgeAgent) external override onlyOwner {
Using assembly to validate msg.sender means that you can manually inspect and manipulate the value of msg.sender using inline assembly code to enforce custom validation rules or conditions.
For-Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SenderValidationExample { address public owner; constructor() { owner = msg.sender; } function validateSender() public view returns (bool) { bool isOwner; assembly { // Load the current sender (msg.sender) into a temporary variable isOwner := eq(sender, sload(owner.slot)) } return isOwner; } }
File: src/ArbitrumBranchBridgeAgent.sol 126 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint();
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L126
File: src/MulticallRootRouter.sol 605 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor();
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L605
File: src/CoreRootRouter.sol 112 if (msg.sender != IPort(rootPortAddress).getBridgeAgentManager(_rootBridgeAgent)) { 278 require(msg.sender == rootPortAddress, "Only root port can call"); 512 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor();
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L112
File: src/RootBridgeAgent.sol 247 if (msg.sender != settlementReference.owner) { 285 if (msg.sender != settlementOwner) { 286 if (msg.sender != address(IPort(localPortAddress).getUserAccount(settlementOwner))) { 311 if (msg.sender != settlementOwner) { 312 if (msg.sender != address(IPort(localPortAddress).getUserAccount(settlementOwner))) { 430 if (!success) if (msg.sender == getBranchBridgeAgent[localChainId]) revert ExecutionFailure(); 1199 if (msg.sender != localRouterAddress) revert UnrecognizedRouter(); 1205 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); 1221 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedExecutor(); 1227 if (msg.sender != localPortAddress) revert UnrecognizedPort(); 1233 if (msg.sender != IPort(localPortAddress).getBridgeAgentManager(address(this))) {
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L247
File: src/BranchBridgeAgent.sol 354 if (deposit.owner != msg.sender) revert NotDepositOwner(); 424 if (getDeposit[_depositNonce].owner != msg.sender) revert NotDepositOwner(); 441 if (deposit.owner != msg.sender) revert NotDepositOwner(); 938 if (msg.sender != address(this)) revert LayerZeroUnauthorizedEndpoint(); 948 if (msg.sender != localRouterAddress) revert UnrecognizedRouter(); 954 if (msg.sender != bridgeAgentExecutorAddress) revert UnrecognizedBridgeAgentExecutor();
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L354
Sometimes, contracts are used to deploy several contracts in one transaction, which necessitates doing it in the constructor.
If the contractās only use is the code in the constructor, then selfdestructing at the end of the operation will save gas.
Although selfdestruct is set for removal in an upcoming hardfork, it will still be supported in the constructor per EIP 6780
Contract calls are expensive, and the best way to save gas on them is by not using them at all. There is a natural tradeoff with this, but having several contracts that talk to each other can sometimes increase gas and complexity rather than manage it.
File: src/ArbitrumBranchBridgeAgent.sol 103 _rootBridgeAgentAddress.call{value: msg.value}("");
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L103
File: src/RootBridgeAgent.sol 754 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 779 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 835 callee.call{value: msg.value}(""); 927 callee.call{value: _value}("");
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L754
File: src/VirtualAccount.sol 101 if (isContract(_call.target)) (success, returnData[i]) = _call.target.call{value: val}(_call.callData);
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L101
File: src/BranchBridgeAgent.sol 717 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata); 737 (bool success,) = bridgeAgentExecutorAddress.call{value: address(this).balance}(_calldata);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L717
Although the calldataload instruction is a cheap opcode, the solidity compiler will sometimes output cheaper code if you cache calldataload. This will not always be the case, so you should test both possibilities.
contract LoopSum { function sumArr(uint256[] calldata arr) public pure returns (uint256 sum) { uint256 len = arr.length; for (uint256 i = 0; i < len; ) { sum += arr[i]; unchecked { ++i; } } } }
File: src/ArbitrumBranchBridgeAgent.sol 89 function retrySettlement(uint32, bytes calldata, GasParams[2] calldata, bool) external payable override lock {}
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L89
File: src/CoreRootRouter.sol 109 GasParams[2] calldata _gParams
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L109
File: src/CoreBranchRouter.sol 43 function addGlobalToken(address _globalAddress, uint256 _dstChainId, GasParams[3] calldata _gParams)
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreBranchRouter.sol#L43
File: src/VirtualAccount.sol 66 function call(Call[] calldata calls) external override requiresApprovedCaller returns (bytes[] memory returnData) { 85 function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { 134 function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
https://github.com/code-423n4/2023-09-maia/blob/main/src/VirtualAccount.sol#L66
File: src/BranchBridgeAgent.sol 467 GasParams[2] calldata _gParams,
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L467
The reason behind this is in way ++i and i++ are evaluated by the compiler.
i++ returns i(its old value) before incrementing i to a new value. This means that 2 values are stored on the stack for usage whether you wish to use it or not. ++i on the other hand, evaluates the ++ operation on i (i.e it increments i) then returns i (its incremented value) which means that only one item needs to be stored on the stack.
File: src/RootBridgeAgent.sol // @audit settlementNonce++ 168 bytes memory payload = abi.encodePacked(bytes1(0x00), _recipient, settlementNonce++, _params);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L168
File: src/BranchBridgeAgent.sol 188 bytes memory payload = abi.encodePacked(bytes1(0x00), depositNonce++, _params); 202 bytes memory payload = abi.encodePacked(bytes1(0x01), depositNonce++, _params); 269 bytes memory payload = abi.encodePacked(bytes1(0x04), msg.sender, depositNonce++, _params);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L188
The solidity compiler outputs more efficient code when the variable is declared in the return statement. There seem to be very few exceptions to this in practice, so if you see an anonymous return, you should test it with a named return instead to determine which case is most efficient.
// SPDX-License-Identifier: MIT pragma solidity 0.8.20; contract NamedReturn { function myFunc1(uint256 x, uint256 y) external pure returns (uint256) { require(x > 0); require(y > 0); return x * y; } } contract NamedReturn2 { function myFunc2(uint256 x, uint256 y) external pure returns (uint256 z) { require(x > 0); require(y > 0); z = x * y; } }
File: src/ArbitrumBranchBridgeAgent.sol 15 return new ArbitrumBranchBridgeAgent(
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L15
File: src/MulticallRootRouter.sol 582 return data;
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouter.sol#L582
File: src/BranchPort.sol 440 return currBalance > minReserves ? currBalance - minReserves : 0; 473 return ((_currBalance + _strategyTokenDebt) * getMinimumTokenReserveRatio[_token]) / DIVISIONER;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L440
File: src/MulticallRootRouterLibZip.sol 38 return data.cdDecompress();
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouterLibZip.sol#L38
File: src/BranchBridgeAgent.sol 32 return new BranchBridgeAgent( 157 return getDeposit[_depositNonce]; 570 return SettlementMultipleParams(numOfAssets, _recipient, nonce, _hTokens, _tokens, _amounts, _deposits);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L32
File: src/BaseBranchRouter.sol 75 return IBridgeAgent(localBridgeAgentAddress).getDepositEntry(_depositNonce);
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L75
File: src/RootPort.sol 181 return _getLocalToken(_localAddress, _srcChainId, _dstChainId); 196 return getLocalTokenFromGlobal[globalAddress][_dstChainId]; 207 return getUnderlyingTokenFromLocal[localAddress][_srcChainId]; 212 return getLocalTokenFromGlobal[_globalAddress][_srcChainId] != address(0); 217 return getGlobalTokenFromLocal[_localAddress][_srcChainId] != address(0); 226 return _getLocalToken(_localAddress, _srcChainId, _dstChainId) != address(0); 231 return getLocalTokenFromUnderlying[_underlyingToken][_srcChainId] != address(0);
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L181
A common pattern, especially in airdrops, is to mark an address as āalready usedā when claiming the airdrop or NFT mint.
However, since it only takes one bit to store this information, and each slot is 256 bits, that means one can store a 256 flags/booleans with one storage slot.
File: src/RootBridgeAgent.sol 68 mapping(uint256 chainId => bool allowed) public isBranchBridgeAgentAllowed;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L68
File: src/BranchPort.sol 32 mapping(address bridgeAgent => bool isActiveBridgeAgent) public isBridgeAgent; 42 mapping(address bridgeAgentFactory => bool isActiveBridgeAgentFactory) public isBridgeAgentFactory; 52 mapping(address token => bool allowsStrategies) public isStrategyToken; 68 mapping(address strategy => mapping(address token => bool isActiveStrategy)) public isPortStrategy;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L32
File: src/RootPort.sol 54 mapping(VirtualAccount acount => mapping(address router => bool allowed)) public isRouterApproved; 61 mapping(uint256 chainId => bool isActive) public isChainId; 64 mapping(address bridgeAgent => bool isActive) public isBridgeAgent; 77 mapping(address bridgeAgentFactory => bool isActive) public isBridgeAgentFactory; 87 mapping(address token => bool isGlobalToken) public isGlobalAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L54
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should beĀ abstractĀ and the function signatures be added without any default implementation.
If the block is an emptyĀ if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if( x ) {}else if(y){ . . . }else{ . . . } => if ( !x) {if(y) { . . . }else{ . . .}}) . EmptyĀ receive()/fallback() payableĀ functions that are not used, can be removed to save deployment gas.
File: src/ArbitrumBranchBridgeAgent.sol 57 {} 89 function retrySettlement(uint32, bytes calldata, GasParams[2] calldata, bool) external payable override lock {}
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumBranchBridgeAgent.sol#L57
File: src/ArbitrumCoreBranchRouter.sol 44 constructor() CoreBranchRouter(address(0)) {}
https://github.com/code-423n4/2023-09-maia/blob/main/src/ArbitrumCoreBranchRouter.sol#L44
File: src/MulticallRootRouterLibZip.sol 31 {}
https://github.com/code-423n4/2023-09-maia/blob/main/src/MulticallRootRouterLibZip.sol#L31
File: src/factories/ArbitrumBranchBridgeAgentFactory.sol 46 {}
#0 - c4-pre-sort
2023-10-15T16:49:34Z
0xA5DF marked the issue as sufficient quality report
#1 - 0xA5DF
2023-10-15T16:53:08Z
Many are from bot report (G3, G10, G24) G21 doesn't seem to be relevant
#2 - c4-judge
2023-10-26T13:47:35Z
alcueca marked the issue as grade-b
š Selected for report: MrPotatoMagic
Also found by: 0xHelium, 0xSmartContract, 0xbrett8571, 0xsagetony, 33BYTEZZZ, Bauchibred, K42, Littlebeast, LokiThe5th, Oxsadeeq, SAAJ, Sathish9098, ZdravkoHr, albertwh1te, alexxander, catellatech, chaduke, hunter_w3b, ihtishamsudo, invitedtea, jauvany, klau5, kodyvim, lsaudit, pavankv, pfapostol, yongskiws
243.335 USDC - $243.33
The Ulysses Protocol revolves around creating a decentralized and community-owned platform known as an "Omnichain Liquidity Protocol." This platform addresses the challenges posed by fragmented liquidity in the DeFi landscape.
The key components of Ulysses Protocol include:
Liquidity Providers: Users can deploy their assets on a single blockchain and earn rewards from activities spanning multiple blockchains, promoting efficient capital utilization.
Unified Liquidity Management: Ulysses Protocol simplifies liquidity management across different blockchains, reducing complexity for liquidity providers.
Single Unified Token: Users can create a single token representing their liquidity across various blockchains, enabling versatility in DeFi applications.
The key contracts of the protocol for this Audit are:
ArbitrumBranchBridgeAgent.sol: This contract serves as an interface with Users/Routers and acts as a middleman.
ArbitrumBranchPort.sol: It's an implementation of the Ulysses Port specifically designed for deployment on the Arbitrum Branch Chain.
ArbitrumCoreBranchRouter.sol: This contract represents the core Branch Router implementation tailored for the Arbitrum deployment.
MulticallRootRouter.sol: This is the Root Router implementation used to interface with third-party decentralized applications (dApps) within the Root Omnichain Environment.
CoreRootRouter.sol: It's the core implementation of the Root Router, deployed in the Root Environment. It plays a crucial role in the protocol's operation.
RootBridgeAgentExecutor.sol: This contract handles token settlement clearance requests and executes transaction requests originating from the branch chains.
CoreBranchRouter.sol: Similar to ArbitrumCoreBranchRouter.sol, this contract represents the core Branch Router but is intended for deployment in Branch Chains.
RootBridgeAgent.sol: Responsible for interfacing with Users and Routers, this contract acts as a middleman within the Root Environment.
VirtualAccount.sol: This contract allows users to manage assets and perform remote interactions. It is a fundamental component of the protocol.
BranchPort.sol: Used to manage the deposit and withdrawal of underlying assets in response to requests from Branch Bridge Agents. It facilitates interaction with the Branch Chain.
MulticallRootRouterLibZip.sol: Another implementation of the Root Router for interfacing with third-party dApps in the Root Omnichain Environment.
BranchBridgeAgent.sol: This contract is deployed in Branch Chains and is responsible for interfacing with Users and Routers.
BaseBranchRouter.sol: A base Branch Contract that serves as an interface with Branch Bridge Agents, common functionality across Branch Chains.
RootPort.sol: Manages the deposit and withdrawal of assets between the Root Omnichain Environment and every Branch Chain in response to Root Bridge Agents' requests.
BranchBridgeAgentExecutor.sol: Used for requesting token deposit clearance and executing transactions in response to requests from the root environment.
ERC20hTokenRoot.sol: Represents a 1:1 ERC20 representation of a token deposited in a Branch Chain's Port in the Root Chain.
ERC20hTokenBranch.sol: Represents a 1:1 ERC20 representation of a token deposited in a Branch Chain's Port in the Branch Chains.
BranchBridgeAgentFactory.sol: This factory contract allows permissionless deployment of new Branch Bridge Agents, enhancing flexibility.
RootBridgeAgentFactory.sol: A factory contract used for deploying new Root Bridge Agents in the protocol.
ERC20hTokenBranchFactory.sol: A factory contract enabling permissionless deployment of new Branch hTokens in Branch Chains.
ERC20hTokenRootFactory.sol: Similar to the Branch counterpart, this factory contract allows permissionless deployment of new Root hTokens in the Root Chain.
ArbitrumBranchBridgeAgentFactory.sol: Another factory contract, specifically for enabling permissionless deployment of new Arbitrum Branch Bridge Agents.
Accordingly, I analyzed and audited the subject in the following steps;
Core Protocol Contract Overview:
I focused on thoroughly understanding the codebase and providing recommendations to improve its functionality. The main goal was to take a close look at the important contracts and how they work together in the Maia DAO Ulysseso Protocol.
Main Contracts I Looked At
ArbitrumBranchPort.sol ArbitrumCoreBranchRouter.sol RootBridgeAgent.sol VirtualAccount.sol BranchPort.sol BranchBridgeAgent.sol RootPort.sol ERC20hTokenRoot.sol ERC20hTokenBranch.sol BranchBridgeAgentFactory.sol
I started my analysis by examining the ArbitrumBranchPort.sol contract. This is the Ulysses Port implementation specifically designed for deployment on the Arbitrum Branch Chain. Ports are essential components of the protocol, making it a critical contract to audit first.
Then, I turned our attention to the ArbitrumCoreBranchRouter.sol contract represents the core Branch Router implementation tailored for the Arbitrum deployment. Routers play a fundamental role in the protocol's operation, so auditing this contract is crucial.
RootBridgeAgent.sol: Responsible for interfacing with Users and Routers within the Root Environment, this contract acts as a middleman. It's a central component that interacts with the protocol's users, making it an important contract to consider for auditing.
VirtualAccount.sol: As a contract that allows users to manage assets and perform remote interactions, it plays a pivotal role in user interactions. Auditing its functionality is important to ensure the security of user assets.
BranchPort.sol: This contract manages the deposit and withdrawal of underlying assets in response to requests from Branch Bridge Agents. Given its role in handling user assets, it's a key contract for auditing.
BranchBridgeAgent.sol: Deployed in Branch Chains, this contract is responsible for interfacing with Users and Routers. Since it interacts with users and bridges between chains, auditing its functionality is essential.
RootPort.sol: Manages the deposit and withdrawal of assets between the Root Omnichain Environment and every Branch Chain in response to Root Bridge Agents' requests. Given its central role in asset movement, auditing is crucial.
ERC20hTokenRoot.sol: This contract represents a 1:1 ERC20 representation of a token deposited in a Branch Chain's Port in the Root Chain. Auditing token contracts is essential to ensure the security of token handling.
Documentation Review:
In the main time Review this document and Learn some basic concept in Readme.
Manuel Code Review In this phase, I initially conducted a line-by-line analysis but this was a little harder to me because of a alot of contracts, following that, I engaged in a comparison mode.
Line by Line Analysis: Pay close attention to the contract's intended functionality and compare it with its actual behavior on a line-by-line basis.
Comparison Mode: Compare the implementation of each function with established standards or existing implementations, focusing on the function names to identify any deviations.
Architecture of the key contracts that are part of the Maia DAO - Ulysses:
The Ulysses Protocol is designed to be an "Omnichain Liquidity Protocol" that connects multiple blockchain networks to enhance liquidity and capital efficiency in the DeFi space. Its architecture involves several key components:
Root Chain: The protocol operates on a root blockchain, which serves as the primary chain for managing the protocol's core functionalities.
Branch Chains: These are secondary blockchains connected to the root chain. Branch chains play a crucial role in handling specific tasks related to liquidity, asset management, and interactions with various DeFi platforms.
Liquidity Pools: Within each branch chain, there are liquidity pools where users can deposit their assets. These pools are managed by the protocol and serve as the source of liquidity for various DeFi activities.
Bridge Agents: Bridge agents act as intermediaries between different chains. They facilitate the movement of assets between the root chain and branch chains, allowing users to deposit and withdraw assets seamlessly.
Virtual Accounts: Users interact with the protocol through virtual accounts, which enable them to manage assets and perform actions across different chains remotely.
Token Contracts: The protocol uses ERC-20 token contracts on both root and branch chains to represent assets. These tokens are created when users deposit assets into liquidity pools.
Factories: Factory contracts allow for the permissionless deployment of various protocol components, including bridge agents and tokens.
Overall, I consider the quality of the to be excellent. The code appears to be very mature and well-developed. Details are explained below:
Codebase Quality Categories | Comments |
---|---|
Code-Readability | The codebase readability of contracts is quite good. The extensive comments and clear structure make it accessible for developers to understand and work with. However, the actual effectiveness and security of the contract also depend on the implementation of the functions and the correctness of the logic, which would require a more thorough code review. |
Modularity | While some contracts are relatively large, they do serve specific purposes. Consider further decomposing larger contracts into smaller, more modular components when possible, as it can improve maintainability and readability. Additionally, leveraging well-tested external libraries and interfaces contributes to modularity. |
Documentation-Comments | The contracts includes extensive inline comments, which is excellent for understanding the code's purpose and functionality. These comments help developers understand each function's role and usage. It follows the NatSpec style for comments.The comments explanations for the contract's purpose, functions, and important variables, making it easier for others to review and work with the code. They also specify details such as function parameters, flags, and expected behavior. |
Code-Complexity | The contracts has multiple components and handles various functionalities, it appears to be reasonably structured and modular. The use of interfaces and inheritance helps manage complexity and promotes code reuse in contracts. However, the contracts do involves some complexities due to the nature of its responsibilities, cross-contract communication, and error handling. Careful testing and auditing would be essential to ensure the correctness and security of the contracts |
Event-Logging | Event logging is a crucial mechanism for emitting notifications and recording significant state changes within the smart contract. Events allow external applications and users to track and respond to these changes on the blockchain but some contracts do not include event logging, Including events to log important contract actions is generally recommended. |
Error-Handling | A lot of contracts includes error handling mechanisms using require statements to revert transactions when certain conditions are not met. it's recommended to use custom errors, using the error keyword. Custom error types are used to throw exceptions in specific error scenarios to help improve the clarity of error messages and provide more information about why a transaction failed. |
Testing | This test script outlines the process for building and testing contracts in a Layer Zero fork testing environment. It covers setting up environment variables, installing libraries, and provides instructions for using 'LzForkTest' to change chains, along with static code analysis using Slither |
The ArbitrumBranchBridgeAgent contract facilitates cross-chain communication between different environments (e.g., Arbitrum Branch Chain and Root Omnichain Environment). Systemic risks could occur if there are issues with the interoperability of these environments, leading to unexpected behavior.
The RootBridgeAgent maintains a mapping of executionState to track the execution status of requests from different chains. If there's a bug in how this state is managed or if an attacker can manipulate it, it could lead to unintended execution or denial of service.
The MulticallRootRouter contract relies on the initialize function to set the bridgeAgentAddress. Centralization risks may arise if this function is called by the contract owner and the owner has centralized control over contract initialization.
MulticallRootRouter contract has Non-Reversible Functions, Certain functions in the contract, such as executeResponse and executeDepositSingle, are designed to revert when called. While this might be intentional, it can limit the contract's functionality and could cause issues if not properly documented.
The CoreRootRouter contract allows adding global tokens (_addGlobalToken
). If a malicious token is added, it could lead to systemic risk for all users.There's no mechanism to remove global tokens, which can be a risk if a token needs to be delisted.
The ArbitrumCoreBranchRouter contract relies on a branchBridgeAgentFactory address to create and add new bridge agents. If this address is controlled by a centralized entity, it could control the creation of bridge agents, potentially affecting the system's decentralization.
The ArbitrumCoreBranchRouter contract uses a fallback function (executeNoSettlement) to execute different functions based on function selectors in the provided data. If not properly validated, this could lead to unexpected behavior and vulnerabilities.
In ArbitrumBranchBridgeAgent contract the owner address has significant control over contract actions. If the owner address is compromised or controlled by a single entity, it could lead to centralization concerns, as this entity can potentially manipulate contract behavior.
BranchPort contract has several state variables that can be modified by external parties. These state variables include lists of bridge agents, bridge agent factories, strategy tokens, and port strategies. If these lists grow too large or are modified in unexpected ways, it could affect the contract's behavior and performance.
In ArbitrumBranchPort contract owner can control the execution of bridging functions (_bridgeIn
and _bridgeOut
), which can impact the movement of assets between chains. Centralization concerns may arise if there is limited oversight or transparency in how these functions are executed.
This RootBridgeAgentExecutor contract relies on RootBridgeAgent
to execute various operations. If RootBridgeAgent is centralized or controlled by a single entity, it introduces centralization risks.
The ERC20hTokenRootFactory contract manages port strategies and their associated tokens. maybe some vulnerabilities in managing strategies could lead to risks, including unauthorized manipulation of strategies.
The factoryAddress is set to msg.sender in the constructor of RootBridgeAgent, which means that only the deployer of the contract can create instances of this contract. This centralization of deployment authority can be a risk if the deployer becomes malicious or compromised.
The VirtualAccount uses the requiresApprovedCaller modifier to control access. It allows either the owner or approved routers to access the contract. If the approval process for routers is not sufficiently decentralized or transparent, it could centralize control over the contract.
The payableCall function allows multiple external calls with different values to be executed in a single transaction in VirtualAccount contract. Depending on how this function is used, this is centralize control over funds and introduce risks related to the order of execution.
MulticallRootRouterLibZip uses data compression through the LibZip library's cdDecompress function. While compression can be useful for saving gas costs, it introduces a potential systemic risk if the decompression process is not well-audited or has vulnerabilities.
there is a centrlization risks in RootPort contract if this contract allows the owner to add new chains and set various parameters related to these chains. If the owner has unilateral control over adding chains, it centralizes control over the network's expansion.
The BranchBridgeAgentExecutor contract handles settlements involving tokens and native assets. Incorrect execution of settlements or vulnerabilities in the settlement process can result in financial losses, which can be systemic risks and aslo the contract supports multiple settlements in a single transaction. This introduces complexity and potential risks related to managing multiple assets and settlements within a single transaction..
Properly managing these risks and implementing best practices in security and decentralization will contribute to the sustainability and long-term success of the Venus Prime protocol.
Gas optimization techniques are critical in smart contract development to reduce transaction costs and make contracts more efficient. The Canto protocol, as observed in the provided contract, can benefit from various gas-saving strategies to enhance its performance. Below is a summary of gas optimization techniques followed by categorized findings within the contract:
Finding | Occurrences |
---|---|
Use mappings instead of arrays | 42 |
Using Storage instead of memory for structs/arrays saves gas | 14 |
UseĀ assemblyĀ to writeĀ address storage values | 40 |
A modifier used only once and not being inherited should be inlined to save gas | 12 |
ERC1155 is a cheaper non-fungible token than ERC721 | 3 |
Use one ERC1155 or ERC6909 token instead of several ERC20 tokens | 9 |
Using assembly to revert with an error message | 177 |
Split revert statements | 1 |
Understand the trade-offs when choosing between internal functions and modifiers | 27 |
Using > 0 costs more gas than != 0 | 17 |
Do-While loops are cheaper than for loops | 15 |
Use assembly to reuse memory space when making more than one external call | 44 |
Missing Zero address checks in the constructor | 14 |
Short-circuit booleans | 4 |
15 Pre-increment and pre-decrement are cheaper thanĀ +1 ,-1 | 4 |
Use hardcode address instead address(this) | 33 |
Use bytes.concat() instead of abi.encodePacked(), since this is preferred since 0.8.4 | 46 |
Shorten the array rather than copying to a new one | 14 |
Initializers can be marked as payable to save deployment gas | 9 |
Use assembly to validate msg.sender | 22 |
Use selfdestruct in the constructor if the contract is one-time use | |
Avoid contract calls by making the architecture monolithic | 8 |
It is sometimes cheaper to cache calldata | 7 |
Use ++i instead of i++ to increment | 4 |
Always use Named Returns | 16 |
Use bitmaps instead of bools when a significant amount of booleans are used | 10 |
Empty blocks should be removed or emit something | 5 |
The Maia DAO Ulysses Protocol is a comprehensive and ambitious project aiming to create an Omnichain Liquidity Protocol that addresses the challenges posed by fragmented liquidity in the DeFi space. The protocol's architecture involves multiple contracts and components designed to enable users to deploy assets on a single blockchain and earn rewards from activities spanning multiple blockchains. It also simplifies liquidity management across different blockchains and provides users with a single unified token for versatility in DeFi applications.
In this analysis, we've reviewed several key contracts within the protocol, providing insights into their functionality, codebase quality, and potential systemic and centralization risks. Here are some key takeaways:
Codebase Quality:
The codebase quality is generally good, with extensive inline comments that provide clear explanations of the contract's purpose, functions, and variables. Contracts are modular, making use of interfaces and inheritance for code reuse and maintainability. Error handling is present in contracts, utilizing require statements for validation. The use of events for logging significant state changes could be improved, and more events could be added for transparency. Some contracts involve complexity due to cross-contract communication and error handling, highlighting the need for careful testing and auditing.
Potential Systemic & Centralization Risks:
Systemic risks can arise from cross-chain communication, interoperability issues, and unexpected behavior. Centralization risks are present in contracts where the owner has significant control over contract actions or deployment, which can lead to a lack of decentralization. Contracts that rely on external addresses, such as factories, can introduce centralization risks if not managed properly. Proper testing, auditing, and the implementation of best practices in security and decentralization are essential to mitigate these risks effectively.
Gas Optimization:
Gas optimization techniques are essential for reducing transaction costs and improving contract efficiency. The contract could benefit from various gas-saving strategies, including the use of mappings instead of arrays, storage for structs/arrays, assembly for address storage values, and more. Several opportunities exist to optimize gas usage in the contract, such as using ERC1155 or ERC6909 tokens instead of multiple ERC20 tokens and using assembly for certain operations.
40 hours
#0 - c4-pre-sort
2023-10-15T14:00:14Z
0xA5DF marked the issue as sufficient quality report
#1 - alcueca
2023-10-20T12:57:24Z
Original content Decent contract by contract description Excellent risk analysis
#2 - c4-judge
2023-10-20T12:57:29Z
alcueca marked the issue as grade-a