Decent - EV_om'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: 2/113

Findings: 4

Award: $2,615.63

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DcntEth.sol#L20-L30

Vulnerability details

Impact

The DcntEth contract is designed to interface with the DecentEthRouter contract, which is intended to be the sole entity capable of calling the functions mint() and burn(). These functions are critical as they directly affect the token supply of DcntEth. The setRouter(address _router) function is meant to establish the router's address, which is then enforced by the onlyRouter modifier on sensitive functions.

However, the current implementation of setRouter() lacks any form of access control, allowing any user to set themselves as the router. This poses a severe security risk as an unauthorized user could call setRouter() to set their address as the router and subsequently invoke mint() to arbitrarily increase the token supply in their favor, leading to potential token inflation and devaluation.

Proof of Concept

  1. An attacker deploys their own contract or uses an EOA.
  2. The attacker calls setRouter() with their address.
  3. The attacker is now set as the router and can call mint() to mint an arbitrary amount of DcntEth to any address they choose.

Tools Used

Manual review

To mitigate this issue, implement access control in the setRouter() function. This can be achieved by adding the onlyOwner modifier.

The code fix would be as follows:

function setRouter(address _router) public onlyOwner {
    router = _router;
}

This change ensures that only the owner of the contract can set or change the router, preventing unauthorized manipulation of the token supply.

Assessed type

Access Control

#0 - c4-pre-sort

2024-01-25T21:19:41Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:19:48Z

raymondfam marked the issue as duplicate of #14

#2 - c4-judge

2024-02-03T13:08:13Z

alex-ppg marked the issue as satisfactory

Findings Information

🌟 Selected for report: iamandreiski

Also found by: EV_om, NPCsCorp, nuthan2x, windhustler

Labels

bug
3 (High Risk)
satisfactory
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#L197 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L103-L109 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L161

Vulnerability details

Impact

This issue is internally the same as described in https://github.com/code-423n4/2023-07-tapioca-findings/issues/1220. To avoid duplication of efforts and unnecessary copy-pasting, only the differences are explained in this issue.

Proof of Concept

The entry point here is the bridgeWithPayload() function in the DecentEthRouter contract, which allows the caller to pass a variable-length additionalPayload variable. This variable is then encoded as part of the payload variable returned by _getCallParams() and passed as parameter to DcntEth.sendAndCall(). This is a function that DcntEth inherits from OFTV2, which inherits it from BaseOFTV2 . This function calls the internal _sendAndCall() function inherited from OFTCoreV2, which finally calls _lzSend(). This ties into the description of the vulnerability in the aforementioned issue.

On the other end, the message will be processed in the onOFTReceived() function of the same contract, which is invoked in the OFTCoreV2.callOnOFTReceived() function when receiving a message. Our payload will be extracted as the callPayload variable and passed on to the executor, where the msgType is hardcoded as MT_ETH_TRANSFER_WITH_PAYLOAD in bridgeWithPayload(). Finally, in DecentBridgeExecutor, we always get a call to our target address with the payload we defined (of which only its size matters), in which we can drain as much gas as needed for the attack. This completes the exploit as all intermediary steps are explained in great detail in the issue referenced above.

Tools Used

Manual review

The recommended mitigation is similar to the one in the reference issue. Alternatively or in addition to it, a maximum size for any variable-length parameters that can be passed in a LayerZero message to an arbitrary address could be implemented.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T21:08:20Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:08:56Z

raymondfam marked the issue as duplicate of #693

#2 - c4-judge

2024-02-01T11:35:04Z

alex-ppg marked the issue as selected for report

#3 - alex-ppg

2024-02-01T11:37:40Z

The Warden has demonstrated that even though the V2 OFT token implements a non-blocking LayerZero application, the insecure DecentEthRouter::onOFTReceived function will perform an arbitrary call that can consume a significant portion of gas allocated to the call, preventing the failed message from being stored and thus blocking the LayerZero channel.

Such an action is irreversible, and I consider a severity of High to be appropriate.

As feedback for the Warden, this submission would not have been selected the best if any of its duplicates properly explained the issue. I strongly advise the Warden to not cite other findings to avoid "effort" as this finding will be part of a publicly available C4 report that should properly describe the issue without having the readers go through other resources.

#4 - c4-judge

2024-02-01T11:37:45Z

alex-ppg marked the issue as satisfactory

#5 - NicolaMirchev

2024-02-05T21:01:37Z

Hey, @alex-ppg

I believe the following issue is not valid, because LZ implement a check for the payload size inisde lzSend function: This is the code that they are using: https://github.com/LayerZero-Labs/solidity-examples/blob/2d68d812260d1f96d0a7a46abd6ade4e8962aae1/contracts/lzApp/LzApp.sol#L70-L73

https://github.com/LayerZero-Labs/solidity-examples/blob/2d68d812260d1f96d0a7a46abd6ade4e8962aae1/contracts/lzApp/LzApp.sol#L95-L102

function _lzSend( uint16 _dstChainId, bytes memory _payload, address payable _refundAddress, address _zroPaymentAddress, bytes memory _adapterParams, uint _nativeFee ) internal virtual { bytes memory trustedRemote = trustedRemoteLookup[_dstChainId]; require(trustedRemote.length != 0, "LzApp: destination chain is not a trusted source"); _checkPayloadSize(_dstChainId, _payload.length); lzEndpoint.send{value: _nativeFee}(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams); }
function _checkPayloadSize(uint16 _dstChainId, uint _payloadSize) internal view virtual { uint payloadSizeLimit = payloadSizeLimitLookup[_dstChainId]; if (payloadSizeLimit == 0) { // use default if not set payloadSizeLimit = DEFAULT_PAYLOAD_SIZE_LIMIT; } require(_payloadSize <= payloadSizeLimit, "LzApp: payload size is too large"); }

#6 - stalinMacias

2024-02-06T01:24:03Z

Hey @alex-ppg , what @NicolaMirchev has just mentioned is true, the LZ contracts have an internal mechanism to prevent exactly what is been reported here. Additionally, if we take a look at the report that this report is referencing, there is a unique precondition for the "issue" to even occur, such a precondition (I'm quoting from the referenced report) is: "if the project decides to change the default payload size to something smaller(or bigger) it will be dictated by the message with the biggest possible payload size."

  • The unique precondition for the "issue" to materialize is that the project/admins change the default parameters of the LZ contracts.
  • This falls into the category of admin privileges/misconfigurations, which are considered to be qa/informational.

#7 - DadeKuma

2024-02-06T02:42:41Z

I agree with @NicolaMirchev and @stalinMacias, this issue is invalid.

The issue cited: https://github.com/code-423n4/2023-07-tapioca-findings/issues/1220 is valid only because the attacker can pass some arbitrary length data in the form of an ICommonData.IApproval[] calldata approvals array to get near the max gas limit; then the payload decoding will not have enough gas if it's large enough, and under the payloadSizeLimit limit.

But in this case, the attacker cannot pass arbitrary length data, this function doesn't accept arrays: https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L197

If the attacker tries to send a large payload, the TX will fail here without any consequences:

require(_payloadSize <= payloadSizeLimit, "LzApp: payload size is too large");

EDIT: Nevermind, I misread the issue, additionalPayload is part of the payload, it has a variable length, and there are no checks about its validity or length, so it can be anything.

#8 - 0xEVom

2024-02-06T06:59:34Z

Hi @alex-ppg, many thanks for the detailed response on every submission and the feedback on this one, that is incredibly helpful. I will keep that in mind for future submissions.

@NicolaMirchev the referenced finding does take into account that the maximum payload size is 10k bytes, which is the DEFAULT_PAYLOAD_SIZE_LIMIT.

@stalinMacias the excerpt you're mentioning is not a precondition, but simply mentioned as an aside in the referenced issue to inform the finding's remediation. The full quote is as follows:

The application architecture was set up in a way that all the messages regardless of their packetType go through the same _lzSend implementation. I’m mentioning that because it means that if the project decides to change the default payload size to something smaller(or bigger) it will be dictated by the message with the biggest possible payload size.

@DadeKuma the bytes type is a dynamically-sized byte array.

#9 - stalinMacias

2024-02-06T10:32:42Z

@0xEVom For one second I almost believed that your report was valid, but guess what, it is totally invalid, here is why.

So, you are saying that the DecentEthRouter::onOFTReceived() will call a malicious contract that will drain all the gas that is used for the execution, and as a result of that, the payload that was sent would get stored in the failedMessages by using the _storeFailedMessage() which is called by the _blockingLzReceive() from the lzReceive().

Or in other words, you are saying that the DcntEth.lzReceive() is able to store payloads when the executions fail.

So, you are saying that when the DecentEthRouter::onOFTReceived() forwards the execution to the DecentBridgeExecutor to call your malicious contract that will force the tx to fail and as a result of that, the huge payload will be stored. But there is something you missed, executions on the BrdigeExecutors don't revert _executeWeth() & _executeEth(). Any of those tx can't revert, if the external call (the one to your contract) fails, the DecentBridgeExecutor will attempt to refund the funds. And, surprise surprise, if your malicious contract consumes all the gas that is used for the execution, in the DecentBridgeExecutor the whole tx will blow up as an OOG exception, thus, nothing will be stored on the failedMessages, the channel won't be blocked because the state won't be updated, it will be as if the transaction would've never been executed.

You missed an important thing in the report you mentioned, in those contracts, if the external call to the final target contract fails, they indeed explicitly cause a revert, but in this protocol, if the external call fails, they refund the funds back and they never revert the tx.

  • This is how it looks the execution of the other report, which of course, is totally different from the execution flow of this report
_executeOnDestination() => _executeModule() => module.delegatecall(_data); ==== https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/BaseTOFT.sol#L417-L439 - If the delegatecall to the module fails, the _executeModule is reverted ==== https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/BaseTOFT.sol#L403-L415 - If the _executeModule() is reverted, the payload is stored using the _storeFailedMessage() ==== https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/BaseTOFT.sol#L430-L438

@alex-ppg Please read carefully this comment to understand why this report is totally invalid. The report of the other contest is valid in the context of the other contest, but in the execution flow of this contest is impossible to deliberately force the same issue to occur on these contracts.

#10 - windhustler

2024-02-06T14:49:17Z

My report was referenced here so I believe I can provide some additional context.

  • Inside the OFTCoreV2.callOnOFTReceived() you can ONLY drain gas that was provided additionally by the user as _dstGasForCall parameter and paid on the sending side.
  • There is no return bomb here as callOnOFTReceived only forwards this additional gas and it is wrapped inside ExcessivelySafeCall library.

So there is no return bomb and the onOFTReceived doesn't drain any base gas.

The Warden has demonstrated that even though the V2 OFT token implements a non-blocking LayerZero application, the insecure DecentEthRouter::onOFTReceived function will perform an arbitrary call that can consume a significant portion of gas allocated to the call, preventing the failed message from being stored and thus blocking the LayerZero channel.

By draining this gas the attacker would only drain what he has additionally paid on the sending side. The whole point of the OFT::sendAndCall interface is to allow arbitrary calls while having the rest of the call succeed regardless.

So the basic assumptions of this report are not accurate.

The only real issue is the size of the payload, which in cases when it's too big causes a huge overhead when it's being saved inside the failedMessages. As the base gas is hardcoded to 100k, this is an issue.

I have explained these dynamics in the: https://github.com/code-423n4/2024-01-decent-findings/issues/670.

#11 - alex-ppg

2024-02-06T17:24:51Z

Thanks to everyone for contributing to the PJQA process! I will remind you that all Wardens should adhere to civilized conduct and that snarky remarks are unwelcome.

We have already established that the gas limit allocation is very small to properly execute across-chain transactions and that may lead to a failure in the cross-chain message relay.

This is a valid concern and can be rectified by updating how gas is configured and so on.

This particular submission relates to how even with a configurable payload it may be possible to still hijack the transaction, causing blockage of the LayerZero channel.

@NicolaMirchev the default payload size is indeed accounted for in the submission as it is quite high at 10_000 bytes.

@DadeKuma as you have correctly re-evaluated an arbitrary payload can indeed be passed in the cross-chain transaction.

@stalinMacias your initial reply treats the adjustment of the default payload size as a precondition but it is not. The original submission clearly uses the 10_000 default limitation and as such no privileged action needs to occur.

Your second reply seems to misunderstand what would occur in this scenario. If the called contract would result in an OOG error, the bridge executors would have to perform refunds with the remaining 1/64 gas which is not going to be sufficient. This will result in the call also running into an OOG error until it is "bubbled up" to the instance the Warden has submitted. To note, the caller of the contract can also be forced to fail via return-bombing which is applicable for the bridge executors.

What we are concerned with is whether the final 1/64 remaining at that point in time is sufficient for storing the failed payload. As the Tapioca submission describes, the code can fail even with the default payload size limitation (it uses a simulated gas limit of 1_000_000, far greater than the LayerZero recommendation, and a payload size of 10_000, which is the default limitation).

I will re-evaluate the issue as it may potentially be a superset of the out-of-gas error described in #525, but will retain it as separate for now.

#12 - stalinMacias

2024-02-06T18:40:20Z

@alex-ppg

First of all, apologies for the snarky remarks, will pay more attention to the wording on my comments.

So, if we track the execution flow on the lzContracts, we have:

LzApp.lzReceive() => NonBlockingLzApp._blockingLzReceive() => NonBlockingLzApp.nonblockingLzReceive() => NonBlockingLzApp._nonblockingLzReceive() => OFTCoreV2._sendAndCallAck() => OFTCoreV2.callOnOFTReceived() => DcntEthRouter.onOFTReceived() => DecentBridgeExecutor.execute() => targetContract()

When retying a failed payload:

NonBlockingLzApp.retryMessage() => NonBlockingLzApp._nonblockingLzReceive() => OFTCoreV2._sendAndCallAck() => OFTCoreV2.callOnOFTReceived() => DcntEthRouter.onOFTReceived() => DecentBridgeExecutor.execute() => targetContract()

By taking a deeper look at the lz contracts involved in the DcntEth contract, I realized that when an execution to retry a payload is sent, it is possible to send more gas for this execution, see here.

  • Let's suppose that the gas that is forwarded to the DcntEthRouter.onOFTReceived() is 100k gas, that means, the gas that will be sent to the maliciousContract will be (98,437.5), leaving (1,562.5) gas on the DecentBridgeExecutor._executeWeth() or DecentBridgeExecutor._executeEth() functions, then, when the execution comes back from the maliciousContract to the DecentBridgeExecutor, the contract will have ~1,562.5 gas to attempt the refunds, this may fail and cause the tx to bubble up again till the LZ contracts. Aight, but, thanks to the logic of the OFTCoreV2._sendAndCallAck() function, it is possible to increase the amount of gas that is forwarded to the DcntEthRouter, thus, the amount of gas that will end up being forwarded to the DecentBridgeExecutor.
    • This allows to unblock the message by sending more gas till the point that the remaining gas in the DecentBridgeExecutor will be enough to handle the refunds and don't revert.
      • i.e., If DcntEthRouter.onOFTReceived() receives 1m gas, the maliciousContract will be able to consume up to ~984,375 of gas, leaving in the DecentBridgeExecutor ~15,625 gas to attempt the refund.

Because of the logic of the contracts in this protocol and the fail-safe mechanism to refund funds in the DecentBridgeExecutor if the external call fails rather than reverting the whole execution, the layer zero messaging can't be blocked permanently (With that said and after I've reviewed #525, I believe this report should be a dupe of #525 instead of being assessed as a unique finding

#13 - 0xEVom

2024-02-07T14:35:08Z

@alex-ppg really appreciate how much thought and consideration you're putting into judging.

I reached out to @windhustler yesterday and while I would gladly take the solo high, I've come around to agreeing with him that the situation here is not quite the same as in the Tapioca case and this finding is pretty much just a needlessly complicated duplicate of #525.

The main difference is that in the referenced finding, the gas was available for the entire call trace and causing a revert required the target contract to consume so much gas that the storing of the payload hit an OOG with the remaining 1/64 of the gas.

However, in this case, DcntEth will only pass as much gas to onOFTReceived() as the user specified as _dstGasForCall in DecentEthRouter.bridgeWithPayload(). This amount is passed as argument to sendAndCall() and encoded in the lzPayload passed to _lzSend(), which is decoded again as gasForCall in _sendAndCallAck(). This is the gas that is forwarded to the onOFTReceived() function and not any more.

At the same time, the endpoint passes a different amount of gas to lzReceive(), which DcntEth inherits from LzApp and ends calling _sendAndCallAck() via blockingLzReceive() -> nonblockingLzReceive() -> _nonblockingLzReceive(). This amount of gas is presumably extracted by the off-chain relayer from the adapterParams, in which it is encoded as GAS_FOR_RELAY + _dstGasForCall.

So we have:

  • _dstGasForCall gas available for all logic within onOFTReceived()
  • ≥ 100k gas (GAS_FOR_RELAY) available for all logic within lzReceive() and outside onOFTReceived(), including storing the message without blocking the channel in _sendAndCallAck()
  • presumably enough additional gas provided by the relayer to handle all the LayerZero logic outside lzReceive(), including storing the failed message blocking the channel in receivePayload()

Since _dstGasForCall is user-controlled, it is trivial to cause onOFTReceived() to fail and there is no need to consume as much gas as to cause an OOG exception—we can just pass 0 or a small amount. The real issue is the fact that GAS_FOR_RELAY is not large enough to handle the storing of arbitrarily large payloads of failed messages. If it was appropriately sized, causing onOFTReceived() to revert due to OOG in any way would still not consume the base GAS_FOR_RELAY and only cause the message to be stored without blocking the channel.

#14 - alex-ppg

2024-02-09T09:45:12Z

Hey @stalinMacias and @0xEVom, thank you for providing additional information to this submission.

@stalinMacias I believe you misunderstand how LayerZero works and how an uncaught revert on the OFT would be handled. Specifically, a cross-chain transaction cannot be retried on-chain if it fails in an uncaught error; it can only be retried if it was properly stored in the failed payloads. Otherwise, it is repeated as a normal transaction until it executes (or is gracefully handled) and the channel is blocked until that occurs.

@0xEVom I appreciate the re-evaluation of your own submission, but you have to keep in mind that point 2 is relatively indeterminate. The gas cost is theoretically unbound for the input argument (only limited by the type(uint256).max limitation as well as block-level gas limits). This means that even if GAS_FOR_RELAY is set to the block's gas limit, we could still incur an out-of-gas exception by utilizing an arbitrarily long payload due to the 63/64 rule.

Based on the above, the submission is somewhat related but not directly linked to what value the GAS_FOR_RELAY has. The variable _dstGasForCall is unrelated as well given that it is user-controlled and can be considered 0 as a user who wishes to block the cross-chain relay channel would act maliciously.

I would normally keep the submissions separate based on the above, however, given that the wardens all agree to their merging (including the main submission's original author), I will de-dupe this submission under the gas-related LayerZero flaw. This discussion is considered final as PJQA has concluded the contest; thanks to everyone who contributed their knowledge to this ruling!

#15 - c4-judge

2024-02-09T09:47:03Z

alex-ppg marked the issue as duplicate of #525

#16 - c4-judge

2024-02-09T09:47:08Z

alex-ppg marked the issue as not selected for report

Findings Information

Awards

78.6887 USDC - $78.69

Labels

bug
3 (High Risk)
partial-75
sufficient quality report
duplicate-436

External Links

Lines of code

https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L36 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L63

Vulnerability details

Impact

The DecentBridgeExecutor contract has two private functions, _executeWeth() and _executeEth(), which are used to execute transactions with a custom payload received through the Decent bridge.

In both functions, if the call to the target contract fails, the contract sends the funds back to the from address. This behavior is problematic because the from address is hardcoded as msg.sender in the _getCallParams() function on the sender side: https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L100-L109

	if (msgType == MT_ETH_TRANSFER) {
		payload = abi.encode(msgType, msg.sender, _toAddress, deliverEth);
	} else {
		payload = abi.encode(
			msgType,
			msg.sender,
			_toAddress,
			deliverEth,
			additionalPayload
		);

The problem is that the sender in the source chain may not be in control of the same address in the destination chain. For instance, this could occur if the sender is a Gnosis Safe created using the createProxy() function of the Proxy Factory. This issue has been exploited in the past to steal 20 million $OP.

Proof of Concept

  1. Alice owns a Gnosis Safe deployed using the createProxy() function of the Gnosis Safe Proxy Factory.
  2. Alice initiates a cross-chain transaction from her safe through the DecentEthRouter.bridgeWithPayload()function. The _getCallParams() function hardcodes the address of Alice's safe on the source chain as the from address.
  3. The transaction is processed by either the _executeWeth() or _executeEth() function in the DecentBridgeExecutor contract of the target chain.
  4. The transaction call to the target contract fails due to an issue outside of Alice's control, such as the target contract being paused.
  5. The DecentBridgeExecutor contract sends the funds to the from address, which is the address of Alice's safe on the source chain.
  6. However, since Alice does not control this address on the destination chain, she is unable to access these funds. This results in a loss of funds for Alice.

Tools Used

Manual review

To mitigate this issue, it is recommended to revert the transaction if the call to the target contract fails, instead of sending the funds to the from address. Since the DcntEth contract is a NonblockingLzApp, this is the recommended behavior as failed transactions are stored and can be retried after they fail.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T21:15:19Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:15:27Z

raymondfam marked the issue as duplicate of #27

#2 - c4-judge

2024-02-02T17:20:54Z

alex-ppg marked the issue as partial-75

Findings Information

🌟 Selected for report: haxatron

Also found by: Aamir, EV_om, MrPotatoMagic, Topmark, bart1e, deth, rouhsamad

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
duplicate-59

Awards

794.0484 USDC - $794.05

External Links

Lines of code

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

Vulnerability details

Impact

The DecentEthRouter.onOFTReceived() function is responsible for handling incoming messages from the Decent bridge. It decodes the payload of the transfer to extract necessary information such as the type of message, sender, recipient, and whether the transfer should be in ETH or WETH.

If the message type is MT_ETH_TRANSFER_WITH_PAYLOAD and the sender sets deliverEth to false, the transaction is expected to allow the target contract to pull the WETH amount in a call with the given payload: https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L31-L33

	weth.approve(target, amount);
	(bool success, ) = target.call(callPayload);

However, if the WETH balance of the DecentEthRouter is lower than the transferred amount, the payload is simply dropped and instead, DcntEth is sent to the target address: https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L266-L269

	if (weth.balanceOf(address(this)) < _amount) {
		dcntEth.transfer(_to, _amount);
		return;
	}

This could lead to loss of funds as the user may have required the payload to be executed to obtain the funds.

Proof of Concept

  1. A user initiates a cross-chain transaction by submitting a message through the DecentEthRouter.bridgeWithPayload() function. They want to stake WETH on a contract on another chain by calling the function stakeWETHFor(address account, uint256 amount).
  2. The DecentEthRouter receives the message in onOFTReceived().
  3. Its WETH balance is less than the amount of WETH the user is transferring.
  4. The payload is dropped and instead transfers DcntEth directly to the target contract.
  5. The user's funds are locked as DcntEth in the contract and cannot be recovered.

Tools Used

Manual review

If the WETH balance is not sufficient when processing a message of type MT_ETH_TRANSFER_WITH_PAYLOAD, revert and allow the sender to retry the message later. Alternatively, ensure there is always enough WETH in all supported chains, or at the very least document this behavior.

Assessed type

Other

#0 - c4-pre-sort

2024-01-25T21:10:36Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:11:03Z

raymondfam marked the issue as duplicate of #59

#2 - c4-judge

2024-02-02T15:14:09Z

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