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: 38/113
Findings: 2
Award: $118.46
🌟 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
An attacker can set his own contract as a router via setRouter. Because mint/burn can only be called by the router, the attacker can mint any DcntEth for himself or burn anyone else's DcntEth. Users can provide WETH to the DecentEthRouter contract via DecentEthRouter.addLiquidityEth/addLiquidityWeth, then attacker can drain all WETH held by DecentEthRouter via redeemWeth.
Consider the following scenario:
bob deploys a contract A.
bob calls A.mintFreeToken(bob, 1000,000 e18)
. The internal process is as follows:
address oldAddress = DcntEth.router(); DcntEth.setRouter(A); DcntEth.mint(bob, 1000,000 e18); DcntEth.setRouter(oldAddress);
alice calls DecentEthRouter.addLiquidityWeth
to deposit 100e18 weth.
bob calls DecentEthRouter.redeemWeth(100e18)
to redeem 100e18 weth.
In this way, bob can steal all weth deposited by any user. And all users will suffer a loss of funds.
Manual Review
File: lib\decent-bridge\src\DcntEth.sol 20:- function setRouter(address _router) public { 20:+ function setRouter(address _router) public onlyOwner { 21: router = _router; 22: }
Access Control
#0 - c4-pre-sort
2024-01-24T07:42:57Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T07:43:03Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:23:46Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: Kaysoft
Also found by: 0xmystery, Aamir, DadeKuma, IceBear, Pechenite, SBSecurity, Shaheen, bronze_pickaxe, ether_sky, nobody2018, rjs, rouhsamad, slvDev, zxriptor
118.3363 USDC - $118.34
If ecrecover(constructedHash, v, r, s) fails, it will return address(0x0), because the signer is not set when the UTBFeeCollector is created. Before the owner calls setSigner, the signer is equal to 0. In this way, the user can use an invalid signature to pay 0 fee.
File: src\UTBFeeCollector.sol 53: address recovered = ecrecover(constructedHash, v, r, s); 54:- require(recovered == signer, "Wrong signature"); 54:+ require(recovered != address(0x0) && recovered == signer, "Wrong signature");
swapAndExecute/bridgeAndExecute all have a signature
parameter, which is used to verify the fee structure. But the signature does not include the deadline
and the caller's nonces
. This is not best practice. Because the fee comes from off-chain (front-end data), if the fee is increased, users can use the old signature to perform operations. Therefore, the signature contains swap information, but if it is a swapNoPath, then using such a signature again will not have any impact on the user. In this way, the protocol will get less fees.
signature
includes the deadline
and the caller's nonces
.
UTBExecutor contract has two execute
functions: first, second. The last parameter of second is extraNative
, which is forwards additional gas or native fees required to execute the payment transaction.
If the owner calls first execute
directly, which calls second execute
internally, setting the extraNative
parameter to 0. This is no problem.
If the owner wants to call second execute
directly with token != 0
, and set extraNative
greater than 0. This is impossible because it have no payable.
File: src\UTBExecutor.sol 41: function execute( 42: address target, 43: address paymentOperator, 44: bytes memory payload, 45: address token, 46: uint amount, 47: address payable refund, 48: uint extraNative 49: ) public onlyOwner {//@@@@@@ this function has no payable ...... 59: uint initBalance = IERC20(token).balanceOf(address(this)); 60: 61: IERC20(token).transferFrom(msg.sender, address(this), amount); 62: IERC20(token).approve(paymentOperator, amount); 63: 64: if (extraNative > 0) { 65:-> (success, ) = target.call{value: extraNative}(payload); 66: if (!success) { 67: (refund.call{value: extraNative}("")); 68: } 69: } else { 70: (success, ) = target.call(payload); 71: } ...... 81: }
Reconstruct the logic of second execute into an internal function, and then let both first execute
and second execute
call this internal function. And add payable
modifier to second execute
.
File: lib\decent-bridge\src\DecentBridgeExecutor.sol 24: function _executeWeth( 25: address from, 26: address target, 27: uint256 amount, 28: bytes memory callPayload 29: ) private { 30: uint256 balanceBefore = weth.balanceOf(address(this)); 31:-> weth.approve(target, amount); 32: 33: (bool success, ) = target.call(callPayload); 34: 35: if (!success) { 36:-> weth.transfer(from, amount); 37: return; 38: } ...... 45: }
Before target.call(callPayload)
is called, weth.approve(target, amount)
is called. Then, if target.call
fails, it means that approve has not been spent. The target still has the allowance of this contract on WETH. If this contract holds WETH, then the target will be able to steal it.
35: if (!success) { 36: weth.transfer(from, amount); +++ weth.approve(target, 0); 37: return; 38: }
File: lib\decent-bridge\src\DecentEthRouter.sol 237: function onOFTReceived( 238: uint16 _srcChainId, 239: bytes calldata, 240: uint64, 241: bytes32, 242: uint _amount, 243: bytes memory _payload 244: ) external override onlyLzApp { ...... 266: if (weth.balanceOf(address(this)) < _amount) { 267:-> dcntEth.transfer(_to, _amount); 268: return; 269: } ...... 282: }
If the amount of weth held by the DecentEthRouter contract is less than _amount
, dcntEth.transfer(_to, _amount)
will be called. This is equivalent to 1e18 weth =1e18 dcntEth
. However, dcntEth.transfer
also transfers the dcntEth held by the DecentEthRouter contract to _to
. If the dcntEth held is less than _amount
, then tx will be revert due to insufficient balance.
This can happen. The whale can deposit WETH to this
via addLiquidityEth/addLiquidityWeth, and mint the same amount of dcntEth to this
. Similarly, he may also remove the amount of WETH and dcntEth of this
contract via removeLiquidityEth
/removeLiquidityWeth
.
When a large amount cross-chain transfer-message is sent from other chain, the whale can notice the tx, then remove liquidity on the target chain, then onOFTReceived
will revert.
File: src\UTB.sol 311: function receiveFromBridge( 312: SwapInstructions memory postBridge, 313: address target, 314: address paymentOperator, 315: bytes memory payload, 316: address payable refund 317: ) public { 318: _swapAndExecute(postBridge, target, paymentOperator, payload, refund); 319: }
From function's name, receiveFromBridge
should only be called by bridge, and we can also see it being called in [1,2].
File: src\UTB.sol 311: function receiveFromBridge( 312: SwapInstructions memory postBridge, 313: address target, 314: address paymentOperator, 315: bytes memory payload, 316: address payable refund 317: ) public { +++ require(bridgeAdapters[IBridgeAdapter(msg.sender).getId()] != address(0x0), "invalid caller"); 318: _swapAndExecute(postBridge, target, paymentOperator, payload, refund); 319: }
Â
#0 - raymondfam
2024-01-26T07:24:55Z
L-03: extraNative has been hardcoded as intended L-04: 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.
4 L
All instances should be git-linked.
#1 - c4-pre-sort
2024-01-26T07:25:00Z
raymondfam marked the issue as sufficient quality report
#2 - alex-ppg
2024-02-04T22:48:46Z
The Warden's QA report has been graded A based on a score of 36
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:48:50Z
alex-ppg marked the issue as grade-a
#4 - alex-ppg
2024-02-04T22:54:38Z
This report is tied with #83 and was not selected as the best because it does not have git links. I advise the Warden to make sure that their submissions are better laid out.
#5 - c4-sponsor
2024-02-13T21:42:17Z
wkantaros (sponsor) confirmed