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: 14/113
Findings: 4
Award: $660.44
π Selected for report: 0
π Solo Findings: 0
π Selected for report: NPCsCorp
Also found by: 0x11singh99, 0xAadi, 0xBugSlayer, 0xE1, 0xPluto, 0xSimeon, 0xSmartContract, 0xabhay, 0xdice91, 0xprinc, Aamir, Aymen0909, CDSecurity, DadeKuma, DarkTower, EV_om, Eeyore, GeekyLumberjack, GhK3Ndf, Giorgio, Greed, Inference, JanuaryPersimmon2024, Kaysoft, Krace, Matue, MrPotatoMagic, NentoR, Nikki, PUSH0, Soliditors, Tendency, Tigerfrake, Timeless, Timenov, ZanyBonzy, ZdravkoHr, abiih, adeolu, al88nsk, azanux, bareli, boredpukar, cu5t0mpeo, d4r3d3v1l, darksnow, deth, dutra, ether_sky, haxatron, ke1caM, kodyvim, m4ttm, mgf15, mrudenko, nmirchev8, nobody2018, nuthan2x, peanuts, piyushshukla, ravikiranweb3, rouhsamad, seraviz, simplor, slylandro_star, stealth, th13vn, vnavascues, wangxx2026, zaevlad
0.1172 USDC - $0.12
DcntEth
represents bridged token for decent bridge, which uses LayerZero OFT. In current implementation, DecentRouterEth
is responsible for minting DcentEth pegged to WETH to users, which are providing liquidity in the router.
The problem is there is no access control on DecentBridge::setRouter
func, which is sets the address authorised for minting/burning DcntEth
tokens. By exploiting the following issue, attacker can mint himself tokens equal to all WETH liquidity in the router for free and then redeem the corresponding WETH, belonging to honest participants.
DecentRouterEth
has accrued liquidity of 100 WETH of honest participants.DcntEth::mint
with his address and amount of 100e18
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DcntEth.sol#L24-L26
3.The same actor return the router control over DcntEth
and calls DecentRouterEth::removeLiquidityWeth
, which successfully transfer him 100 WETH, which has been stolen from all participantsManual Review
Set onlyOwner
modifier on DcntEth::setRouter
:
- function setRouter(address _router) public { + function setRouter(address _router) public onlyOwner { router = _router; }
Access Control
#0 - c4-pre-sort
2024-01-23T23:48:28Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-23T23:48:35Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:29:29Z
alex-ppg marked the issue as satisfactory
104.9182 USDC - $104.92
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L105 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L245-L246 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L62-L64
There is big issue in combining UTB
and DecentBridge
in current implementation. DecentBridge
implements a logic to refund a sender on dest chain, if the call reverts:
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L33-L38
The problem is that from
address, which is used here will always be wrong, because it will correspond to the address of DecentBridgeAdapter
on src chain, which on dest chain will be some random address. This is also an issue, if we don't use UTB
, but directly call DecentEthRouter::bridgeWithPayload
from a Multisig wallet contract, because on destination chain the same address is not controlled by users of the multisig, so their funds are lost.
Lets roughly examine the execution flow:
When using UTB::callBridge
, the contract/function flow is as follows:
Source Chain (src):
UTB::callBridge
DecentBridgeAdapter::bridge
DecentEthRouter::bridgeWithPayload
DcntEth::_send
Destination Chain (dest):
5. DcntEth::onOFTReceived
6. DecentEthRouter::onOFTReceived
7. DecentBridgeExecutor::execute
Other contracts on the destination chain, which are not important in the current scenario, may also be involved.
Inside DecentEthRouter::bridgeWithPayload
we call _getCallParams
, where we encode destChain
, adapterParams
and payload
. Inside the payload from
address in set, which on destination chain is used as the refund address:
if (msgType == MT_ETH_TRANSFER) { payload = abi.encode(msgType, msg.sender, _toAddress, deliverEth); } else { payload = abi.encode( msgType, msg.sender, _toAddress, deliverEth, additionalPayload ); }
We can notice that we use msg.sender
, which in main flow, using UTB would be DecentBridgeAdapter
.
Here is the decoding of this payload
inside DecentEthRouter::onOFTReceived
(uint8 msgType, address _from, address _to, bool deliverEth) = abi .decode(_payload, (uint8, address, address, bool)); bytes memory callPayload = ""; if (msgType == MT_ETH_TRANSFER_WITH_PAYLOAD) { (, , , , callPayload) = abi.decode( _payload, (uint8, address, address, bool, bytes) ); }
In the end of the function we can see that _from
is passed as to DecentBridgeExecutor::execute
Here is the refund:
function _executeWeth( address from, address target, uint256 amount, bytes memory callPayload ) private { uint256 balanceBefore = weth.balanceOf(address(this)); weth.approve(target, amount); (bool success, ) = target.call(callPayload); if (!success) { weth.transfer(from, amount); return; } uint256 remainingAfterCall = amount - (balanceBefore - weth.balanceOf(address(this))); // refund the sender with excess WETH weth.transfer(from, remainingAfterCall); }
Any malicious actor can sandwich user swap transaction, so target.call(callPayload);
reverts and funds are sent to _from
address, which is some random address, because deployed contracts (as DecentBridgeAdaper
) has different addresses on different chains.
I have provided pretty detailed walk trough in the above section, but lets examine the following scenario:
UTB::callBridge
is executed.DecentEthRouter::bridgeWithPayload
encodes from
address param to DecentBridgeAdaper
on Arbitrum and flow continues.UTB::performSwap
, but it reverts, because of the slippage check and minAmountOut
is not satisfactory._from
address is not his.Manual Review
refundAddress
, instead of msg.sender
, inside DecentEthRouter::_getCallParams
additionalPayload
to obtain the refundAddress
previously provided in UTB::SwapInstructions
DecentEthRouter::bridgeWithPayload
to be used not only from bridge adapters, you should add a from
param argument to handle the case of multisig walletsToken-Transfer
#0 - c4-pre-sort
2024-01-25T01:03:53Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-25T01:04:00Z
raymondfam marked the issue as duplicate of #27
#2 - c4-judge
2024-02-02T17:22:32Z
alex-ppg marked the issue as satisfactory
π Selected for report: NPCsCorp
Also found by: Soliditors, haxatron, nmirchev8, peanuts
522.8302 USDC - $522.83
The flow to swapAndBridge/bridgeAndSwap assets is long and starts in UTB contract, where participant provides authenticator signature:
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L259-L266
The problem is that a malicious actor can directly call DecentEthRouter::bridge
with unchecked payload data, which means that it can be anything. For example - giving allowance for some ERC20 token from DescentExecutor to the attacker.
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L61
Another example is provided data, which is not checked by the signer, but reaches the following line and continues down the flow inside BridgeAdapter::receiveFromBridge
and UTB::receiveFromBridge
One example is:
bridgeWithPayload
Manual Review
Add a modifier on DecentEthRouter::bridge/bridgeWithPayload
so only BridgeAdapter can access it
Access Control
#0 - c4-pre-sort
2024-01-23T23:53:29Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-23T23:55:36Z
raymondfam marked the issue as duplicate of #15
#2 - c4-judge
2024-02-03T12:04:52Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2024-02-03T12:05:08Z
alex-ppg marked the issue as duplicate of #221
#4 - c4-judge
2024-02-03T13:03:29Z
alex-ppg marked the issue as satisfactory
#5 - c4-judge
2024-02-04T23:12:50Z
alex-ppg marked the issue as duplicate of #647
32.5727 USDC - $32.57
DecentEthRouter::bridgeWithPayload
configures payload an LZ params before calling dcntEth.sendAndCall
(layer zero send
functionality).
ICommonOFT.LzCallParams memory callParams = ICommonOFT.LzCallParams({ // @audit : refundAddress is set to `DecentBridgeAdapter` on src chain? refundAddress: payable(msg.sender), zroPaymentAddress: address(0x0), adapterParams: adapterParams });
In case of LayerZero transaction being reverted, user wonβt receive any refund, but the gas that he has pre-paid would be sent to DecentBridgeAdapter
and stucked. Worse thing is that other msg.sender(DecentBridgeAdapter)
address on dst chain will have another address on other chains, which means that if a refund is being triggered on the destination chain, funds would be send to unknown address.
Manual Review
bridgeWithPayload
use the param provided from the users:ETH-Transfer
#0 - c4-pre-sort
2024-01-24T23:58:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T23:58:22Z
raymondfam marked the issue as duplicate of #27
#2 - c4-judge
2024-02-02T16:57:11Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2024-02-02T16:58:27Z
alex-ppg marked the issue as duplicate of #262
#4 - c4-judge
2024-02-02T17:02:30Z
alex-ppg marked the issue as partial-75