Organization
- @infinifi
Engagement Type
Cantina Reviews
Period
-
Findings
High Risk
1 findings
1 fixed
0 acknowledged
Medium Risk
3 findings
1 fixed
2 acknowledged
Low Risk
4 findings
2 fixed
2 acknowledged
Informational
6 findings
2 fixed
4 acknowledged
Gas Optimizations
4 findings
3 fixed
1 acknowledged
High Risk1 finding
Losses up to liquid buffer do not burn shares, overstating supply
Severity
- Severity: High
Submitted by
r0bert
Description
OutlandVault.portalUpdateis intended to align the farm’s share balance with the externally reported_totalAssetsValue. When a loss is reported it computessharesLost = sharesTotalBefore - sharesTotalAfterandsharesLiquid = liquidShares(), then only burns ifsharesLost > sharesLiquid.If the loss is less than or equal to current liquidity, the burn branch is skipped and the farm keeps its old share balance. Example: the farm holds 200 shares, the vault has 100 shares worth of liquid assets and
_totalAssetsValueis reported as 150.Do notice that this state with higher shares than liquid assets is easily achieved simply after a
portalUpdatecall that reported some earned yield. Continuing the example, heresharesLost=50andsharesLiquid=100and assharesLostis lower thansharesLiquidno burn occurs and the farm remains at 200 shares even though assets are only 150. Accounting that trusts share supply now overstates assets by 50 and redemptions later can hitInsufficientLiquiditybecause outstanding shares exceed backing.This design prevents readjusting previously recognized assets downward when a loss is within the liquid buffer, so the share supply stays overstated and the reported asset value cannot be corrected for those losses.
Recommendation
On losses, always burn the full delta between
sharesTotalBeforeandsharesTotalAfter, capped by the farm balance, without gating on liquidity. This keeps the farm’s share supply equal to portal-reported assets while preserving liquidity backing.InfiniFi Labs: Fixed in 2a71b18.
Cantina: Fix verified. This change fixes the core bug provided one assumption holds:
_totalAssetsValuemust represent only off-vault/remote assets. AddingliquidShares()then setssharesTotalAfterto total (remote + local) assets and burningsharesTotalBefore - sharesTotalAfterrealigns supply with backing regardless of the buffer, which resolves the issue.
Medium Risk3 findings
Cross-chain pause does not halt portals
Severity
- Severity: Medium
Submitted by
r0bert
Description
PortalHubandPortalOutpostinheritPausableviaCoreControlled, yet all bridge entrypoints ignore the pause state.PortalHub.sendTokensandPortalHub.receiveMessagelack thewhenNotPausedguard. Likewise,PortalOutpost.sendTokens,PortalOutpost.sendAssetsUpdateandPortalOutpost.receiveMessageomit pause enforcement.Even after
pause()is invoked by the PAUSE role, keepers and connectors can continue pushing tokens and processing cross-chain messages. This nullifies the intended emergency brake as an incident response can not halt fund movement.Recommendation
Enforce pause on all externally reachable bridge flows. Add
whenNotPausedtosendTokens,sendAssetsUpdateandreceiveMessagein both portals (and optionally restrict special recovery handlers towhenPaused). This lets the PAUSE role stop cross-chain token sends and message handling immediately during incidents.InfiniFi Labs: Fixed in 61b0428.
Cantina: Fix verified.
LayerZero settings are not configured
State
- Acknowledged
Severity
- Severity: Medium
Submitted by
r0bert
Description
The deployment proposal does not perform any LayerZero OApp configuration, so the
ConnectorLZrelies on Layer Zero defaults and unset peers. None of the recommended setup calls from the LayerZero docs are executed (transferOwnership,setPeer,setEnforcedOptions,EndpointV2.setSendLibrary,EndpointV2.setReceiveLibrary,EndpointV2.setReceiveLibraryTimeout,EndpointV2.setConfigfor send/receive,EndpointV2.setDelegate). Without these, the OApp will use default libraries/options leading to routing through unintended defaults rather than explicit configuration.Recommendation
Extend the proposal to explicitly configure the OApp per LayerZero guidance:
- Set the OApp owner/delegate to your multisig wallet.
- Register peers for each destination EID.
- Set enforced options and send/receive libraries (and timeouts/configs) for each path. Choose the right send and receive library here: https://docs.layerzero.network/v2/deployments/deployed-contracts
- Consider adjusting the confirmations respecting the chain's finality.
- Finally, make sure the confirmations set for the send library in Chain A to B, are the same confirmations set for the receive library in Chain B from A.
This ensures deterministic routing and prevents reliance on unspecified defaults.
If you are planning to skip the manual LZ configuration steps (setting the send/receive libraries, confirmations...) consider checking the defaults in layerzeroscan: https://layerzeroscan.com/tools/defaults?version=V2. However, do note, that these settings are not always up to date.
Relevant documentation link: https://docs.layerzero.network/v2/get-started/create-lz-oapp/configuring-pathways
InfiniFi Labs: Acknowledged.
portalUpdate burn logic in OutlandVault is sensitive to out of order assets updates
State
- Acknowledged
Severity
- Severity: Medium
Submitted by
slowfi
Description
The function
portalUpdatefrom contractOutlandVaultadjusts the farm’s share balance so that the total shares match the_totalAssetsValuereported by the portal. When_totalAssetsValueis lower than the current share supply and the calculated loss exceeds the currently liquid shares, the function burns part of the farm’s shares.This behaviour is correct only if
_totalAssetsValueis a fresh and ordered view of the total position (local vault liquidity plus remote liquidity). In practice, cross-chain messaging introduces latency and potential reordering, so assets updates can arrive out of date relative to subsequent liquidity-changing actions such asportalDepositandportalWithdraw.The burn path is triggered whenever an update is lower than the current share supply and
sharesLost > sharesLiquid. That can happen any time there is a mismatch between the ordering/freshness of assets updates and later actions, for example:Single stale update after profit and withdrawal, turning profit into a loss
- Snapshot/report at 100 (message in flight).
- Profit on L1:
portalDeposit(+60) →shares = 160,liquidity = 160. - Liquidity sent out before the report lands:
portalWithdraw(140)→liquidity = 20, shares still 160 (no burn here). - Stale
portalUpdate(100)arrives:sharesLost = 60,sharesLiquid = 20, so it burns60 - 20 = 40 shares. Supply drops to 120 while only 20 USDC remain. Part of the profit was treated as a loss solely because the report was stale relative to the intervening mint/withdraw steps.
Similar patterns can occur with multiple updates in flight that are delivered out of order (a higher total applied first, then an older lower total applied later), or with reordering between transfer messages and updates (liquidity bridged out after the snapshot but before the update is processed).
The guard that prevents burning below the current local liquidity (
sharesLost > sharesLiquid) ensures local backing, but does not guarantee that the resulting share supply remains aligned with the latest intended cross-chain total. As a result, the effective accounting of profit and loss becomes sensitive to message ordering rather than purely to the latest global state.Recommendation
Consider to make
portalUpdateresilient to out-of-order or stale assets updates, so that share adjustments always reflect the latest intended total. Possible options include:- Adding a monotonic nonce or timestamp to assets updates and ignoring or reverting any update older than the last applied one.
- Tracking the latest accepted total and rejecting updates that attempt to move the total to a value inconsistent with known liquidity movements (for example, ignoring a lower total that would retroactively convert previously realized profit into loss).
- Moving from absolute total updates to delta-based updates or a sequenced state machine, so that each update represents a well-ordered change relative to the last applied state.
These changes would reduce the dependency on cross-chain message ordering and help keep the vault’s share supply aligned with the protocol’s latest cross-chain accounting.
InfiniFi Labs: Acknowledged. It is a classic race condition issue. We plan to do these updates every 6-8 hours. In any case message ordering is not enforced and our backend will be configured to never attempt sending new messages until previous ones are delivered.
Low Risk4 findings
Stale liquidity sync enables MEV on accounting
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
OutlandVaultonly reconciles farm shares to on-chain liquidity when_syncSharesToLiquidity(orportalUpdate) is explicitly called inside deposit/redeem/portal flows. Between syncs, the farm’s share balance stays unchanged while token balances can drift (airdrop/yield/loss), soOutlandFarm.assets()and upstreamAccounting.totalAssetsValue()can be stale. Positive drift understates assets until the next sync mints shares; negative drift overstates assets until a burn. This creates step changes in reported TVL when a privileged actor finally calls_syncSharesToLiquidity/portalUpdate.Because
YieldSharingV2.unaccruedYield()relies onAccounting.totalAssetsValue()vs.receiptToken.totalSupply(), a large positive jump allows public users to mint/lock iUSD/siUSD via the gateway right before (or by) callingaccrue()and capture the pending surplus they did not bear risk for. The accrual mints new receipt tokens to stakers/lockers based on the updated assets, so late joiners can buy cheap to sell at a higher price. Relevant code:function _syncSharesToLiquidity() internal returns (uint256) { uint256 sharesLiquid = liquidShares(); uint256 sharesInFarm = balanceOf(outlandFarm); if (sharesInFarm >= sharesLiquid) return 0; uint256 sharesToMint = sharesLiquid - sharesInFarm; _mint(outlandFarm, sharesToMint); return sharesToMint;}Recommendation
Reduce the step size by syncing frequently (e.g., periodic CCIP/portal updates on a fixed cadence and running the yield smoother), so any jump is small and uneconomic to front-run.
InfiniFi Labs: Acknowledged.
Configurable chainId in PortalBase can diverge from the actual chain ID
State
- Acknowledged
Severity
- Severity: Low
Submitted by
slowfi
Description
The contract
PortalBasedefines the state variablechainIdas a mutable storage variable:uint256 public chainId;and this value is set via initialization logic and used to tag and validate cross-chain messages (for example, when constructing payloads and comparing remote
chainIdvalues).Because
chainIdis taken from configuration instead of the EVM’s nativechainidopcode (block.chainid), the contract relies on correct governance / deployment configuration to match the actual chain ID. A misconfiguration, upgrade, or faulty initialization could lead tochainIdbeing inconsistent withblock.chainidwhile the contract continues to operate, which can cause inconsistent identification of the local chain in the Outland protocol’s cross-chain messages.This is especially relevant for portal contracts, which act as trust anchors for routing and validating messages between chains. Having the local chain identifier as a configurable value increases the surface for configuration errors compared to deriving it directly from the execution environment.
Recommendation
Consider to remove the configurable
chainIdstorage variable inPortalBaseand instead derive the local chain identifier directly from the EVM:- Use
block.chainidin high-level Solidity wherever the local chain ID is needed. - Use the
chainidopcode in inline assembly.
If a stored value is still desired (for gas or interface reasons), consider to:
- Set
chainIdonce usingblock.chainidin the initializer. - Make it effectively immutable (no external setter).
- Optionally add an internal sanity check that
chainId == block.chainidbefore using it in critical message-processing paths.
InfiniFi Labs: Acknowledged.
Missing events on configuration and asset-management state transitions
Severity
- Severity: Low
Submitted by
slowfi
Description
The functions that modify protocol configuration and asset routing in the Outland bridging stack do not consistently emit dedicated events. In particular, the following functions perform state transitions without emitting an event:
-
The function
setConfigurationfrom contractConnectorBaseupdates thechainConfig/selectorConfigmappings used for cross-chain routing, but does not emit an event when configuration changes. -
The functions
enableChainAssetanddisableChainAssetfrom contractConnectorBaseupdate the per-chain supported asset set, but do not emit any event on asset enable/disable. -
The internal function
_setConfigurationfrom contractConnectorBasemutates the same configuration mappings and is used by higher-level configuration functions, but does not emit an event. -
The function
pullAssetsfrom contractConnectorCCIPtransfers tokens from the connector to the portal without emitting any connector-specific event (only the ERC20Transferevent is available). -
The function
setConfigurationfrom contractConnectorLZupdates the cross-chain configuration and LayerZero peers, but does not emit an event. -
The functions
enableOFTanddisableOFTfrom contractConnectorLZupdate theoftsregistry for asset → OFT mapping, but do not emit events when the mapping is added or removed. -
The function
pullAssetsfrom contractConnectorLZtransfers tokens from the connector to the portal without emitting any connector-specific event. -
The function
addConnectorfrom contractPortalBaseadds a new connector address to the authorized set, but does not emit an event. -
The function
removeConnectorfrom contractPortalBaseremoves a connector from the authorized set, but does not emit an event. -
The function
initfrom contractPortalHubsets the core address and hub chain identifier, but does not emit an event. -
The function
setVaultfrom contractPortalHubregisters or updates theOutlandVaultassociated with a chain, but does not emit an event. -
The function
setAssetMappingfrom contractPortalHubupdates the bidirectional mapping between hub assets and outpost assets for a chain, but does not emit an event. -
The function
initfrom contractPortalOutpostsets the core, local chain identifier, hub chain identifier, accounting contract, farm registry, and receiver farm, but does not emit an event. -
The function
setReceiverFarmfrom contractPortalOutpostupdates thereceiverFarmused to receive bridged assets, but does not emit an event.
These functions control critical configuration and asset-routing behaviour for the bridging system. Without explicit events, off-chain monitoring, auditing, and incident response have to rely on low-level storage inspection or indirect effects, which makes it harder to track configuration changes, asset whitelist changes, and updates to routing endpoints over time.
Recommendation
Consider to introduce explicit events for all external (and relevant internal) state transition functions that change configuration or asset routing, including the ones listed above. For example, events can capture the previous and new values for:
- Chain configuration (peer, selector, gas limit) in
ConnectorBaseandConnectorLZ. - Enabled / disabled chain assets in
ConnectorBase. - Enabled / disabled OFTs in
ConnectorLZ. - Added / removed connectors in
PortalBase. - Registered vaults and asset mappings in
PortalHub. - Initialization and updates of
receiverFarmand other key parameters inPortalHubandPortalOutpost. - Asset pulls from connectors (
pullAssets), if you want explicit logs for cross-component token flows in addition to ERC20Transferevents.
This would provide clearer on-chain observability for operational changes, simplify indexer implementations, and help detect misconfiguration or unexpected changes more easily.
InfiniFi Labs: Fixed in e475358.
Cantina: Fix verified. It adds some events (
ConfigurationSet,AssetEnabled/DisabledinConnectorBase;OFTSet/DisabledinConnectorLZ;VaultSetandAssetMappingSetinPortalHub;ReceiverFarmSetinPortalOutpost), but the original issue isn’t totally fixed as:ConnectorBase.pullAssets(used by CCIP and LZ) still emits no connector-specific event, so asset pulls remain unlogged beyond ERC20 Transfers.PortalHub.initstill emits nothing for core/hub chain setup.PortalOutpost.initstill emits nothing for core/chain/accounting/farm registry/receiver setup.
Missing storage gap in upgradeable base contract PortalBase
Severity
- Severity: Low
Submitted by
slowfi
Description
The contract
PortalBasedeclares state variables (chainIdand theconnectorsset) and is intended to be used in an upgradeable setup (through a proxy), but it does not reserve a storage gap at the end of its storage layout.When an upgradeable base contract is inherited by other upgradeable contracts, adding new state variables in future versions without a reserved gap increases the risk of storage layout collisions between the implementation and its proxies or between different layers of inheritance. This can lead to silent corruption of state in future upgrades, especially if the inheritance structure changes or additional fields are appended in derived contracts.
Because
PortalBaseis a core shared base forPortalHubandPortalOutpost, any storage layout mistake here would propagate to all portal implementations.Recommendation
Consider to add a reserved storage gap at the end of the
PortalBasestorage layout (following the common upgradeable-contract pattern) or to adopt an equivalent structured-storage approach. This would provide room for future variables without shifting the existing storage layout and would reduce the risk of state corruption during upgrades for all contracts inheriting fromPortalBase.InfiniFi Labs: Fixed in 3331ae7.
Cantina: Fix verified.
Informational6 findings
CCIP token rate limits unhandled by portals
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
InfiniFi’s CCIP connector and portal flows do not account for Chainlink token pool rate limits. For example, the USDC pool on Base (0x6378c36C44B28f4d1513e7a5510A8481a23eecda, USDCTokenPool) currently has both outbound and inbound rate limiters disabled, so USDC is unaffected. However, any token (including USDC if limits are later enabled) can be throttled at the pool layer:
- Outbound: when a keeper calls
sendTokensvia CCIP, the router invokeslockOrBurnon the pool; if the outbound bucket lacks capacity the pool reverts (TokenRateLimitReached), preventingccipSendfrom emitting and causing the entire portal/connector call to revert, burning gas with no state change. - Inbound: if the pool’s inbound bucket is exhausted, the offRamp’s
releaseOrMintreverts on delivery; the message remains pending and tokens stay locked/burned on the source chain until capacity refills, delaying liquidity and skewingOutlandaccounting.
The connectors do not preflight or surface pool headroom, so operators have no visibility and must manually check these limits. Because of this, enabled or tightened pool limits can silently halt outbound sends or stall inbound delivery for any CCIP-listed token; current configs avoid this for USDC, but newly whitelisted tokens must be checked against Chainlink’s directory (e.g., USDC: https://docs.chain.link/ccip/directory/mainnet/token/USDC; all tokens: https://docs.chain.link/ccip/directory/mainnet) to avoid unexpected throttling.
Recommendation
Add operational checks before enabling a token/route on CCIP: query the token pool’s
getCurrentOutboundRateLimiterStateandgetCurrentInboundRateLimiterStatefor the relevant selectors to confirm limits and headroom, and document the configured rate/capacity for operators. If limits are present, enforce preflight in the keeper workflow (split sends under bucket capacity or wait for refill) and monitor buckets to alert on low headroom. Re-verify these settings whenever a new token is whitelisted or limits are changed.InfiniFi Labs: Acknowledged. This will be monitored off-chain.
USDeOFT rate limits
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
USDe is bridged via an OFT adapter that enforces per-destination rate limits in
_debitbefore a LayerZero send is emitted (USDeOFT.sol,RateLimiter.sol). Limits are keyed by LayerZero endpoint IDs (EIDs). On Arbitrum, for example,rateLimits(30101)returns {amountInFlight:10_008.524852e18,lastUpdated:1764058403,limit:10_000_000e18,window:3600}, meaning about 10k USDe is currently counted against a 10M-per-hour cap that decays over 1h. The limiter is only applied on the send path._credit(receive) is unthrottled, so inbound mints have no rate cap and rely solely on the sender’s limiter. The window/limit can also be set to 0, which silently disables throttling or bricks a route. If the rate limit is consumed, Outland Outpost --> Hub sends (L2->L1 viaPortalOutpost+ConnectorLZ+PortalHub) will temporary revert withRateLimitExceededbefore any message is emitted, stranding the bridge action.Recommendation
Merely informative. Be aware of this behaviour when bridging.
InfiniFi Labs: Acknowledged. This will be monitored off-chain.
Governance receive bypasses xchain auth
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
ConnectorBase.govReceivelets thePROTOCOL_PARAMETERSrole push arbitrary_messagepayloads straight to the portal, bypassing the LayerZero/CCIP transport and their peer/chain validation. The inline comment says “should be gated behind 1 day timelock”, but no delay or extra authentication is enforced. A compromised or malicious governance signer can immediately forge cross-chain effects (e.g., bogus accounting updates or transfer payloads) without any cross-chain authenticity guarantees, collapsing the trust model to a single role and removing the intended safety window. Because of this, governance compromise or misuse can inject arbitrary portal messages instantly.Recommendation
Enforce a timelock/multisig delay on
govReceive(or remove it) and ensure any governance path still validates expected peers/chainIds. Restrict usage to audited emergency flows and emit events for monitoring; prefer routing through the normal transport rather than direct message injection.InfiniFi Labs: Acknowledged. If a chain is unavailable for any reason, something happened, there was a disaster, etc. we will use this method to cast manual updates. Also, if for any reason our bridging partner failed to deliver their message without retry in place, this will be used. It is understandable how this can be abused to create significant losses or inflate the yield. However, doing so is nothing we can profit from other than ruining our reputation. This method is a last resort in case there is anything wrong with a cross-chain communication and is not meant to be used ever in normal circumstances.
Unused imports and using directives across contracts
Severity
- Severity: Informational
Submitted by
r0bert
Description
Several contracts carry unused imports/using directives, adding dead code and inflating bytecode/metadata:
ConnectorBaseimportsIERC20,IERC165,SafeERC20,IAny2EVMMessageReceiver,Client,IRouterClient, and setsusing SafeERC20but never uses them.ConnectorLZimportsCoreControlledwithout any reference.OutlandFarmimportsFixedPointMathLiband appliesusing FixedPointMathLib for uint256without calling its helpers.
Recommendation
Remove the unused imports and associated
usingstatements from these contracts to trim bytecode and eliminate lint noise.InfiniFi Labs: Fixed in 99ceba6.
Cantina: Fix verified.
Missing asset mapping check in receiveMessage
Severity
- Severity: Informational
Submitted by
r0bert
Description
In
PortalHub.receiveMessagethe TRANSFER branch decodessenderTokenand immediately callsassetMapping[senderChainId].get(senderToken)without acontainsguard. If the mapping is missing,EnumerableMap.getreverts with its generic string error instead of the portal’sInvalidAssetrevert used elsewhere:if (messageType == OutlandMsgCodec.TRANSFER) { (address senderToken, uint256 amount) = OutlandMsgCodec.getTransferPayload(_data); address actualToken = assetMapping[senderChainId].get(senderToken); _handleReceiveTokenTransfer(connector, senderChainId, actualToken, amount);}This yields inconsistent revert reasons and less clear failures for bad/unknown assets.
Recommendation
Mirror the send path: check
assetMapping[senderChainId].contains(senderToken)and revertInvalidAsset(senderToken)before callingget. This keeps revert reasons consistent and avoids generic library string reverts.InfiniFi Labs: Fixed in 225452a.
Cantina: Fix verified.
Outland.index.sol test-only contract is located under the main source tree
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The file
Outland.index.solundersrc/integrations/outland/appears to be intended only for testing purposes, but it is placed in the main source directory alongside production contracts. Keeping test-only code in the primary source tree can make it harder to distinguish deployable contracts from testing helpers, and can increase the risk that non-production code is accidentally included in builds, audits, or deployment pipelines. It may also introduce noise for tooling that assumes everything undersrc/is part of the production surface.Recommendation
Consider to move
Outland.index.solto a dedicated test directory (for example,test/or an equivalent testing-specific folder) or adjust the project structure so that test-only contracts are clearly separated from production contracts and excluded from deployment artifacts.
Gas Optimizations4 findings
Cache OFT address in sendTokens to save SLOADs
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
r0bert
Description
ConnectorLZ.sendTokensrepeatedly readsofts[_assetToken]for approval, fee quoting and the send call. Each lookup costs an extra SLOAD. Caching the OFT address once after validation and reusing it would slightly reduce gas for each bridge send. The path is keeper/portal-only, so this is a minor optimization, not a security concern.Recommendation
Store
address oft = ofts[_assetToken];after therequirechecks and reuseoftforforceApprove,quoteSendandsend.InfiniFi Labs: Acknowledged.
String-based revert is inconsistent and less efficient
Severity
- Severity: Gas optimization
Submitted by
slowfi
Description
The function
setPeerfrom contractConnectorLZoverrides the LayerZeroOApphook and unconditionally reverts with a string message indicating thatsetConfigurationshould be used instead.Using a string-based revert here is inconsistent with the rest of the connector contracts, which rely on custom errors for failures. String-based reverts are slightly more gas expensive and less structured than custom errors, and they reduce the ability to programmatically reason about specific failure types. Since this is a deliberate guard rail (to force configuration via
setConfiguration), it fits well as a dedicated custom error.Recommendation
Consider to replace the string-based revert in
ConnectorLZ.setPeerwith a custom error that explicitly conveys that directsetPeerusage is not allowed and thatsetConfigurationmust be used instead. This keeps error handling consistent across the codebase and provides a marginal gas optimization.InfiniFi Labs: Fixed in 99ceba6.
Cantina: Fix verified.
Use of assert in production code
Severity
- Severity: Gas optimization
Submitted by
slowfi
Description
The function
pullAssetsfrom contractConnectorLZusesassert(msg.sender == portal)to validate the caller in production code. The functionsinitfrom contractsPortalHubandPortalOutpostalso useassert(address(core()) == address(0))to enforce one-time initialization.In Solidity ≥0.8,
assertis intended for invariants that are assumed to be always true and, on failure, triggers aPanic(0x01)and consumes all remaining gas. In the cases above, the conditions depend on deployment or configuration correctness and could realistically be violated due to misconfiguration or misuse. Usingassertfor these checks makes the failure mode less diagnosable and more gas-expensive compared to usingrequirewith a custom error. It also goes against the common convention of reservingassertfor internal invariants that the compiler is allowed to treat as unreachable.Recommendation
Consider to replace the
assertstatements inConnectorLZ.pullAssets,PortalHub.init, andPortalOutpost.initwithrequirestatements or custom errors that:- Explicitly validate
msg.senderagainstportalinConnectorLZ.pullAssets, and - Explicitly enforce the one-time initialization guard in
PortalHub.initandPortalOutpost.init.
This would keep the semantics of the checks while providing clearer error reporting, avoiding
Panicreverts, and aligning the contracts with common production best practices for error handling.InfiniFi Labs: Fixed in 6720af0.
Cantina: Fix verified.
Use of memory for _data payload in connector external functions increases gas cost
Severity
- Severity: Gas optimization
Submitted by
slowfi
Description
The function
sendMessagefrom contractConnectorLZtakes its_datapayload as a bytes value stored in memory, even though this function is an external entry point and only reads the payload before forwarding it. The equivalent functions in the CCIP connector and other Outland components (such asgetMessageFee,getSendTokensFee, andsendTokensin both connectors, as well asreceiveMessagein the portal contracts and the message codec helpers) already operate on the message payload in calldata.Using a memory-allocated bytes parameter on an external function causes the compiler to copy the entire payload into memory on every call, which increases gas cost without providing additional safety or functionality in this case. Since
_datais only read and forwarded, keeping it in calldata is sufficient and cheaper. Having one connector function still using memory for the same kind of payload also makes the interface slightly inconsistent between the CCIP and LayerZero connectors.Recommendation
Consider to update the connector interfaces so that all external and public functions receiving the
_datamessage payload use calldata instead of memory, in particular:- Change the
_dataparameter ofsendMessageinConnectorLZto be stored in calldata. - Align any other external/public message-related functions that still use memory for
_data(if present in other versions of the connectors) so they also accept_datain calldata when the payload is only read.
This would remove unnecessary memory copies, reduce gas consumption on message sends and fee estimations, and keep the connector interfaces consistent across both CCIP and LayerZero implementations.
InfiniFi Labs: Fixed in d4e502a.
Cantina: Fix verified.