Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · February 2025

Multihop

for USDT0

USDT0 engaged Guardian to review the security of their USDT0 multihop contract. From the 6th of February to the 10th of February, a team of 2 auditors reviewed the source code in scope.

Published
Review window
February 6 to 10, 2025
Language
Solidity
Chains
Ethereum, Arbitrum, Ink, Hyperliquid, Polygon, Monad, Solana, Stellar
Sector
Stablecoins
  • 0 Critical
  • 0 High
  • 1 Medium
  • 8 Low
  • 16 Informational

9 resolved · 2 partially resolved · 14 acknowledged

Scope

Overview

USDT0 engaged Guardian to review the security of their USDT0 multihop contract. From the 6th of February to the 10th of February, a team of 2 auditors reviewed the source code in scope.

Findings 25

  1. M-01 Medium Funds Trapped On Ethereum DoS Resolved
    Location
    MultiHopCompserV1.sol

    Description

    The MultiHopComposerV1 contract uses an interface which returns a boolean value for the approve function, however USDT on Ethereum does not return a boolean from the approve function.

    This results in a revert upon approval which will cause the compose message to be un-executable, thereby trapping the USDT in the MultiHopComposerV1 contract.

    Recommendation

    For MultiHopComposerV1 deployment on Ethereum mainnet use an IERC20 interface which does not include a boolean return value for the approve function.

    Resolution

    USDT0 Team: Resolved.

  2. L-01 Low retrySend Is Unusable For Users With Multiple Failures Unexpected Behavior Acknowledged
    Location
    MultiHopComposerV1.sol: 126

    Description

    The retrySend function forces the user to send their entire amountOwed balances in a single oft.send call.

    However the user may have had multiple failed hops, in which case the amountNative would be too large for a single oft.send call and the amountToken amount could be larger than the total amount the user wishes to send to any single receiving address.

    For example:

    • Bob initiates a composed hop A which is to be sent to Alice on Chain 1
    • Bob initiates a composed hop B which is to be sent to Carol on Chain 2
    • Both hop A and B fail and are stored within the amountOwed mapping
    • It is impossible for Bob to retry both of these hops individually since they are grouped into the same

    amountOwed entry

    Recommendation

    Consider allowing users to specify what amount of both tokens they would like to use from their amountOwed mapping entry for the retrySend call.

    Resolution

    USDT0 Team: Acknowledged.

  3. L-02 Low Excess Ether Cannot Be Easily Retrieved Unexpected Behavior Resolved
    Location
    MultiHopComposerV1.sol: 103

    Description

    In the lzCompose function the refund address in the oft.send call is assigned as the MultiHopComposerV1 contract. As a result excess msg.value sent to the lzCompose function remains in the MultiHopComposerV1 contract.

    There is a function for the owner to reclaim native assets from the MultiHopComposerV1 contract, however this function only allows sweeping the entire native balance which includes refunds for failed OFT transfers which should be claimable by those users.

    As a result it is unwieldy for the owner to claim this excess Ether.

    Recommendation

    Consider sending the excess value to a dedicated refund receiver address, or implement logic to track the totality of pending user native refunds and implement a function which allows the owner to withdraw only the excess native tokens which were refunded from the oft.send call.

    Resolution

    USDT0 Team: Resolved.

  4. L-03 Low Force Retry With Insufficient Message Value Censoring Acknowledged
    Location
    MultiHopComposerV1.sol: 77

    Description

    The lzCompose function in the LayerZero EndpointV2 contract can be called by anyone and simply validates that the message contents are the same as those which were sent by the OApp.

    Therefore any arbitrary address can force a retry by executing the compose message with a value less than the MessagingFee specifies, causing a NotEnoughNative revert and _handleError to be triggered.

    A malicious actor can observe that a composed message to the MultiHopComposerV1 contract has been posted, or force it to be posted themselves by executing the parent lzReceive message, and then use their own malicious lzCompose invocation to force the Oft.send call to fail.

    Recommendation

    Ensure the msg.value matches the fee.

    Resolution

    USDT0 Team: Acknowledged.

  5. L-04 Low Force Retry With Insufficient Gas Censoring Acknowledged
    Location
    MultiHopComposerV1.sol: 77

    Description

    The lzCompose function in the LayerZero EndpointV2 contract can be called by anyone and simply validates that the message contents are the same as those which were sent by the OApp.

    Therefore any arbitrary address may invoke the lzCompose function for a MultiHopComposerV1 compose call with an amount of gas such that the oft.send function reverts with out of gas, but the rest of the execution completes due to the reservedGas.

    A malicious actor can observe that a composed message to the MultiHopComposerV1 contract has been posted, or force it to be posted themselves by executing the parent lzReceive message, and then use their own malicious lzCompose invocation to force the Oft.send call to fail.

    Recommendation

    Consider adding validation to the lzCompose function that requires that a sufficient amount of gas has been provided by the caller to successfully execute the oft.send call.

    Resolution

    USDT0 Team: Acknowledged.

  6. L-05 Low Blacklisted Addresses May Retrieve Funds Unexpected Behavior Acknowledged
    Location
    MultiHopComposerV1.sol

    Description

    Through the lzCompose function USDT tokens may be credited to an arbitrary evmRefundAddress with funds in the amountOwed. The evmRefundAddress may be blacklisted at the time of the lzCompose execution or may be blacklisted in the future.

    In either case, the blacklisted address is able to reclaim their funds through either the retrieveFunds or retrySend functions.

    Recommendation

    Consider adding validation to require that the user is not blacklisted in the retrieveFunds and retrySend functions.

    Resolution

    USDT0 Team: Acknowledged.

  7. L-06 Low Reentrancy Checks Abused For Censoring Censoring Acknowledged
    Location
    Global

    Description

    The send function in the StarGatePool contract invokes the sendToken function which uses the nonReentrantAndNotPaused modifier.

    If any functions in the StarGatePool contract with the nonReentrantAndNotPaused modifier have been entered into then any subsequent calls to send will revert due to the reentrancy check.

    This behavior can be leveraged to censor composed hops by causing them to fail on the Oft.send function.

    A malicious actor can observe that a composed message to the MultiHopComposerV1 contract has been posted, or force it to be posted themselves by executing the parent lzReceive message, and then use their own malicious lzCompose invocation to force the Oft.send call to fail.

    The malicious actor can first invoke their own dummy StarGatePool.send invocation to trigger the reentrancy guard, and then inside of that function call, during the native refund callback to their address.

    The malicious actor can invoke the lzCompose function on the EndpointV2 contract to trigger the MultiHopComposerV1.lzCompose function which will then make a call to the StarGatePool.send function that fails due to a reentrant call.

    The user’s send is then censored and they are forced to retry their send with the retrySend function.

    Recommendation

    If the target oft address is the StarGatePool, consider checking if the public status value is ENTERED, and if so reverting the lzCompose transaction rather than censoring the transaction for the user. Otherwise the lzCompose function can validate that the executor is a trusted executor.

    Resolution

    USDT0 Team: Acknowledged. 16

  8. L-07 Low Unexpected Zero Amount Oft Sends Validation Acknowledged
    Location
    MultiHopComposerV1.sol: 126

    Description

    The retrySend function does not validate that the sender has a nonzero token balance in the amountOwed mapping before triggering the OFT send. As a result any user may call the retrySend function and trigger an OFT send from the MultiHopComposerV1 contract with a zero token amount.

    This may be unexpected, particularly since the user can send composed messages from the MultiHopComposerV1 along with this zero token amount.

    Recommendation

    Consider validating that the user has a nonzero amountToken in the retrySend function.

    Resolution

    USDT0 Team: Acknowledged.

  9. L-08 Low Insufficient Reserved Gas Warning Resolved
    Location
    MultiHopComposerV1.sol

    Description

    With additional logic inside of the forceApprove function and the added gas for an external token approval call, the reservedGas is not sufficient for the _handleError function and forced approval to occur in the event that the OFT send runs out of gas.

    Recommendation

    Consider increasing the reservedGas by a minor amount to 42,000.

    Resolution

    USDT0 Team: Resolved.

  10. I-01 Informational Unused Import Imports Resolved
    Location
    MultiHopComposerV1.sol

    Description

    In the MultiHopComposerV1 the IOAppCore interface is imported but not used in the contract code.

    Recommendation

    Remove the extraneous IOAppCore interface import.

    Resolution

    USDT0 Team: Resolved.

  11. I-02 Informational Missing Event Events Resolved
    Location
    MultiHopComposerV1.sol: 121

    Description

    In the retrieveFunds function there is an event to indicate the retrieval of the underlying token with the LogRetrieveFunds event, but no event nor data entry in the LogRetrieveFunds event to indicate that native value was retrieved from the contract.

    Similarly there is no indication of native value retrieved in the retrySend function.

    Recommendation

    Consider either adding a native value field to the LogRetrieveFunds event or introducing an event to indicate the retrieval of native funds in the retrieveFunds and retrySend function.

    Resolution

    USDT0 Team: Resolved.

  12. I-03 Informational Unused Events Superfluous Code Resolved
    Location
    MultiHopComposerV1.sol

    Description

    The MultiHopComposerV1 contract contains the LogTooHighSendAmount and Swapped events which are declared but never used in the contract.

    Recommendation

    Implement the use case for the LogTooHighSendAmount or Swapped events or remove them from the contract.

    Resolution

    USDT0 Team: Resolved.

  13. I-04 Informational Lacking Zero Address Checks Validation Resolved
    Location
    MultiHopFactoryV1.sol: 17

    Description

    The constructor for the MultiHopFactoryV1 contract performs no zero address validation on the _endPoint address.

    Recommendation

    Consider performing validation on the _endPoint parameter to ensure it is not the zero address.

    Resolution

    USDT0 Team: Resolved.

  14. I-05 Informational Outdated Documentation Documentation Partially resolved
    Location
    MultiHopComposerV1.sol

    Description

    The documentation for the constructor of the MultiHopComposerV1 contract references the StableComposer contract as the contract which it constructs. However the MultiHopComposerV1 is the contract which is constructed.

    Additionally, the documentation for the contract indicates that The contract is intended to unwrap USDT0 into native gas tokens. However this is not the current functionality of the contract.

    Recommendation

    Update the comment for the constructor to reflect that it is the MultiHopComposerV1 contract which is being constructed. And update the documentation for the contract to reflect its current intended behavior.

    Resolution

    USDT0 Team: Partially Resolved.

  15. I-06 Informational Zero Transfer Attempted Superfluous Code Acknowledged
    Location
    MultiHopComposerV1.sol: 117

    Description

    Within the retrieveFunds function, if retrieveNative is set to true but the amountToken has already been retrieved, the function will still attempt to transfer amountToken even if it is a zero amount.

    For USDT and USDT0 implementations this is not a large concern as they do not revert on zero amount transfers, however if the MultiHopComposerV1 contract were to be used with a token that reverts on zero transfers this would prevent claiming of native funds.

    Recommendation

    Consider only executing the token transfer if the amountToken is larger than zero as an optimization.

    Resolution

    USDT0 Team: Acknowledged.

  16. I-07 Informational Arbitrary Oft Address Best Practices Acknowledged
    Location
    MultiHopComposerV1.sol: 126

    Description

    The MultiHopComposerV1 contract accepts an arbitrary oft address as a part of the composed _message in the lzCompose and retrySend functions.

    While no exploit has been identified with the call to an arbitrary oft address, out of an abundance of caution it may be best to limit the attack surface by requiring that the provided oft address is explicitly whitelisted.

    It may be noteworthy that an untrusted oft contract can:

    • Remove the approved amount of USDT from the MultiHopComposerV1 contract
    • Revert on purpose, causing a _handleError invocation
    • Consume an unexpected amount of gas
    • Return malformed returndata, causing a revert of the lzCompose function
    • Re-enter into MultiHopComposerV1 functions as well as other related OFT/LayerZero systems.

    Recommendation

    Consider introducing a whitelistedOfts mapping and validating the oft addresses used against this.

    Resolution

    USDT0 Team: Acknowledged.

  17. I-08 Informational Malformed Composed Messages Trap USDT Trapped Funds Acknowledged
    Location
    MultiHopComposerV1.sol

    Description

    In the lzCompose function in the event that the Oft.send function reverts a refund is stored for users with the _handleError function. However in the event that a revert occurs outside of the Oft.send function the user will not be able to retrieve their funds from the MultiHopComposerV1 contract.

    Specifically, if the composed _message is malformed and does not contain the expected evmRefundAddress, oft, sendParam, and messageingFee types with no dirty upper bits then the lzCompose will chronically revert and the composed action can never be executed.

    Recommendation

    Be aware of this risk and warn users and integrators to verify the correctness of their composed message structure.

    Resolution

    USDT0 Team: Acknowledged.

  18. I-09 Informational Arbitrary Oapp Deployed Through Factory Warning Partially resolved
    Location
    MultiHopComposerFactoryV1.sol

    Description

    Function MultiHopComposerFactoryV1.createMultiHopComposer takes an arbitrary oApp address. Therefore, users can populate the composers mapping with a composer that uses a malicious Oapp and lead to user interaction with an undesired contract and token within the MultiHopComposer.

    There is also risk with a block reorg that a user may believe they are interacting with a composer with a safe Oapp, but within the context of the reorg a composer with a malicious OApp is deployed to that address first leading to users interacting with a malicious contract unexpectedly.

    This is because the CREATE opcode relies solely on the sender address and nonce. Consider the following example:

    (1) Alice deploys a composer with a safe Oapp A to address A. (2) Bob deploys a composer with a malicious Oapp B to address B. (3) A reorg occurs, such that Bob’s tx is included first and the malicious composer is now deployed to address A, since the sender address and nonce are the same as when Alice created a composer initially. (4) Users who thought they were interacting with Alice’s safe composer at address A now interact with Bob’s malicious composer.

    Recommendation

    Consider enforcing a whitelist for the oApp or document this risk.

    Resolution

    USDT0 Team: Partially resolved by using CREATE2 to mitigate re-org risk.

    Guardian Team: Arbitrary addresses are still allowed to create arbitrary multihop contracts with 27 arbitrary OApps, this is acknowledged by the team.

  19. I-10 Informational SendParam Can Differ From Initial lzCompose Warning Acknowledged
    Location
    MultiHopComposerV1.sol

    Description

    The SendParam initially used for the lzCompose can differ from the one passed by users during retrySend.

    Recommendation

    Be aware of this behavior.

    Resolution

    USDT0 Team: Acknowledged.

  20. I-11 Informational Stargate Only Allows Same-Asset Transfers Documentation Acknowledged
    Location
    Global

    Description

    Stargate currently does not support Bera/INK USDT0 to be transferred to USDT that is on other chains such as Polygon. Therefore, users should be aware that for most USDT0 transfers Arbitrum will be the intermediate step.

    Recommendation

    Make users aware that these pathways must be implemented on StarGate and cannot immediately be supported.

    Resolution

    USDT0 Team: Acknowledged.

  21. I-12 Informational Refunds Are Not Supplied Documentation Acknowledged
    Location
    MultiHopComposerV1.sol

    Description

    When invoking the oft.send function through lzCompose the refund address is the MultiHopComposer. In contrast, when sending through retrySend the refund address is the msg.sender.

    This asymmetry makes it preferable for a user to attempt a retry to ensure unused native fee is returned to them rather than donated to the MultiHopComposer to be swept by the owner at a later time.

    Recommendation

    It may be unpreferred to allow the user to specify a refund address for the oft.send invocation given the address can make arbitrary actions, expend an unexpected amount of gas, or simply revert.

    Instead, the USDT0 team may consider implementing amountOwed logic in the receive function to credit the from address of the current compose message, but only when an lzCompose invocation is in progress.

    Resolution

    USDT0 Team: Acknowledged.

  22. I-13 Informational Arguments Can Be Calldata Optimization Acknowledged
    Location
    MultiHopComposerV1.sol: 126

    Description

    The messagingFee function of the retrySend function is not modified and can therefore be declared as calldata.

    Recommendation

    Consider declaring the messagingFee parameter as calldata instead of memory.

    Resolution

    USDT0 Team: Acknowledged.

  23. I-14 Informational Lacking setReservedGas Validation Validation Acknowledged
    Location
    MultiHopComposerV1.sol: 64

    Description

    The setReservedGas function does not implement any validation which prevents the owner from assigning the reservedGas value to an errantly high amount.

    If the reservedGas is assigned too high it will cause underflow reverts and can potentially trap USDT funds in the MultiHopComposerV1 contract.

    Recommendation

    Consider adding maximum configurable value validation for the setReservedGas function.

    Resolution

    USDT0 Team: Acknowledged.

  24. I-15 Informational retrySend DoS DoS Resolved
    Location
    MultiHopComposerV1.sol: 126

    Description

    The oft.send function requires that the provided msg.value is exactly the same as the nativeFee specified by the MessagingFee parameter.

    Since the value sent to the oft.send function is based upon amountNative stored in the amountOwed mapping, it is possible for a malicious actor to frontrun an attempt to retrySend and increase the amountNative entry to DoS the retrySend invocation.

    The malicious actor can achieve this with a composed message waiting in the EndpointV2 which uses the target victim address as the evmRefundAddress and sends to an invalid eid to force a send revert.

    The malicious actor can then frontrun the retrySend invocation to execute their own compose message and increment the amountNative by even just 1 wei.

    Recommendation

    Be aware of this frontrunning DoS vector and document it for users and integrators of the MultiHopComposerV1.

    Resolution

    USDT0 Team: Resolved.

  25. I-16 Informational Smart Contract Claimers Incompatibility Compatibility Resolved
    Location
    MultiHopComposerV1.sol: 139

    Description

    In the retrySend function the refundReceiver is hardcoded as the msg.sender. As a result smart contract addresses which do not implement a receive function cannot accept the refund and therefore cause a revert when additional native fees are provided.

    Users may provide an evmRefundAddress which can accept native funds to avoid this, however if the provided evmRefundAddress is not compatible then the user will be forced to redeem their funds with the retrieveFunds function to a recipient address.

    Recommendation

    Consider allowing the user to specify a refundRecipient address in the retrySend function.

    Resolution

    USDT0 Team: Resolved.

More from USDT0

All 20 reports
  1. Stellar Deployment

    2 findings 2 findings: 2 informational
  2. Corn Network Delisting

    1 finding 1 finding: 1 low
  3. Canary Chain Configuration Verification

    4 findings 4 findings: 4 informational
  4. Solana Transaction Verification

    4 findings 4 findings: 1 medium, 3 informational

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.

Get a quote