Decent - windhustler's results

Decent enables one-click transactions using any token across chains.

General Information

Platform: Code4rena

Start Date: 19/01/2024

Pot Size: $36,500 USDC

Total HM: 9

Participants: 113

Period: 3 days

Judge: 0xsomeone

Id: 322

League: ETH

Decent

Findings Distribution

Researcher Performance

Rank: 1/113

Findings: 3

Award: $3,078.89

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: iamandreiski

Also found by: EV_om, NPCsCorp, nuthan2x, windhustler

Labels

bug
3 (High Risk)
partial-50
sufficient quality report
duplicate-525

Awards

1742.7673 USDC - $1,742.77

External Links

Lines of code

https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L96

Vulnerability details

Impact

While relaying payloads through DecentEthRouter the gas for relayer to submit the transaction on the receiving chain is hardcoded to 100_000. This was hardcoded with EVM compatible chains in mind and works fine expect for Arbitrum that has a totally different gas model than Ethereum. Differences are summarized in the following article: https://docs.arbitrum.io/devs-how-tos/how-to-estimate-gas. As an example even a simple approve transaction requires more than 100_000 gas on Arbitrum: https://arbiscan.io/tx/0x04f97a39c6d03029518d1953226b343555ea585d2064a24a767155a548b0665e.

As this is hardcoded it will result with messages being sent to Arbitrum to always revert at the beginning of the lzReceive, and will result in blocked pathway between all chains and Arbitrum.

The impact is that the pathway between all chains and Arbitrum will be blocked and no messages can be sent to Arbitrum.

Proof of Concept

DecentEthRouter hardcodes 100_00 gas for the relayer to deliver the message to remote chain. If this is invoked from some chain to Arbitrum the following happens:

  1. The transaction is submitted to Arbitrum with 100_000 gas as gasLimit.
  2. It enters lzReceive and reverts.
  3. This results in storedPayload and blocking the pathway due to nonce ordering.

Tools Used

  • Manual review

Don't hardcode gas costs for each chain but implement a different gas model for Arbitrum. A reference point for calculating equivalent gas costs for Arbitrum is the following: https://docs.arbitrum.io/devs-how-tos/how-to-estimate-gas.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T20:04:03Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T20:04:24Z

raymondfam marked the issue as duplicate of #212

#2 - raymondfam

2024-01-25T20:05:49Z

Same root cause as in #212 but this isn't pertaining to layerzero.

#3 - c4-judge

2024-02-02T12:32:51Z

alex-ppg marked the issue as partial-50

#4 - windhustler

2024-02-06T15:24:03Z

Hi @alex-ppg,

https://github.com/code-423n4/2024-01-decent-findings/issues/697 was extracted as a separate issue based on the assumption that you can drain all the gas inside the OFTCoreV2.callOnOFTReceived().

I have left a comment describing why this is not the case: https://github.com/code-423n4/2024-01-decent-findings/issues/697#issuecomment-1929936744.

This report details a separate root cause which is a different gas model on Arbitrum.

https://github.com/code-423n4/2024-01-decent-findings/issues/670 highlights the issue with storing variable size payloads inside the failedMessages.

As https://github.com/code-423n4/2024-01-decent-findings/issues/668 and https://github.com/code-423n4/2024-01-decent-findings/issues/670 are two different issues I believe they should be treated separately.

Thanks!

#5 - alex-ppg

2024-02-09T09:58:56Z

Hey @windhustler, thanks for noting your concerns! This vulnerability concerns the same root cause (fixed gas cost of variable) and rationalizes it from a different perspective while its mitigation would not fix the underlying vulnerability (i.e. it only advises that the gas cost should change for the Arbitrum chain).

Adjusting the default gas cost will resolve all duplicated vulnerabilities (except those for varying payloads in #697) and as such, this submission falls under the same issue based on the relevant judge guideline to group submissions under the same root cause. A penalty was applied because the vulnerability was solely rationalized for the Arbitrum chain and not for all other chains for which it is an issue for.

If I judged this vulnerability independently, we would have to create a separate issue per unique gas model EVM supported by the LayerZero network which is counter-intuitive when all of them would concern the same problem; fixed gas cost of the GAS_FOR_RELAY variable.

Findings Information

🌟 Selected for report: iamandreiski

Also found by: EV_om, NPCsCorp, nuthan2x, windhustler

Labels

bug
3 (High Risk)
high quality report
satisfactory
duplicate-525

Awards

1742.7673 USDC - $1,742.77

External Links

Lines of code

https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L185-L193

Vulnerability details

Impact

Due to variable cost of saving failed messages inside the failedMessages storage variable, it is possible to block the LayerZero pathway by sending a big payload with the default amount of gas.

The impact is that the LayerZero pathway gets blocked and no other messages can be sent through it.

Proof of Concept

Let's examine the DecentEthRouter:bridgeWithPayload() function:

  1. It uses the sendAndCall function to send the payload to the receiving chain.
  2. The adapterParams and the corresponding gasLimit to deliver the message are set to 100_000 plus any additional gas for the external call inside the onOFTReceived function.
  3. If the onOFTReceived function fails, the messages is being saved into the failedMessages storage variable.
  4. A malicious user can force the onOFTReceived function to fail, just to provoke saving the message. I'll leave the test to showcase the cost of saving the message in the failedMessages storage variable and emitting an event.
  5. As the cost for bigger payloads is around 131k, and all the extra gas is used in the onOFTReceived function, the uint256 GAS_FOR_RELAY = 100000; is not enough to execute all the logic.
  6. If the transaction fails inside the _sendAndCallAck function it will result in storedPayload inside the Endpoint.sol and block the LayerZero pathway.

Gas costs of storing big paylaod inside the failedMessages storage variable and emitting an event:

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "forge-std/console.sol";

contract FailedMessagesTest is Test {

    mapping(uint16 => mapping(bytes => mapping(uint64 => bytes32))) public failedMessages;

    event MessageFailed(uint16 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, bytes _reason);

    function setUp() public {}

    function testFMessagesGas() public {
        uint16 srcChainid = 1;
        bytes memory srcAddress = abi.encode(makeAddr("Alice"));
        uint64 nonce = 10;
        bytes memory payload = getDummyPayload(9999); // max payload size someone can send is 9999 bytes
        bytes memory reason = getDummyPayload(2);

        uint256 gasLeft = gasleft();
        _storeFailedMessage(srcChainid, srcAddress, nonce, payload, reason);
        emit log_named_uint("gas used", gasLeft - gasleft());
    }


    function _storeFailedMessage(
        uint16 _srcChainId,
        bytes memory _srcAddress,
        uint64 _nonce,
        bytes memory _payload,
        bytes memory _reason
    ) internal virtual {
        failedMessages[_srcChainId][_srcAddress][_nonce] = keccak256(_payload);
        emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, _reason);
    }

    function getDummyPayload(uint256 payloadSize) internal pure returns (bytes memory) {
        bytes memory payload = new bytes(payloadSize);
        for (uint256 i = 0; i < payloadSize; i++) {
            payload[i] = bytes1(uint8(65 + i));
        }
        return payload;
    }
}

Tools Used

  • Manual review

Estimate the maximum gas cost for big payloads and set the GAS_FOR_RELAY considering that value. The exact fix for this issue will also depend on how you decide to fix issues I mentioned in my other reports.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T20:08:05Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T20:08:23Z

raymondfam marked the issue as duplicate of #212

#2 - c4-pre-sort

2024-01-26T04:11:09Z

raymondfam marked the issue as high quality report

#3 - c4-judge

2024-02-02T12:32:39Z

alex-ppg marked the issue as satisfactory

Findings Information

🌟 Selected for report: windhustler

Also found by: imare, monrel, nuthan2x

Labels

bug
2 (Med Risk)
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
sufficient quality report
M-01

Awards

943.999 USDC - $944.00

External Links

Lines of code

https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/bridge_adapters/StargateBridgeAdapter.sol#L183

Vulnerability details

Impact

While sending funds through StargateBridgeAdapter, the user passes the swap data as a parameter. The flow is stargate sending tokens on the receiving chain into the StargateBridgeAdapter and executing sgReceive.

The issue here is that sgReceive will fail if the swap data gets outdated, but this is not going to make the whole transaction revert.

Stargate will still send the tokens to the StargateBridgeAdapter, and if the sgReceive fails, the swap will be cached for later execution. See StargateComposer logic here: https://stargateprotocol.gitbook.io/stargate/stargate-composability/stargatecomposer.sol#sgreceive.

Now, tokens will be left sitting in the StargateBridgeAdapter contract, and since the user can only retry the transaction with the same swap data, the tokens will be stuck forever.

The impact is loss of transferred tokens for the user.

Proof of Concept

Consider the following flow:

  1. User invokes StargateBridgeAdapter:bridge() function as part of the whole flow of calling UTB:bridgeAndExecute() function.
  2. He is passing the swapData parameters to the remote chain.
  3. StargateComposer first transfers the tokens to the StargateBridgeAdapter contract.
  4. After that the StargateBridgeAdapter:sgReceive() function is executed.
  5. It then calls UTB:receiveFromBridge that tries to execute the swap.
  6. If the swap fails, which can frequently occur due to the fact that the swap data is outdated, most commonly minimum amount out gets outdated.
  7. The whole payload gets stored inside the StargateComposer contract, and the tokens are left in the StargateBridgeAdapter contract.
  8. As the user can only retry the transaction with the same payload, the tokens will be stuck forever.
  9. An example of StargateComposer contract can be seen on: https://stargateprotocol.gitbook.io/stargate/developers/contract-addresses/mainnet#ethereum

Note: The application needs to use the StargateComposer contract as opposed to StargateRouter because it is transferring funds with payload, i.e. it executes sgReceive on the receiving chain. You can only use StargateRouter directly if you're sending funds empty payload. More on it: https://stargateprotocol.gitbook.io/stargate/stargate-composability

Tools Used

  • Manual review

Wrap the whole StargateBridgeAdapter:receiveFromBridge() call into a try/catch and if it reverts send the transferred tokens back to the user.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T20:00:01Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T20:00:05Z

raymondfam marked the issue as primary issue

#2 - raymondfam

2024-01-25T20:01:01Z

Would indeed be a serious issue if the vulnerability is valid.

#3 - c4-sponsor

2024-01-31T16:01:43Z

wkantaros (sponsor) disputed

#4 - c4-sponsor

2024-01-31T16:01:46Z

wkantaros (sponsor) confirmed

#5 - alex-ppg

2024-02-01T11:58:08Z

The Warden has demonstrated that it is possible for a Stargate-based cross-chain interaction to fail at the swap level perpetually.

While the StargateComposer::clearCachedSwap function exists to retry the same payload, as the Warden states, a sharp market event (i.e. token launch that leads to an upward trend or market crash that leads to a downward event) can cause the swap to fail.

Theoretically, the tokens can be rescued using a flash-loan if they are substantial, however, this would be very unconventional and not a real mitigation to the issue. The proposed solution by the Warden is adequate.

I believe a severity of Medium is more appropriate as the vulnerability relies on external requirements (i.e. a sharp market event), the maximum impact is the user's own funds, and when users specify slippage (i.e. minimum output) they usually factor in the time it takes for a transaction to execute. Network congestion, market events, and cross-chain relay delays all play a role in whether the vulnerability manifests but present external requirements.

#6 - c4-judge

2024-02-01T11:58:15Z

alex-ppg changed the severity to 2 (Med Risk)

#7 - c4-judge

2024-02-01T11:58:20Z

alex-ppg marked the issue as selected for report

#8 - c4-judge

2024-02-01T11:58:24Z

alex-ppg marked the issue as satisfactory

#9 - windhustler

2024-02-06T10:13:01Z

Hi @alex-ppg, thanks for the swift judging process.

This vulnerability does depend on external requirements but those external requirements are always present in a cross-chain system. In practice, this means if this wasn't identified and fixed every once in a while the user using this system would result in loss of funds.

It's up to the users to specify slippage but why would someone use a system where if you specify a low slippage threshold it results in total loss of funds? And if you specify a high slippage threshold you don't lose all the funds but you end up sandwiched and getting a bad trade.

To emphasize the impact of this issue:

  • Affects all sent messages since you would need to set a high slippage threshold.
  • The unlucky users who set the slippage too low or there is a significant relayer delay will lose all their funds.

I believe this is a clear loss of funds in almost all cases so it deserves a high severity.

#10 - alex-ppg

2024-02-09T09:51:03Z

Hey @windhustler, thank you for providing your feedback. The ruling of this submission will remain the same given that I explain even in a dire scenario the funds can be rescued. The submission implies that the market event was so sharp that it led to an unfulfillable trade on the target chain. This is a very strong external requirement; normal market conditions and correct user input will never permit this issue to manifest.

Based on the above, the submission cannot constitute a high-severity vulnerability if coupled with the fact that only the user's own funds are affected. Thank you for voicing your thoughts.

#11 - windhustler

2024-02-09T11:18:36Z

Hey @alex-ppg,

I am not sure how these funds can be saved with a flashloan. Could you point to how that is done?

Could you please reconsider the external requirements. Reverting due to low slippage is not uncommon given that slippage has to be set tightly to protect against sandwich attack. It is common to see 1-3% moves in a matter of seconds in the crypto market. Given that this is a bridging protocol that supports arbitrary tokens and that it is the sponsors ambition to support as much traffic as possible I don't think it is unreasonable to see frequent reverted swaps given enough volume. Looking at etherscan, the latest reverted call due to low slippage was 10 minutes ago. We can see that this usually happens multiple times every hour.

Consider this: Would it not be a very serious issue in Uniswap if users risked blocking their funds for an unknown amount of time (potentially forever) each time uniswap reverts a trade due to low slippage?

I believe the issue here is more severe than the hypothetical above due to the delays in cross-chain swaps.

I also do not think it can be seen as a user error to set a low slippage, given that it is a safety parameter that reflects a user's worst-case acceptable exchange rate.

#12 - alex-ppg

2024-02-09T11:24:05Z

Hey @windhustler, I will kindly remind you that the PJQA period has concluded and no further input has been requested.

A flash loan can be used to skew the exchange pair's price temporarily, execute the failed but stored cross-chain transaction, and return the pair's normal state. Even if we do not consider this rescue mechanism being present, the user's funds are solely affected if they set very strict slippage requirements. I am not saying this is not a flaw, I am saying that this is not a high-risk and is simply a medium-risk vulnerability.

I will retain my judgment and no further input is requested. Feel free to voice your concerns in different avenues such as C4 organization issues, but this particular submission's judgment can be considered final.

Findings Information

🌟 Selected for report: Soliditors

Also found by: NentoR, Soliditors, gesha17, peanuts, wangxx2026, windhustler

Labels

bug
2 (Med Risk)
downgraded by judge
insufficient quality report
partial-50
duplicate-520

Awards

392.1226 USDC - $392.12

External Links

Lines of code

https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/UTB.sol#L186

Vulnerability details

Impact

The UTB:bridgeAndExecute() assumes that stargate transfers the amount of tokens specified in the minAmountOut parameter of the swap function. In reality, it can transfer more than that and this difference is lost for the user.

As this is a direct loss of funds for the user, I rate this as a high severity issue.

Proof of Concept

The flow while using the UTB:bridgeAndExecute() function is as follows:

  1. First a swap is executed, i.e. UTB:performSwap function.
  2. After that the amountIn is set for the newPostSwapParams
  3. If the adapter is StargatBridgeAdapter, this amount is calculated using the actual amount received after the swap and applying the fee deduction.
  4. The fee is hardcoded inside the StargatBridgeAdapter and is set to 6 BPS.
  5. The issue here that stargate swap function works so that you can pass the minimum amount out but the actual tokens transferred can be higher than that.
  6. The minimum amount is just there to protect the user from getting less than the minimum amount.
  7. This can be observed by looking at the Stargate Pool contract code: https://github.com/stargate-protocol/stargate/blob/main/contracts/Pool.sol#L187

Let's take an example of 5 tokens received after the swap.

  1. After applying the fee deduction we get 4.997 tokens which is the minimum amount the user expects to receive.
  2. But in reality the user receives 4.999 tokens on the destination chain.
  3. The difference is lost for the user.

As only the set amountIn in the newPostSwapParams is approved and sent to utb, the leftover tokens are left sitting in the StargateBridgeAdapter contract.

Tools Used

  • Manual review

When the sgReceive is invoked on the receiving chain check the actual amount of received tokens and used that for swapping. Don't leave any tokens hanging in the StargateBridgeAdapter contract.

Assessed type

Token-Transfer

#0 - c4-pre-sort

2024-01-25T19:44:08Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-01-25T19:44:18Z

raymondfam marked the issue as duplicate of #245

#2 - c4-judge

2024-02-02T11:31:51Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2024-02-02T11:32:02Z

alex-ppg marked the issue as duplicate of #303

#4 - c4-judge

2024-02-02T11:35:45Z

alex-ppg marked the issue as partial-50

#5 - c4-judge

2024-02-02T14:42:39Z

alex-ppg changed the severity to 2 (Med Risk)

#6 - windhustler

2024-02-06T14:21:44Z

Hi @alex-ppg, could you elaborate on why is this submission marked with partial-50?

It correctly highlights the issue and there is a reference to Stargate Pool contract where it is visible that the minAmountOut is just the minimum amount checked. It's not the actual amount transferred. The actual amount transferred can be greater or equal to the minimum amount.

Thanks!

#7 - alex-ppg

2024-02-09T09:53:45Z

Hey @windhustler, thanks for flagging this! The actual vulnerability is not the loss of minuscule dust units of funds. The actual vulnerability is described in the original primary exhibit #303 and specifically:

The Denial-of-Service attack vector was not correctly considered in the submission, and it only identified one of the two vulnerabilities (a decrease in the fee) which is also unlikely to occur based on the referenced Stargate documentation.

Findings Information

🌟 Selected for report: Soliditors

Also found by: NentoR, Soliditors, gesha17, peanuts, wangxx2026, windhustler

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-520

Awards

392.1226 USDC - $392.12

External Links

Lines of code

https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/bridge_adapters/StargateBridgeAdapter.sol#L23

Vulnerability details

Impact

Hardcoding the stargate fee to calculate the minimum amount out is problematic due to 2 reasons:

  • Stargate can update the fee library at any time which will break the contract
  • Even if the fee library is not updated, the fee can be higher than the hardcoded value and the transaction will revert

As this is the main functionality of the system and hardcoding fee can cause the system to break, but there is no risk of losing funds, I would classify this as a medium risk.

Proof of Concept

Stargate's swap interface lets the caller specify the minimum amount of tokens he wants to receive on another chain: https://github.com/stargate-protocol/stargate/blob/main/contracts/Router.sol#L107-L133. Each pool has a variable fee calculated by the fee library: https://github.com/stargate-protocol/stargate/blob/main/contracts/Pool.sol#L164-L212 The fee library can be freely set at any time: https://github.com/stargate-protocol/stargate/blob/main/contracts/Pool.sol#L413. The StargateBridgeAdapter might work with the hardcoded fee value for the exisiting fee library, but it will break if the fee library is updated to have different logic.

As a consequence hardcoding minAmountOut doesn't work as the fee can be higher than the hardcoded value and the transaction will revert. if we look at the currently deployed FeeLibrary it is version 7: https://stargateprotocol.gitbook.io/stargate/developers/contract-addresses/mainnet

Tools Used

  • Manual review

Allow the user to specify the minimum amount out and don't hardcode it.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T20:09:50Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T20:10:14Z

raymondfam marked the issue as duplicate of #62

#2 - c4-judge

2024-02-02T14:53:20Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2024-02-02T14:53:27Z

alex-ppg marked the issue as duplicate of #520

#4 - c4-judge

2024-02-02T14:53:32Z

alex-ppg marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter