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: 39/113
Findings: 3
Award: $117.32
🌟 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
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DcntEth.sol#L20-L22 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DcntEth.sol#L24-L26 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L285-L291
The setRouter
function exists in DcntEth
.
Anyone can set any route because this function lacks a modifier.
Consequently, a user can create DcntEth
tokens as his wish and redeem ETH
for them through DecentEthRouter
.
route
.function setRouter(address _router) public { router = _router; }
DcntEth
tokens using his router
.function mint(address _to, uint256 _amount) public onlyRouter { _mint(_to, _amount); }
ETH
from DecentEthRouter
.function redeemEth( uint256 amount ) public onlyIfWeHaveEnoughReserves(amount) { dcntEth.transferFrom(msg.sender, address(this), amount); weth.withdraw(amount); payable(msg.sender).transfer(amount); }
And the incorrect route
can lead to numerous unexpected problems.
- function setRouter(address _router) public { + function setRouter(address _router) public onlyOwner { router = _router; }
Access Control
#0 - c4-pre-sort
2024-01-24T03:54:53Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T03:55:00Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:26:34Z
alex-ppg marked the issue as satisfactory
104.9182 USDC - $104.92
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L273 https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L289 https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/bridge_adapters/DecentBridgeAdapter.sol#L117 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L161 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L103-L109 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L185 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L245 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L63 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L36
When users attempt to swap tokens using this protocol, they also specify a refund
who can receive the excess funds.
However, in the event of a failed external call, the tokens are transferred to the Bridge Adapter
on that chain, and there is no way to recover them.
Chain 1
for a token on Chain 2
can invoke the bridgeAndExecute
function in UTB
.
This function calls the callBridge
function within the same contract.function bridgeAndExecute() { ... return callBridge(amt2Bridge, fees.bridgeFee, updatedInstructions); // <== here }
The callBridge
function calls the bridge
function in the DecentBridgeAdapter
contract.
function callBridge() { return IBridgeAdapter(bridgeAdapters[instructions.bridgeId]).bridge{ // <== here value: bridgeFee + (native ? amt2Bridge : 0) }( amt2Bridge, instructions.postBridge, instructions.dstChainId, instructions.target, instructions.paymentOperator, instructions.payload, instructions.additionalArgs, instructions.refund // <== We insert refund as a parameter to the adapter. ); }
bridge
function calls the bridgeWithPayload
function in the DecentEthRouter
contract.function bridge() { router.bridgeWithPayload{value: msg.value}(...); // <== here }
_bridgeWithPayload
function, we call the _getCallParams
function.function _bridgeWithPayload() { ( bytes32 destinationBridge, bytes memory adapterParams, bytes memory payload ) = _getCallParams(...); // <== here }
In the _getCallParams
function, the payload
to transfer to Chain 2
will be
msgType, msg.sender, _to, deliverEth, additionalPayload
.
Here, the msg.sender
is the Bridge Adapter
that calls this function.
function _getCallParams() private view returns (bytes32 destBridge, bytes memory adapterParams, bytes memory payload) { if (msgType == MT_ETH_TRANSFER) { payload = abi.encode(msgType, msg.sender, _toAddress, deliverEth); } else { payload = abi.encode( msgType, msg.sender, // <== msg.sender is BridgeAdapter _toAddress, deliverEth, additionalPayload ); } }
payload
using LayerZero
.function _bridgeWithPayload() { ( bytes32 destinationBridge, bytes memory adapterParams, bytes memory payload ) = _getCallParams(...); dcntEth.sendAndCall{value: gasValue}( // <== here address(this), _dstChainId, destinationBridge, _amount, payload, // <== here _dstGasForCall, callParams ); }
Chain 2
, the onOFTReceived
function in the DecentEthRouter
contract will be invoked.
Here, the _payload
is exactly what the user sent on Chain 1
.
So, the _from
will refer to the Bridge Adapter
.function onOFTReceived( uint16 _srcChainId, bytes calldata, uint64, bytes32, uint _amount, bytes memory _payload ) external override onlyLzApp { (uint8 msgType, address _from, address _to, bool deliverEth) = abi // <== _from is BridgeAdapter .decode(_payload, (uint8, address, address, bool)); if (msgType == MT_ETH_TRANSFER) { } else { weth.approve(address(executor), _amount); executor.execute(_from, _to, deliverEth, _amount, callPayload); } }
execute
function in the DecentBridgeExecutor
contract.
If the external call to the target contract fails, we send the tokens to the _from
, i.e., the Bridge Adapter
.function _executeEth( address from, // <== from is BridgeAdapter address target, uint256 amount, bytes memory callPayload ) private { weth.withdraw(amount); (bool success, ) = target.call{value: amount}(callPayload); if (!success) { payable(from).transfer(amount); // <== here } }
There may be several cases where the call fails. For instance, the user could set incorrect swap parameters or call another contract not part of this protocol, etc.
function _executeWeth( address from, // <== from is BridgeAdapter 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); // <== here return; } uint256 remainingAfterCall = amount - (balanceBefore - weth.balanceOf(address(this))); weth.transfer(from, remainingAfterCall); // <== here }
Set the refund
address instead of the Bridge Adapter
.
Error
#0 - c4-pre-sort
2024-01-24T04:43:13Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T04:44:02Z
raymondfam marked the issue as duplicate of #27
#2 - c4-judge
2024-02-02T17:27:10Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2024-02-04T23:04:02Z
alex-ppg changed the severity to 3 (High Risk)
🌟 Selected for report: Kaysoft
Also found by: 0xmystery, Aamir, DadeKuma, IceBear, Pechenite, SBSecurity, Shaheen, bronze_pickaxe, ether_sky, nobody2018, rjs, rouhsamad, slvDev, zxriptor
12.2818 USDC - $12.28
[L-1] There is no check between gasIsEth
and bridgeToken
.
There is a strict relationship between gasIsEth
and bridgeToken
.
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/bridge_adapters/DecentBridgeAdapter.sol#L21-L24
constructor(bool _gasIsEth, address _bridgeToken) BaseAdapter() { gasIsEth = _gasIsEth; bridgeToken = _bridgeToken; }
If the bridge token
is set, the user should transfer this bridge token
.
If not, the user should send ETH
.
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L216
function approveAndCheckIfNative( BridgeInstructions memory instructions, uint256 amt2Bridge ) private returns (bool) { IBridgeAdapter bridgeAdapter = IBridgeAdapter(bridgeAdapters[instructions.bridgeId]); address bridgeToken = bridgeAdapter.getBridgeToken( instructions.additionalArgs ); if (bridgeToken != address(0)) { IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge); return false; } return true; }
In the Bridge Adapter
, we assume that the adapter
receives the bridge token
when gasIsEth
is false
and receives ETH
when gasIsEth
is true
.
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/bridge_adapters/DecentBridgeAdapter.sol#L108-L115
function bridge() { if (!gasIsEth) { IERC20(bridgeToken).transferFrom( msg.sender, address(this), amt2Bridge ); IERC20(bridgeToken).approve(address(router), amt2Bridge); } }
Therefore, the following relationship should be satisfied.
bridgeToken = weth if gasIsEth is false, 0 if gasIsEth is true
And gasIsEth
should be the same as gasCurrencyIsEth
in DecentEthRouter
.
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L22
contract DecentEthRouter is IDecentEthRouter, IOFTReceiverV2, Owned { bool public gasCurrencyIsEth; }
[L-2] The transaction can fail when there is not enough DcntEth
tokens in the DecentEthRouter
contract.
When users attempt to swap tokens, they send WETH
tokens to the router
.
The router
receives WETH
tokens and removes an equivalent amount of DcntEth
tokens.
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L177-L193
function _bridgeWithPayload ( if (gasCurrencyIsEth) { weth.deposit{value: _amount}(); gasValue = msg.value - _amount; } else { weth.transferFrom(msg.sender, address(this), _amount); gasValue = msg.value; } dcntEth.sendAndCall{value: gasValue}( address(this), // from address that has dcntEth (so DecentRouter) _dstChainId, destinationBridge, // toAddress _amount, // amount payload, //payload (will have recipients address) _dstGasForCall, // dstGasForCall callParams // refundAddress, zroPaymentAddress, adapterParams ); }
So, if the router
does not have enough DcntEth
tokens, the transaction can fail.
While anyone can deposit DcntEth
to the router
or the owner
can mint tokens to the router
, for safety, we should add the following.
function bridge() { + if (dcntEth.balanceOf(address(this)) < _amount) { + dcntEth.mint(address(this), _amount); + } }
This is possible because the router
has the ability to mint DcntEth
tokens.
[L-3] Users can use the protocol without paying fees.
When users attempt to swap tokens using swapAndExecute
or bridgeAndExecute
, they should pay protocol fees.
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L115
function swapAndExecute( SwapAndExecuteInstructions calldata instructions, FeeStructure calldata fees, bytes calldata signature ) public payable retrieveAndCollectFees(fees, abi.encode(instructions, fees), signature) { }
However, users can also swap tokens using the receiveFromBridge
function, and they are not required to pay protocol fees.
https://github.com/code-423n4/2024-01-decent/blob/07ef78215e3d246d47a410651906287c6acec3ef/src/UTB.sol#L311-L319
function receiveFromBridge( SwapInstructions memory postBridge, address target, address paymentOperator, bytes memory payload, address payable refund ) public { _swapAndExecute(postBridge, target, paymentOperator, payload, refund); }
[L-4] Missing payable
The execute
function in UTBExecuto
r is not marked as payable
.
https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/UTBExecutor.sol#L49
function execute( address target, address paymentOperator, bytes memory payload, address token, uint amount, address payable refund, uint extraNative ) public onlyOwner { }
[L-5] Unable to invoke the receiveFromBridge
function from DecentBridgeExecutor
properly.
If gasCurrencyIsEth
and deliverEth
are both true
in the execute
function, the _executeEth
function is called.
In this function, we perform a withdrawal of WETH
and send ETH
to the Bridge Adapter
.
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentBridgeExecutor.sol#L61
function _executeEth() { weth.withdraw(amount); (bool success, ) = target.call{value: amount}(callPayload); }
However, in the receiveFromBridge
function, we only accept ERC20
tokens.
https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/bridge_adapters/DecentBridgeAdapter.sol#L139-L143
function receiveFromBridge() { IERC20(swapParams.tokenIn).transferFrom( msg.sender, address(this), swapParams.amountIn ); }
[L-6] There's a possibility that some funds may become trapped in the Bridge Adapter
.
In the _bridgeWithPayload
function, we set the Bridge Adapter
as the refund
recipient.
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L171
function _bridgeWithPayload() { ICommonOFT.LzCallParams memory callParams = ICommonOFT.LzCallParams({ refundAddress: payable(msg.sender), zroPaymentAddress: address(0x0), adapterParams: adapterParams }); }
Consequently, certain funds will be accumulated as fees within the Bridge Adapter
.
There should be no remaining funds left in the Bridge Adapter in any case.
#0 - raymondfam
2024-01-26T07:32:09Z
[L-6] to #27
[L-4]: The calling payable function has had extraNative hardcoded to 0 as intended.
5 L
Report format should be improved.
#1 - c4-pre-sort
2024-01-26T07:32:13Z
raymondfam marked the issue as sufficient quality report
#2 - alex-ppg
2024-02-04T22:49:11Z
The Warden's QA report has been graded B based on a score of 23
combined with a manual review per the relevant QA guideline document located here.
The Warden's submission's score was assessed based on the following accepted findings:
#3 - c4-judge
2024-02-04T22:49:14Z
alex-ppg marked the issue as grade-b