Decent - NPCsCorp'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: 3/113

Findings: 5

Award: $2,570.95

🌟 Selected for report: 2

🚀 Solo Findings: 0

Lines of code

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

Vulnerability details

Impact

By allowing anybody to set the address of the Router contract to any address they want to set it allows malicious users to get access to the mint and burn functions of the DcntEth contract.

Proof of Concept

The DcntEth::setRouter() function has not an access control to restrict who can call this function. This allows anybody to set the address of the router contract to any address they'd like to set it.

DcntEth.sol

//@audit-issue => No access control to restrict who can set the address of the router contract
function setRouter(address _router) public {
    router = _router;
}

The functions DcntEth::mint() function & DcntEth::burn() function can be called only by the router contract.

DcntEth.sol


    //@audit-info => Only the router can call the mint()
    function mint(address _to, uint256 _amount) public onlyRouter {
        _mint(_to, _amount);
    }

    //@audit-info => Only the router can call the burn()
    function burn(address _from, uint256 _amount) public onlyRouter {
        _burn(_from, _amount);
    }

A malicious user can set the address of the router contract to an account of their own and:

  1. Gain access to mint unlimited amounts of DcntEth token, which could be used to disrupt the crosschain accounting mechanism, or to steal the deposited weth in the DecentEthRouter contract.
  2. Burn all the DcntEth tokens that were issued to the DecentEthRouter contract when liquidity providers deposited their WETH or ETH into it.
  3. Cause a DoS to the add and remove liquidity functions of the DecentEthRouter contract. All of these functions end up calling the DcntEth::mint() function or the DcntEth::burn() function, if the router address is set to be different than the address of the DecentEthRouter, all the calls made from the DecentEthRouter to the DcnEth contract will revert.

DecentEthRouter.sol


    /// @inheritdoc IDecentEthRouter
    function addLiquidityEth()
        public
        payable
        onlyEthChain
        userDepositing(msg.value)
    {
        weth.deposit{value: msg.value}();
        
        //@audit-issue => If router in the dcnteth contract is not set to the address of the DecentEthRouter, this call will revert
        dcntEth.mint(address(this), msg.value);
    }

    /// @inheritdoc IDecentEthRouter
    function removeLiquidityEth(
        uint256 amount
    ) public onlyEthChain userIsWithdrawing(amount) {

      //@audit-issue => If router in the dcnteth contract is not set to the address of the DecentEthRouter, this call will revert
        dcntEth.burn(address(this), amount);
        weth.withdraw(amount);
        payable(msg.sender).transfer(amount);
    }

    /// @inheritdoc IDecentEthRouter
    function addLiquidityWeth(
        uint256 amount
    ) public payable userDepositing(amount) {
        weth.transferFrom(msg.sender, address(this), amount);

        //@audit-issue => If router in the dcnteth contract is not set to the address of the DecentEthRouter, this call will revert
        dcntEth.mint(address(this), amount);
    }

    /// @inheritdoc IDecentEthRouter
    function removeLiquidityWeth(
        uint256 amount
    ) public userIsWithdrawing(amount) {

      //@audit-issue => If router in the dcnteth contract is not set to the address of the DecentEthRouter, this call will revert
        dcntEth.burn(address(this), amount);
        weth.transfer(msg.sender, amount);
    }

Tools Used

Manual Audit

Make sure to add an Acess Control mechanism to limit who can set the address of the Router in the DcnEth contract.

Assessed type

Access Control

#0 - c4-pre-sort

2024-01-25T21:45:55Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:46:06Z

raymondfam marked the issue as duplicate of #14

#2 - c4-judge

2024-02-03T13:08:01Z

alex-ppg marked the issue as satisfactory

#3 - alex-ppg

2024-02-03T13:33:37Z

This and all relevant submissions correctly specify that the lack of access control in the DcntEth::setRouter function can be exploited maliciously and effectively compromise the entire TVL of the Decent ETH token.

A high-risk severity is appropriate, and this submission was selected as the best due to detailing all possible impacts:

  • Arbitrary mints of the token to withdraw funds provided as liquidity to UTB
  • Arbitrary burns to sabotage liquidity pools and other escrow-based contracts
  • Sabotage of liquidity provision function invocations

#4 - c4-judge

2024-02-03T13:33:44Z

alex-ppg marked the issue as selected for report

#5 - c4-sponsor

2024-02-13T21:41:56Z

wkantaros (sponsor) confirmed

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#L80

Vulnerability details

The vulnerability

The root cause of the vulnerability exists in the DecentEthRouter::_getCallParams() internal function which is responsible for configuring the adapter parameters which will be sent to Layerzero

function _getCallParams( uint8 msgType, address _toAddress, uint16 _dstChainId, uint64 _dstGasForCall, bool deliverEth, bytes memory additionalPayload ) private view returns ( bytes32 destBridge, bytes memory adapterParams, bytes memory payload ) { uint256 GAS_FOR_RELAY = 100000; uint256 gasAmount = GAS_FOR_RELAY + _dstGasForCall; adapterParams = abi.encodePacked(PT_SEND_AND_CALL, gasAmount); destBridge = bytes32(abi.encode(destinationBridges[_dstChainId])); if (msgType == MT_ETH_TRANSFER) { payload = abi.encode(msgType, msg.sender, _toAddress, deliverEth); } else { payload = abi.encode( msgType, msg.sender, _toAddress, deliverEth, additionalPayload ); } }

The DecentEthRouter::_getCallParams() function takes the value of the parameter _dstGasForCall (which is user supplied) and adds it to the pre-set amount of gas for the relayer (100,000) and sets the result as the amount of gas to be sent.

The issue is that the attacker can supply zero as the value of _dstGasForCall parameter, leading to only 100,000 gas being set which is not sufficient and will result in StoredPayload and the message pathway getting blocked.

The user gets to supply the value of _dstGasForCall parameter at the very beginning when he decides to call the function UTB::bridgeAndExecute() in bridgeInstructions.additionalArgs, and then it keeps getting passed until it reaches DecentEthRouter::bridgeWithPayload which calls the internal function DecentEthRouter:_bridgeWithPayload() which will call the DecentEthRouter::_getCallParams() which will prepare the adapter parameters for the call to be sent to LayerZero

Additionally, the attacker can instruct the protocol to conduct an cross-chain arbitrary call to a malicious gas-griefing contract of his which will deplete all of the gas supplied leading the LZ call to fail and getting the message pathway to be be blocked.

Impact

The communication channel between a source chain and a destination chain can be blocked by exploiting the ability to specify arbitrary gas parameters for the destination chain function calls. An attacker consistently (and at a low cost), can block communication between the source chaind and a destination chain (which can only be unblocked through a manual intervention)

Tools Used

Manual Audit & LayerZero

This is most easily fixed by checking if the gas passed in the instructions.additionalArgs in the UTB::bridgeAndExecute() function is less than the minimum threshold.

Or fix it in the DecentEthRouter::_getCallParams() function and check if the _dstGasForCall meets the minimum amount of gas needed.


Found & reported by: sin1st3r__

Team: NPCsCorp

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-01-25T21:59:53Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T22:00:02Z

raymondfam marked the issue as duplicate of #212

#2 - raymondfam

2024-01-25T22:02:33Z

Same root cause as in #212 with additional attacking vector.

#3 - c4-judge

2024-02-02T12:32:10Z

alex-ppg marked the issue as satisfactory

Findings Information

Awards

104.9182 USDC - $104.92

Labels

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

External Links

Lines of code

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

Vulnerability details

Impact

Users will lost all their bridged funds in case that the bridge execution fails in the destination chain.

Proof of Concept

When a user is bridging tokens to perform an execution in the destination chain, the users initiates their transaction by:

  1. Calling the UTB::bridgeAndExecute() function, from there it will do the swap pre-bridge (in the src chain) & will modify the swapInstructions for the swap post-bridge (in the dest chain), then it will invoke the UTB::callBridge() function
  2. In the UTB::callBridge() function some approvals are granted to the DecentBridgeAdapter contract, and then the execution is forwarded to the DecentBridgeAdapter::bridge() function
  3. In the DecentBridgeAdapter::bridge() function, some data is encoded and decoded to prepare the calldata for the bridge, and then the DecentEthRouter::bridgeWithPayload() function is called.
  4. When the execution reaches the DecentEthRouter::bridgeWithPayload() function, notice how the msg.sender in the DecentEthRouter contract will be the DecentBridgeAdapter contract, (from step 3 to step 4, the caller is the DecentBridgeAdapter contract). When the payload that is sent for the execution in the destination chain is defined in the DecentEthRouter::_getCallParams() function, which is called by the DecentEthRouter::_bridgeWithPayload() function, the second parameter that is encoded as part of the payload variable it is set to be the msg.sender, keep in mind that the msg.sender is the address of the DecentBridgeAdapter, not the user's address or an address controlled by him.
  5. From here onwards, the execution is sent to LayerZero, which will send the payload variable to the DecentEthRouter::onOFTReceived() function in the destination chain.
Steps 1 to 5 of the execution flow
  1. When the DecentEthRouter::onOFTReceived() function is called, the received _payload is decoded and the encoded values are saved in individual variables,*** the one we are interested is the second variable***, which is the msg.sender that was encoded in the DecentEthRouter::_getCallParams() function, then, if the crosschain message happens to be of an execute type, the execution will be forwarded to the DecentBridgeExecutor::execute() function
  2. In the DecentBridgeExecutor::execute() function, either the DecentBridgeExecutor::_executeWeth() function or the DecentBridgeExecutor::_executeEth() function are called, and the received from parameter (which was just decoded from the received payload in the onOFTReceived()) is forwarded to those functions.
Steps 6 to 7 of the execution flow
  1. In any of those functions, the execution will call the target contract (which is the DecentBridgeAdapter::receiveFromBridge() of the current chain).
  2. If the call to the target contract reverts, all the bridged funds are refunded to the from parameter, which is exactly the msg.sender that was encoded in the DecentEthRouter::_getCallParams() function. (See step 4)
  • Refunding to the msg.sender who called the DecentEthRouter::_getCallParams() function in the source chain will cause that the users lost all his bridged funds, and instead, the DecentBridgeAdapter contract address of the source chain will be the one that received the refund of the bridged funds.
Steps 8 to 9 of the execution flow

Tools Used

Manual Audit

Instead of hardcoding the refundAddress to be the msg.sender, use the refundee address that was already provided by the user. This address was passed in the calldata sent to the UTB::bridgeAndExecute() function, specifically, the refundAddress specified by the user is encoded in the instructions.refund variable.

  • Make sure to decode the encoded calldata that gets to the DecentEthRouter::_getCallParams() function. Read the value of the instructions.refund variable that was sent at the beginning of the execution, and use that address to set the second variable of the payload variable that is passed as a parameter for the execution in the destination chain.

Assessed type

en/de-code

#0 - c4-pre-sort

2024-01-25T21:40:45Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:40:53Z

raymondfam marked the issue as duplicate of #27

#2 - c4-judge

2024-02-02T17:19:24Z

alex-ppg marked the issue as satisfactory

Findings Information

🌟 Selected for report: NPCsCorp

Also found by: Soliditors, haxatron, nmirchev8, peanuts

Labels

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

Awards

679.6793 USDC - $679.68

External Links

Lines of code

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

Vulnerability details

The execution flow of bridgeAndExecute function

To understand the vulnerability, we need to understand the execution flow of the bridgeAndExecute() function, at least a small portion of it.

When the user wants to bridge tokens of him and execute an action on another chain, he will need to execute the UTB::bridgeAndExecute() function.

Suppose the user exists in Polygon, he has USDC and he wants to mint an NFT in Optimism which costs 1000 DAI. What will happen is that the protocol will first, in polygon, swap the user's USDC with WETH, then bridge the WETH to Optimism, then swap the WETH with DAI and then execute the arbitrary call the user wants to execute, which will be to mint the NFT in exchange for the resulting 1000 DAI from the post-bridge swap operation.

When this function is called, the following will happen:

  1. Step 1: When the user calls the UTB::bridgeAndExecute() function, it will do three things: first, it will collect the fees by calling the UTBFeeCollector:collectFees() function, secondly, it will conduct the pre-bridge swap operation (occurs in the source destination), it will swap the user's USDC to WETH. thirdly, it will modify the swapInstructions which the user supplied to prepare for the post-bridge swap. Then after all of the 3 operations take place, it will invoke the UTB::callBridge() function.

  2. Step 2: In the UTB::callBridge() function, some approvals are granted to the DecentBridgeAdapter contract, and then the it will invoke the function DecentBridgeAdapter::bridge() in the DecentBridgeAdapter contract.

  3. Step 3: In the DecentBridgeAdapter::bridge() function, some data like the post-bridge swap payload and bridge payload (what to execute when the TX reaches destination) will be encoded, then it will reach out to the DecentEthRouter contract and invoke the function DecentEthRouter::bridgeWithPayload

  4. Step 4: When the execution reaches the DecentEthRouter::bridgeWithPayload function, an internal function containing the actual logic, with the same name will also be called: DecentEthRouter::_bridgeWithPayload

    Note: Notice that the `DecentEthRouter::bridgeWithPayload() function isn't protected by any modifiers, any body can call it directly

  5. Step 5: When the execution gets inside the DecentEthRouter::_bridgeWithPayload function, the function will prepare the LzCallParams for the layerzero call and the actual bridging will happen when the dcntEth::sendAndCall function is actually invoked.

  6. Step 6: The bridging process kickstarts and the execution flow is continued in the destination chain.

Here is a graph of the execution flow Execution flow

The vulnerability & PoC

The vulnerability is that any user can conduct the pre-bridge swap operation using uniswap by himself, prepare the right calldata and call the function DecentEthRouter::bridgeWithPayload directly (since anybody can call it), doing so will allow the user to bypass the fee collection process completely.

The fee collection as mentioned in the previously detailed execution flow, happens when the user first calls the bridgeAndExecute() function. But as I mentioned, nothing really forces him to start execution from there. All that function does is collect the fees, conduct the pre-bridge swap operation and prepare the proper call data. The user can conduct the pre-bridge swap operation himself, prepare the proper calldata and talk directly to the DecentEthRouter::bridgeWithPayload function, effecitvely bypassing fee collection.

Anybody can call the DecentEthRouter::bridgeWithPayload directly, and the protocol has no mechanism of determining whether or not the user is using the protocol with or without paying fees.

Impact

Users can use the protocol without paying any fees.

Tools Used

Manual Audit

Tighten up the access control on the DecentEthRouter::bridgeWithPayload function. Allow only the DecentBridgeAdapter to call the bridgeWithPayload() function.


Found & reported by: sin1st3r__

Team: NPCsCorp

Assessed type

Access Control

#0 - c4-pre-sort

2024-01-25T05:44:02Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T05:44:08Z

raymondfam marked the issue as duplicate of #15

#2 - c4-pre-sort

2024-01-26T02:50:55Z

raymondfam marked the issue as not a duplicate

#3 - c4-pre-sort

2024-01-26T02:51:06Z

raymondfam marked the issue as duplicate of #51

#4 - raymondfam

2024-01-26T02:54:56Z

This report details a clear flow on how bridge dodging can be used to avoid fees.

#5 - c4-pre-sort

2024-01-26T02:55:04Z

raymondfam marked the issue as high quality report

#6 - c4-judge

2024-02-02T16:22:20Z

alex-ppg marked the issue as selected for report

#7 - alex-ppg

2024-02-02T16:23:47Z

The Warden has demonstrated that it is possible to bypass Decent fees when performing bridging operations by directly interacting with the DecentEthRouter. This is indeed a valid concern and has been confirmed by the Sponsor.

I believe a severity of medium is more appropriate given that uncaptured profit is solely affected.

#8 - c4-judge

2024-02-02T16:23:51Z

alex-ppg marked the issue as satisfactory

#9 - c4-judge

2024-02-02T16:23:56Z

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

#10 - mcgrathcoutinho

2024-02-06T16:54:08Z

Hi @alex-ppg, thank you for the detailed response.

This issue is invalid since the decent bridge does not charge any bridge fees in the first place. If we look at the modifier retrieveAndCollectFees, it uses the fees.feeAmount (which is the swap fee) while the fees.bridgeFee is not considered. This is expected since the decent bridge is not charging fees.

We can see this expected behaviour here. When the bridgedAmount() function is called on the adapter, the decentBridgeAdapter just returns the same amount without any bridge fees applied (see here), while the stargateBridgeAdapter applies the bridgeFee correctly here.

Please consider re-evaluating this issue.

Thank you.

#11 - stalinMacias

2024-02-07T00:56:19Z

This issue is invalid since the decent bridge does not charge any bridge fees in the first place.

That doesn't make any sense. In the bypassed retrieveAndCollectFees function natspec, it is clearly saying: "Transfers fees from the sender to UTB, and finally to the Fee Collector" And it does in fact execute a transferFrom TX to transfer the fees.feeAmount count of fees.feeToken to the UTB Fee collector (which is supposed to be holding fees).

Additionally, if we look at the docs, here is it's saying: Screenshot 2024-02-07 at 2 54 45 AM

Do you understand from all this that there are no fees being collected?

#12 - mcgrathcoutinho

2024-02-07T01:12:30Z

Hi @stalinMacias, thank you for the comment.

The fees.feeAmount is the swap fee and not the bridge fee. We can see this in the FeeStructure struct below:

struct FeeStructure {
    uint bridgeFee;
    address feeToken;
    uint feeAmount;
}

The decent bridge not taking bridge fees is something that the sponsor had confirmed as well (although in my private thread). It would be good if the judge clarifies this separately with the sponsor since it would be against the code of conduct for me to introduce information from the private thread (though I'm willing to invite anyone to it for proof).

#13 - stalinMacias

2024-02-07T02:41:09Z

@mcgrathcoutinho Let's break this down.

  1. The decent bridge contract per se may/or may not charge a fee that will be received directly into the contract, but one thing is taking fees when a contract is called, and another very different thing is charging fees for using a service..
  1. All the parameters that are supplied to the UTB.bridgeAndExecute() function are generated by an API/backend of the protocol, why do we know this? Because in the retrieveAndCollectFees() modifier all the inputs are sent to the FeeCollector.collectFees() function to be verified that they match with the signature that was signed with an account controlled by the protocol.
  • That means, the API that generates the signatures and calldatas that will be sent to the UTB contract to use any of their services will compute the corresponding fees that will be charged depending on the type of operation the user requests to perform.
  1. The amount of fees computed in the FeeStructure.feeAmount represents the amount of fees that the user will be charged for the usage of the protocol's services, again, such feeAmount was previously computed by their API that will generate a signature to validate that all the inputted parameters are correct and have not been altered.

From your comment: "The fees.feeAmount is the swap fee and not the bridge fee. "

  • That is a totally incorrect assumption, feeAmount doesn't represent the swap fee, feeAmount represents the fees that the protocol will charge for their services.

From your comment: "The decent bridge not taking bridge fees is something that the sponsor had confirmed as well "

  • Again, one thing is that the bridge itself charges fees for its usage, like the Stargate Bridge, anybody who uses that bridge will pay a fee that will be stored in that contract. And another very different thing is charging fees for a service provided by the Decent Protocol. As per the Decent's documentation, all of their services charges fees (except the direct executions), those fees are what is represented in the FeeStructure.feeAmount .

  • At this point, I've already made a clear explanation about the FeeStructure. Now, reiterating the point of this report, by directly calling the DecentEthRouter::bridgeWithPayload() function, users will be able to use the protocol's service bridgeSwapAndExecute without paying fees to the protocol for the usage of their infrastructure.

@mcgrathcoutinho I hope this explanation clears any doubts you may had about the report.

#14 - alex-ppg

2024-02-09T10:18:26Z

Hey @mcgrathcoutinho and @stalinMacias, thank you for contributing to this thread.

Regardless of what the Sponsor shared in private discussions, all material in the documentation as well as the code of the project points to a fee being charged. The entire presence of the UTBFeeCollector (as its name implies) is to collect fees from the UTB contract. Otherwise, it would not exist in the repository and not be in the scope of the contest.

Based on the above, my original ruling on this issue stands.

#15 - c4-sponsor

2024-02-13T21:42:43Z

wkantaros (sponsor) confirmed

Findings Information

Awards

43.4302 USDC - $43.43

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
duplicate-262

External Links

Lines of code

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

Vulnerability details

Impact

Users will lose any excess gas they pay for LZ calls because the refundee address is incorrectly hardcoded to be the msg.sender of the DecentEthRouter::bridgeWithPayload() function rather than the refund address the user specified

Proof of Concept

When a crosschain message is sent through LayerZero it is required to prepare a calldata that it'll be sent to the LayerZero contracts. One of the parameter for this calldata it is the refundAddress, which indicates to the LayerZero contracts where to refund the excess gas that was paid for the transaction in case that the transaction's cost is cheaper than the amount of value passed.

This is the execution flow when a user wants to perform a bridge of funds from one chain to another

  1. The user starts the execution by calling the a UTB::bridgeAndExecute() function, here the fees for the operation are charged, a swap pre-bridge its executed && the swapParams for the swap post-bridge are prepared, then execution goes to the UTB::callBridge() function
  2. In the UTB::callBridge() function, some approvals are granted to the DecentBridgeAdapter contract, and then the execution is forwarded to the DecentBridgeAdapter::bridge() function
  3. In the DecentBridgeAdapter::bridge() function, some data is encoded and decoded to prepare the calldata for the bridge, and then the DecentEthRouter::bridgeWithPayload() function is called.
  4. When the execution reaches the DecentEthRouter::bridgeWithPayload() function, the msg.sender is be the DecentBridgeAdapter contract's address, (from step 3 to step 4, the caller is the DecentBridgeAdapter contract), when the LZCallParams are defined in the DecentEthRouter::_bridgeWithPayload() function, the callParams.refundAddress is set to msg.sender.
  • By setting the callParams.refundAddress to be the msg.sender, what is happening is that the DecentBridgeAdapater contract is set as the refundAddress for the excess gas that was paid for the execution, instead of setting the refundAddress to be an address controller by the user.
    • This will cause that the DecentBridgeAdapter contract receives the refund of excess gas instead of an account controlled by the user who initiated the bridge operation.
      • The DecentBridgeAdapter contract will accumulate a balance, and the user losses the excess gas that was paid for the lz call, which means that instead of refunding the excess gas to an account controlled by the user, any excess gas in the destination chain will be refunded to the address of the DecentBridgeAdapater contract.
  1. From the DecentEthRouter::_bridgeWithPayload() function, the execution is sent to the DcntEth::sendAndCall() function where it will end up calling the LayerZeroEndpoint::send() function.

  2. Once the execution reaches the LayerZeroEndpoint contract, at the end of the execution of the LayerZeroEndpoint::send() function, the endpoint contract computes if there is any leftover gas that was paid but was not spent, if so, it refunds the excess gas to the _refundAddress that was passed in the callParams.refundAddress. The problem is that the callParams.refundAddress was encoded as the msg.sender who called the DecentEthRouter::bridgeWithPayload() function [Step 4]. This will cause that the refunded excess gas is sent to the DecentBridgeAdapater contract instead of an account controlled by the user.

Chain Of Calls

Tools Used

Manual Audit, LayerZero Documentation & LayerZeroEndpoint contract

Instead of hardcoding the refundAddress to be the msg.sender, use the refundee address that was already provided by the user. This address was passed in the calldata sent to the UTB::bridgeAndExecute() function, specifically, the refundAddress specified by the user is encoded in the instructions.refund variable.

  • Make sure to decode the encoded calldata that gets to the DecentEthRouter::bridgeWithPayload() function. Read the value of the instructions.refund variable that was sent at the beginning of the execution, and use that address to set the callParams.refundAddress which specifies the address where the excess gas paid to LayerZero should be refunded.

Assessed type

en/de-code

#0 - c4-pre-sort

2024-01-25T21:36:09Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-25T21:36:16Z

raymondfam marked the issue as duplicate of #27

#2 - c4-judge

2024-02-02T16:57:06Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2024-02-02T16:58:08Z

alex-ppg marked the issue as duplicate of #262

#4 - c4-judge

2024-02-02T17:00:32Z

alex-ppg marked the issue as satisfactory

#5 - c4-judge

2024-02-04T23:04:27Z

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

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