Skip to content

Latest commit

 

History

History
192 lines (134 loc) · 14.7 KB

2024-01-decent.md

File metadata and controls

192 lines (134 loc) · 14.7 KB

[H-01] Insufficient access control in setRouter() allows uncontrolled minting of DcntEth

Lines of code

DcntEth.sol#L20-L30

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

Recommended Mitigation Steps

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.

[H-02] Attacker can block LayerZero channel due to variable gas cost of saving payload

Lines of code

DecentEthRouter.sol#L197
DecentEthRouter.sol#L103-L109
DecentEthRouter.sol#L161

Impact

This issue is internally the same as described in code-423n4/2023-07-tapioca-findings#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

Recommended Mitigation Steps

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.

[H-03] Failed calls made via bridgeWithPayload() may transfer funds to address not under user control

Lines of code

DecentBridgeExecutor.sol#L36
DecentBridgeExecutor.sol#L63

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

Recommended Mitigation Steps

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.

[H-04] Dropping payload in onOFTReceived() may lead to loss of funds

Lines of code

DecentEthRouter.sol#L266-L269

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

Recommended Mitigation Steps

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.

[L-01] Address size assumption in payload decoding can lead to issues in cross-chain compatibility

This issue was downgraded from Medium to Low

Lines of code

DecentEthRouter.sol#L245

Impact

From the contest docs:

consider scope of blockchains to those supported by layerzero

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.

The issue arises in the decoding of the payload. The function assumes that the address size is always 20 bytes. This is true for Ethereum-based chains, but not for all blockchains. For instance, Aptos uses 32-byte addresses. This discrepancy in address size can lead to incorrect decoding of the payload, potentially causing transfers to fail or be misdirected.

Proof of Concept

  1. User initiates a transfer from Aptos to an Ethereum-based chain using the bridge.
  2. The onOFTReceived() function on the Ethereum side tries to decode the payload.
  3. The function assumes a 20-byte address, but the payload contains a 32-byte Aptos address.
  4. The decoding fails or produces incorrect results, leading to a failed or misdirected transfer.

Tools Used

Manual review

Recommended Mitigation Steps

The contract should not make assumptions about the size of addresses. Instead, it should dynamically determine the correct size based on the chain ID. This could be achieved by maintaining a mapping of chain IDs to address sizes, which can be updated as new chains are supported. The function would then use this mapping to correctly decode the payload. This would ensure compatibility with all supported chains, regardless of their address size.

[L-02] Solidity v0.8.20 is unsupported by Avalanche

This issue was downgraded from Medium to Low

Lines of code

foundry.toml#L5

Impact

From the contest docs:

consider scope of blockchains to those supported by layerzero

Avalanche is also supported by LayerZero, however, the project uses Solidity v0.8.20, which is unsupported by Avalanche: https://github.com/code-423n4/2024-01-decent/blob/main/foundry.toml#L5

solc = "0.8.20"

For more details see code-423n4/2024-01-renft-findings#127.

Proof of Concept

See code-423n4/2024-01-renft-findings#127

Tools Used

Manual review

Recommended Mitigation Steps

It's recommended that v0.8.19 is used instead to maintain compatibility with all blockchains supported by LayerZero.