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: 37/113
Findings: 4
Award: $122.24
🌟 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.setRouter
is used to change the router address. The router is a privileged role that can mint and burn DcntEth
tokens. setRouter
can be called by anyone, meaning that a malicious user can create his own router which behaves the same as the original one, but with 1 change - he will add a function that allows him to mint/burn wherever they want.
This can be unnoticed for a long time. The user will then have control over the whole protcol.
function testAnyoneCanChangeRouter() public { vm.prank(address(0xb0b)); dcntEth.setRouter(address(weth)); assertEq(dcntEth.router(), address(weth)); }
Foundry
Add the onlyOwner modifier to DcntEth.setRouter()
- function setRouter(address _router) public { + function setRouter(address _router) public onlyOwner { router = _router; }
Access Control
#0 - c4-pre-sort
2024-01-25T03:30:14Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-25T03:30:33Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:10:03Z
alex-ppg marked the issue as satisfactory
78.6887 USDC - $78.69
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L35-L38 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L62-L64
Smart contracts that interact with the protocol may not receive their refund in case a transaction fails on the destination chain. This is because refunds are sent to the from
address of the source chain. Smart contacts are not guaranteed to have the same address on different chains. This means that any funds sent to from
address that is not controlled by the protocol interacting with Decent will probably be lost.
There are two (Gnosis) Safes controlled by a protocol on two different chains. Since they are smart contracts, their addresses can be different. The protocol wants to use Decent for cross-chain execution. For some reason, their transaction fails on the destination chain.
DecentBridgeExecutor
will send the funds to the from
address
function _executeEth( address from, address target, uint256 amount, bytes memory callPayload ) private { weth.withdraw(amount); (bool success, ) = target.call{value: amount}(callPayload); if (!success) { // here payable(from).transfer(amount); } }
This from
will be different from the address of the smart contract on the currentChain. Funds will be send to the wrong address and will be lost.
Add a new parameter address destinationRefund
that can be specified on the source chain. Then use it instead of from
.
if (!success) { - payable(from).transfer(amount); + payable(destinationRefund).transfer(amount); }
Other
#0 - c4-pre-sort
2024-01-25T05:17:21Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-25T05:17:30Z
raymondfam marked the issue as duplicate of #27
#2 - c4-judge
2024-02-02T17:21:32Z
alex-ppg marked the issue as partial-75
#3 - c4-judge
2024-02-04T23:04:04Z
alex-ppg changed the severity to 3 (High Risk)
43.4302 USDC - $43.43
Any refunds from LayerZero will be sent to DecentBridgeAdapter
, not to the initiator of the action. The adapter does not have any way to send this funds to the correct recipient, so they will be left in the contract, available for further use by other users.
DecentBridgeAdapter
to execute a cross-chain action.UTB.bridgeAndExecute()
.UTB.bridgeAndExecute
calls UTB.callBridge()
, which makes an internal call to DecentBridgeAdapter.bridge()
and sends native tokens for gas.IBridgeAdapter(bridgeAdapters[instructions.bridgeId]).bridge{ value: bridgeFee + (native ? amt2Bridge : 0) }(...);
DecentBridgeAdapter.bridge()
calls DecentEthRouter.bridgeWithPayload()
and forwards the native value.router.bridgeWithPayload{value: msg.value}( lzIdLookup[dstChainId], destinationBridgeAdapter[dstChainId], swapParams.amountIn, false, dstGas, bridgePayload );
DecentEthRouter.bridgeWithPayload
calls the internal function _bridgeWithPayload()
.
It then constructs call parameters for layer zero and sets the refund address to msg.sender
. If we look at step 4, it's clear that msg.sender
is DecentBridgeAdapter
. The funds will be sent to it and not to the initiator of the action, resulting in loss for the sender and profit for the next user that uses the protocol.
ICommonOFT.LzCallParams memory callParams = ICommonOFT.LzCallParams({ refundAddress: payable(msg.sender), zroPaymentAddress: address(0x0), adapterParams: adapterParams });
Manual Review
Instead of msg.sender
, use a parameter specified by the initiator of the action.
Other
#0 - c4-pre-sort
2024-01-25T02:12:55Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-25T02:13:02Z
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:17Z
alex-ppg marked the issue as duplicate of #262
#4 - c4-judge
2024-02-02T17:01:16Z
alex-ppg marked the issue as satisfactory