Guardian's review of OFT for Fun.xyz, published November 2025. The report records 12 findings, including 1 critical and 2 high.
- Published
- Review window
- November 5 to 6, 2025
- Language
- Solidity
- Sector
- Payments
- 1 Critical
- 2 High
- 2 Medium
- 5 Low
- 2 Informational
Scope
1 file in scope · 86 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/OFT/FunOappComposer.sol | 86 | 132 |
Findings 12
-
C-01 Critical All In-Flight Assets Stolen Logical Error Resolved
Description
In the
FunOAppComposercontractlzComposefunction there are several cases where thetoaddress is allowed to be any arbitrary contract, along with any arbitrary calldata:(bool success, bytes memory data) = address(_to).call(_callData);The composer has a case to handle ERC20 OFTs which means it will have a nonzero balance for these ERC20 OFTs when they have been delivered but not yet had their composed message executed.
The arbitrary call to an arbitrary address however allows all ERC20 tokens sitting in the Composer to be stolen. This occurs when a malicious actor provides the token address as the
toand provides the calldata ofapprove(...)to approve themselves to spend the entire composer contract's balance.Recommendation
Consider removing the support of ERC20 OFTs and also removing all calls where the
toaddress is not explicitly thecomposerConfig.VaultWrapper. -
H-01 High Composer Allows Griefing Ethereal DoS Resolved
Description
As mentioned in L-04, there is a griefing vector where the composer is trusting that other delegate depositors do not instantiate alternate subaccounts for the user as this would DoS the composed message for eternity, trapping user funds.
However the FunOAppComposer contract could itself be used to grief accounts in this way. This is because the composed message is what provides the arbitrary calldata which will hold the details of which subaccount and account to deposit into. Any malicious actor could create a composed message that routes through the FunOappComposer system and depositOnBehalf’s a dust amount of USDe for an unsuspecting account who also has an in-flight compose message.
Execution of compose messages is permissionless, so it is easily possible that a malicious actor could execute theirs through the LZ EndpointV2 contract before the user’s honest depositOnBehalf action is executed, thus trapping their cross-chain deposit amounts forever.
Recommendation
Consider requiring that the compose message also includes a signature, which validates that the compose message provided is indeed attested for by the account that is being deposited on behalf of. This signature will include attestation for the subaccount, deposit amount, and deposit token so there is no way to DoS the user’s lzCompose by making the same deposit.
-
H-02 High Open VaultDepositooor Allows Griefing Access Control Resolved
Description
Ethereal requires that only whitelisted delegate depositors can invoke the depositOnBehalf function because there is a risky griefing vector associated with it, whereby a user can DoS the depositOnBehalf flow for an account by registering an unexpected subaccount before the user's cross-chain depositOnBehalf is completed.
The VaultDepositooor contract will be whitelisted for the delegate depositor permission on Ethereal, and also contains a permissionless deposit function which opens this access up to anyone, with arbitrary calldata going to the depositOnBehalf function.
Recommendation
Make the
VaultDepositooordeposit function whitelisted so that only the composer contract can invoke it. -
M-01 Medium Unnecessary Nonce Check DoS Superfluous Code Resolved
Description
Now included in the composeMsg is a nonce value, however this nonce value and the associated check are not necessary as they don't prevent any kind of replay attack as there is no longer any signature of the composeMsg.
This nonce check in fact creates opportunities for user's to have their funds trapped by accidentally using the same nonce in the composeMsg.
Recommendation
Remove all associated nonce logic.
-
M-02 Medium Users Or Other Delegate Depositors Can DoS DoS Acknowledged
Description
The depositOnBehalf function is not allowed when the account has not registered the corresponding subaccount and the account itself has already been initialized.
In other words, if the account already has a separate subaccount that is active, the deposit is not allowed.
This can cause the lzCompose action to be DoS’d in the case where the user creates a deposit for a different subaccount themselves, or another delegateDepositor creates a separate subaccount for the user before the lzCompose action is executed.
In this scenario, the user’s funds in the cross-chain bridge would be permanently trapped.
Recommendation
Understand that you are trusting the other whitelisted delegate depositors to not DoS the lzCompsoe actions this way, and warn users that they should never make a direct deposit to Ethereal before their bridge and lzCompose action is completed.
-
L-01 Low User's Funds Stuck Due To Errant Encoding DoS Acknowledged
Description
In the
lzComposefunction the application requires that the provided_amountinside the user specified_messagecomposed message is exactly equal to the OFT amount that was sent and is recorded as a part of the basecomposeMsginOFTComposeMsgCodec.amountLD(_message).However if an amount that is different than the exact transferred amount is errantly supplied, this will cause the compose execution to fail and the users funds to then be trapped in the composer.
Recommendation
Consider removing the ability to even specify the redundant
_amountvalue in the composed message contents and simply rely on theOFTComposeMsgCodec.amountLD(_message)as the single source of truth. -
L-02 Low Only One DepositOnBehalfRequest Should Be Made Warning Acknowledged
Description
The Ethereal
depositOnBehalffunction accepts a list ofDepositOnBehalfRequestobjects to initiate multipledepositOnBehalfactions.However multiple request objects is incompatible with the VaultDepositooor system as it only replaces the first instance of the
AMOUNT_PLACEHOLDERin the provided call data.Recommendation
Ensure that in the frontend and all flows routing through the
FunOappComposerthat only one suchDepositOnBehalfRequestentry is used. -
L-03 Low Deposits Can Be DoS’d At Cap DoS Acknowledged
Description
The Ethereal system has a cap for deposits of any particular asset, for which only USDe is enabled at the moment.
This means that a deposit through the Fun system that would put the Ethereal system right at the cap, or at least near it, could be DoS’d by sending a small amount of native tokens directly to the Depositooor contract right before the lzCompose action is called.
The lzCompose message would then be stuck until that additional balance in the Depositooor contract is cleared.
Recommendation
Simply be aware of this issue, it is unlikely that such a cap will be reached with a Fun deposit and that a user would grief the system like this.
-
L-04 Low General Calldata Warning Warning Acknowledged
Description
In any lzComposer it is adamant that the call data provided is correctly formatted and contains accurate data. If anything causes a revert in the decoding, or application of the call data — whether that be in the composer contract or the downstream Ethereal contracts, this will permanently trap user funds in the composer contract.
Furthermore, no rescue functions should be added, since for some errors they may only be temporary, and if you were to rescue funds it would then allow users to steal assets from other bridges once their lzCompose message does become executable.
Recommendation
Be extremely cautious about the call data that is provided to the system, it should not trigger any of the revert cases that Ethereal holds in it’s depositOnBehalf flow such as the following amongst several others:
uint256 depositsLength = deposits.length; if (depositsLength == 0) { revert ZeroLength(); } uint16 maxBatchSize = Exchange.load().delegateDepositMaxBatchSize; if (depositsLength > maxBatchSize) { revert DepositOnBehalfBatchSizeExceeded(maxBatchSize); } ICollateralManager.DepositOnBehalfRequest calldata d = deposits[i]; if (d.tokenName != USD_TOKEN_NAME) { revert InvalidDepositOnBehalf(msg.sender, d.account, d.subaccount, "INVALID_TOKEN"); } if (d.account == address(0)) { revert ZeroAddress(); } if (amount == 0) { revert ZeroAmount(); } if (subaccount == bytes32(0)) { revert ZeroLength(); } if (depositToken == address(0)) { revert InvalidDepositToken(depositToken); } -
L-05 Low Refund Is Lost Rounding Acknowledged
Description
The
depositOnBehalffunction may refund a dust amount back to theFunOappComposerbut no logic is implemented to sweep it. These funds are therefore locked in the contract and accumulate over time.Recommendation
Be aware about this behavior and consider to document it.
-
I-01 Informational Missing Zero Address Checks Best Practices Acknowledged
Description
The
constructorof theFunOappComposercontract saves the givencomposerConfigwithout performing zero address checks.Recommendation
Consider to perform zero address checks to follow best practices.
-
I-02 Informational Outdated NatSpec Documentation Acknowledged
Description
The NatSpec for the constructor still references the VaultWrapper contract, however this is no longer used in the contract flow.
Recommendation
Update the NatSpec throughout the composer contract.
No findings match.
More from Fun.xyz
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
