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
Rank: 1/113
Findings: 3
Award: $3,078.89
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: iamandreiski
Also found by: EV_om, NPCsCorp, nuthan2x, windhustler
1742.7673 USDC - $1,742.77
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.
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:
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.
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.
🌟 Selected for report: iamandreiski
Also found by: EV_om, NPCsCorp, nuthan2x, windhustler
1742.7673 USDC - $1,742.77
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.
Let's examine the DecentEthRouter:bridgeWithPayload()
function:
sendAndCall
function to send the payload to the receiving chain.onOFTReceived
function.onOFTReceived
function fails, the messages is being saved into the failedMessages
storage variable.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.onOFTReceived
function, the uint256 GAS_FOR_RELAY = 100000;
is not enough to execute all the logic._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; } }
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.
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
🌟 Selected for report: windhustler
943.999 USDC - $944.00
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.
Consider the following flow:
StargateBridgeAdapter:bridge()
function as part of the whole flow of calling UTB:bridgeAndExecute()
function.StargateComposer
first transfers the tokens to the StargateBridgeAdapter
contract.StargateBridgeAdapter:sgReceive()
function is executed.UTB:receiveFromBridge
that tries to execute the swap.StargateComposer
contract, and the tokens are left in the StargateBridgeAdapter
contract.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
Wrap the whole StargateBridgeAdapter:receiveFromBridge()
call into a try/catch and if it reverts send the transferred tokens back to the user.
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:
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.
🌟 Selected for report: Soliditors
Also found by: NentoR, Soliditors, gesha17, peanuts, wangxx2026, windhustler
392.1226 USDC - $392.12
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.
The flow while using the UTB:bridgeAndExecute()
function is as follows:
UTB:performSwap
function.StargatBridgeAdapter
, this amount is calculated using the actual amount received after the swap and applying the fee deduction.StargatBridgeAdapter
and is set to 6 BPS.Let's take an example of 5 tokens received after the swap.
As only the set amountIn in the newPostSwapParams
is approved and sent to utb, the leftover tokens are left sitting in the StargateBridgeAdapter
contract.
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.
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.
🌟 Selected for report: Soliditors
Also found by: NentoR, Soliditors, gesha17, peanuts, wangxx2026, windhustler
392.1226 USDC - $392.12
Hardcoding the stargate fee to calculate the minimum amount out is problematic due to 2 reasons:
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.
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
Allow the user to specify the minimum amount out and don't hardcode it.
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