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: 35/113
Findings: 3
Award: $148.47
🌟 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 function is currently wrongly left unrestricted, thus callable by everyone, as seen:
function setRouter(address _router) public { router = _router; }
Here are some of the effects of this:
DecentEthRouter
liquidity providers, by updating DcntEth
router address to another address(e.g. the attacker-controlled address), thus, reverting all calls from DecentEthRouter
contract, since the router now points to the user's set address. Hence, blocking all user's calls to DecentEthRouter::removeLiquidityWeth and removeLiquidityEth functions.address public router; modifier onlyRouter() { require(msg.sender == router); _; }
DcntEth::setRouter(bobAddress)
, to set the router to his address, then make another call to DcntEth::mint(bobAddress, amount) function to mint an arbitrary amount ofDcntEth
token to self, since the caller is the router(bob's address), bob will be minted with the input amount.
Bob can then go on to redeem the tokens for weth
or eth
from DecentEthRouter::redeemWeth or redeemEth function.Note that the amount Bob will be redeeming, will be less than or equal to the available liquidity(i.e. the contract weth
balance).
Manual Review
Update DcntEth::setRouter function to:
function setRouter(address _router) public onlyOwner { router = _router; }
Error
#0 - c4-pre-sort
2024-01-24T07:42:13Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T07:42:21Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:23:56Z
alex-ppg marked the issue as satisfactory
104.9182 USDC - $104.92
If a user wishes to bridge assets from one chain to another UTB::bridgeAndExecute function will be interacted with.
Assuming DecentBridgeAdapter will be used, on call to this function, the following further calls are made:
UTB::bridgeAndExecute --> UTB::callBridge --> DecentBridgeAdapter::bridge --> DecentEthRouter::bridgeWithPayload
At DecentEthRouter::bridgeWithPayload function, the payload is computed via _getCallParams private function as:
} else { payload = abi.encode( msgType, msg.sender, _toAddress, deliverEth, additionalPayload ); }
The set msg.sender
from the payload here, is intended to be used as the refund address in the destination chain if call execution fails and when executing a refund for _executeWeth calls, as we will see shortly.
There are two issues with the current implementation:
When the call originates from UTB::bridgeAndExecute
function, the caller here(msg.sender
) will be the DecentBridgeAdapter
contract and not the user. With this, on failure in the destination chain execution, the user's assets are instead sent to the adapter's address.
The owner of an address in chain A, might not be the owner of the address in chain B.
Assuming the caller interacted with DecentEthRouter::bridgeWithPayload
function directly the set msg.sender
in the payload will indeed be the caller, but if the caller's address isn't controlled by the caller in the destination chain, the user will lose his assets on failure.
Going further on the execution:
DecentEthRouter::_bridgeWithPayload, further calls dcntEth.sendAndCall
, sendAndCall
will further go on to send the payload over to LayerZero
endpoint.
On the destination chain, dcntEth::lzReceive
function will be called by LayerZero
endpoint, which will go on to call the destination chain DecentEthRouter::onOFTReceived function down its execution, this function also further calls DecentBridgeExecutor::execute function.
DecentBridgeExecutor::execute
function handles the execution of the passed-in payload, which on revert(i.e bridging failed), refunds assets to the from address(remember this will be the source bridge adapter address when the call originates from UTB
).
As stated in the contest Readme:
Any edge cases should be properly handled such that the user is issued a refund on the destination chain.
The expected implementation is that users be refunded their assets in the destination chain, on failure.
Manual Review
Update DecentEthRouter::bridgeWithPayload and bridge function to allow users to pass in a destination bridge refund address, then update _getCallParams to take in this address. Instead of passing in the msg.sender, use the input refund address:
if (msgType == MT_ETH_TRANSFER) { payload = abi.encode(msgType, refundAddress, _toAddress, deliverEth); //<--@ } else { payload = abi.encode( msgType, refundAddress, //<--@ _toAddress, deliverEth, additionalPayload ); }
Now, in DecentBridgeAdapter::bridge function, pass the already passed in refund address to the DecentEthRouter::bridgeWithPayload
call params
ETH-Transfer
#0 - c4-pre-sort
2024-01-24T06:32:58Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T06:33:12Z
raymondfam marked the issue as duplicate of #27
#2 - c4-judge
2024-02-02T17:26:49Z
alex-ppg marked the issue as satisfactory
43.4302 USDC - $43.43
When a user wishes to perform cross-chain txns UTB::bridgeAndExecute function will be interacted with.
This function performs cross-chain calls, leveraging layerZero
messaging.
Down the execution of this function, the private function callBridge is invoked, which then further makes an external call to the bridge adapter based on the based in bridgeId
instruction.
Assuming the passed in bridgeId
instruction is 0
, the DecentBridgeAdapter::bridge function will be called, which will further call DecentEthRouter::bridgeWithPayload
function.
The issue comes with how _bridgeWithPayload sets the layerZero
call params refund address:
ICommonOFT.LzCallParams memory callParams = ICommonOFT.LzCallParams({ refundAddress: payable(msg.sender), zroPaymentAddress: address(0x0), adapterParams: adapterParams });
The refund address is currently being set to the bridge adapter(the message sender), whenever the call path originates from UTB::bridgeAndExecute
.
note: the refund address is used by layer zero to refund the excess sent-in native fee:
// assert the user has attached enough native token for this address require(totalNativeFee <= msg.value, "LayerZero: not enough native for fees"); // refund if they send too much uint amount = msg.value.sub(totalNativeFee); if (amount > 0) { (bool success, ) = _refundAddress.call{value: amount}(""); require(success, "LayerZero: failed to refund"); }
With the current implementation, the bridge adapters will be refunded the user 's sent in excess native fee, this also goes contrary to the sponsor's note in the contest Readme:
Fund Accumulation: Other than the UTBFeeCollector, and DcntEth, the contracts are not intended to hold on to any funds or unnecessary approvals. Any native value or erc20 flowing through the protocol should either get delivered or refunded.
Manual Review
Add an extra address param to DecentEthRouter::bridgeWithPayload function, to take in a refund address, rather than use the msg.sender.
Also, refactor the preceding function calls from UTB::bridgeAndExecute
to allow passing in a layerZero
refund address.
ETH-Transfer
#0 - c4-pre-sort
2024-01-24T04:37:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T04:38:11Z
raymondfam marked the issue as duplicate of #27
#2 - raymondfam
2024-01-24T04:40:04Z
Same root cause as in #27, albeit with a different impact.
#3 - c4-judge
2024-02-02T16:57:13Z
alex-ppg marked the issue as not a duplicate
#4 - c4-judge
2024-02-02T16:58:48Z
alex-ppg marked the issue as duplicate of #262
#5 - c4-judge
2024-02-02T17:03:30Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2024-02-04T23:04:25Z
alex-ppg changed the severity to 2 (Med Risk)