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: 12/113
Findings: 3
Award: $806.42
🌟 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.0879 USDC - $0.09
setRouter function does not have any access controls, allowing anyone to change router to an arbitrary address and mint unlimited tokens.
mint function is onlyRouter
, which means router (or exploiter address) is able to mint unlimited tokens:
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DcntEth.sol#L24-L26
setRouter should be declared as onlyOwner
:
function setRouter(address _router) public onlyOwner { router = _router; }
Access Control
#0 - c4-pre-sort
2024-01-24T04:18:40Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T04:18:46Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:25:55Z
alex-ppg marked the issue as partial-75
#3 - alex-ppg
2024-02-03T13:26:01Z
Comparative quality of submission is relatively low.
794.0484 USDC - $794.05
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L266-L269 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L250-L255
When receiving a cross-chain message, the onOFTReceived function of DecentRouter is invoked. Now, assuming that this message contains a payload for calling the destination (_to
) , we can reasonably infer that _to
is most likely a smart contract (though it might also be an EOA, but in most cases, it is a smart contract). The issue arises when there is an insufficient amount of WETH
in the DecentEthRouter
. In this scenario, Decent tokens are sent to _to, which is a smart contract. Consequently, there is a high risk that these Decent tokens might be locked indefinitely in the smart contract, leading to potential loss of funds.
loss of funds - The likelihood of encountering this issue is very high, given that users utilize the UTB contract for making cross-chain swaps. in this case if the DecentEthRouter
at destination
lacks a sufficient amount of WETH, Decent tokens are then sent to the DecentBridgeAdapter
in the destination chain, which, crucially, lacks any functions for withdrawing these tokens. This situation poses a significant risk as there is no mechanism in place for retrieving or managing the Decent tokens sent under these circumstances.
In this scenario, I would suggest implementing a check to verify if the message contains a payload. If it does, there is no reason for calling _to
when there isn't enough WETH
in the contract. It would be more secure to redirect the funds to the refund address. Conversely, if the message doesn't contain a payload and _to
is an externally owned account (EOA), it is deemed safe to send Decent tokens to either _to
or the refund
address.
Invalid Validation
#0 - c4-pre-sort
2024-01-24T23:31:58Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T23:32:06Z
raymondfam marked the issue as duplicate of #59
#2 - c4-judge
2024-02-02T15:15:00Z
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
12.2818 USDC - $12.28
https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L285-L291 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L275-L276 https://github.com/decentxyz/decent-bridge/blob/7f90fd4489551b69c20d11eeecb17a3f564afb18/src/DecentEthRouter.sol#L313-L319
High - imho the impact of this issue is should be classified as high due to its substantial likelihood, simplicity in execution (requiring only a single function call), and the potential for substantial financial losses. Users face a considerable risk, as demonstrated by scenarios such as swapping 1 Ethereum for 1 Matic, resulting in significant loss of funds.
In some chains, such as Polygon/AVAX, Ethereum is not recognized as their gas token. This distinction is addressed in the DecentEthRouter
by configuring the gasCurrencyIsEth
variable as either true
or false
. However, some sections of both DecentEthRouter
and DecentBridgeExecutor
(as mentioned below) are mistaking Ethereum as the native chain token without checking gasCurrencyIsEth
.
DecentEthRouter::redeemEth:
When a user wants to redeem their Decent tokens (on Polygon/Avax) using this function, the user's Decent tokens are transferred to the contract, WETH is withdrawn (burned), and native chain tokens (e.g., Matic in the Polygon example) are sent to the user. This implies that if, for example, Alice decides to redeem her 10 dcntEth tokens, she will receive 10 Matic tokens, which are worth only 7 USD, resulting in the loss of all her 10 WETH (worth 22,000 usd)
DecentBridgeExecutor::_executeETH:
Also in this function, WETH is withdrawn and Matic/Avax are received which might be useless when calling the target.
this function results in loss of funds for user (from
) and stuck funds in DecentBridgeExecutor
which can be withdrawn by a malicious actor.
NOTE: there might not be enough MATIC/AVAX in router in order to be sent to user, however, a malicious actor is able to fund contract with enough MATIC/AVAX.
Code PoC:
while the issue is clear, i have decided to put a code PoC (Copy from next line to before Tools Used
and paste in a new file, no additional settings needed):
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test, console, console2} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol";
interface IWETH { function withdraw(uint amt) external payable;
function deposit() external payable; function balanceOf(address user) external view returns (uint);
}
contract MockRouter is Test { IWETH polygon_weth = IWETH(0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619); IWETH wrappedMatic = IWETH(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270);
ERC20Mock dcntToken; address alice; modifier onlyIfWeHaveEnoughReserves(uint256 amount) { require( polygon_weth.balanceOf(address(this)) > amount, "not enough reserves" ); _; } function setUp() public { vm.createSelectFork("https://polygon.meowrpc.com"); alice = makeAddr("Alice"); dcntToken = new ERC20Mock(); dcntToken.mint(alice, 10 ether); //transfer WETH to router vm.startPrank(0x62ac55b745F9B08F1a81DCbbE630277095Cf4Be1); IERC20(address(polygon_weth)).transfer(address(this), 11 ether); vm.stopPrank(); } function redeemEth( uint256 amount ) public onlyIfWeHaveEnoughReserves(amount) { dcntToken.transferFrom(msg.sender, address(this), amount); polygon_weth.withdraw(amount); payable(msg.sender).transfer(amount); } function testWithdrawWETH() public { //alice deciding to withdraw his 10 Dcnt tokens for ether assertEq(dcntToken.balanceOf(alice), 10 ether); assertEq(alice.balance, 0); //no matics assertEq(polygon_weth.balanceOf(alice), 0); //no WETH assertEq(wrappedMatic.balanceOf(alice), 0); //alice doesn't have any MATIC vm.startPrank(alice); dcntToken.approve(address(this), 10 ether); this.redeemEth(10 ether); wrappedMatic.deposit{value: alice.balance}(); assertEq(dcntToken.balanceOf(alice), 0);//tokens sent to the contract assertEq(alice.balance, 0); //no matics assertEq(polygon_weth.balanceOf(alice), 0); //no WETH assertEq(wrappedMatic.balanceOf(alice), 10 ether); //alice has 10 matics instead of WETH vm.stopPrank(); } receive() external payable {}
}
Manual review Forge testing
While I don't have a clear solution in mind yet, I would suggest first checking gasCurrencyIsEth
to determine whether the chain supports ETH as the gas token or not. If it does not (false), then proceed to swap WETH for the native chain token (e.g., WETH => MATIC) and send MATIC
tokens to the user. Alternatively, consider not allowing certain functions to be executed if the chain does not support ETH as the native gas token.
Invalid Validation
#0 - c4-pre-sort
2024-01-25T00:45:11Z
raymondfam marked the issue as sufficient quality report
#1 - raymondfam
2024-01-25T00:45:37Z
Would indeed be a serious issue if the vulnerability is valid.
#2 - c4-pre-sort
2024-01-25T00:45:40Z
raymondfam marked the issue as primary issue
#3 - c4-sponsor
2024-01-31T16:01:10Z
wkantaros (sponsor) confirmed
#4 - alex-ppg
2024-02-01T13:53:19Z
The Warden demonstrates a discrepancy in the system concerning the weth
token and how it is treated in the overall system.
I investigated the overall deployment system of the Sponsor and indeed validated that the DecentEthRouter
is deployed with the WETH
address rather than a wrapped native asset variant (i.e. wMATIC
for Polygon).
The problem lies in the actual exploitability of the function paths defined. The entire system would not really behave as the Warden details.
Specifically, DecentEthRouter::redeemEth
mandates that the contract has a sufficient WETH
balance. Router deployments that are made outside the Ethereum network will have a 0
balance and thus the user will not be able to redeem their Decent ETH (unless someone sent WETH
to the contract). This idea is flawed, as its pre-conditions are:
DecentEthRouter::redeemEth
on a non-Ethereum network by the victimIn relation to the DecentBridgeExecutor
, the code will "never" execute in non-Ethereum chains. This function is invoked solely by DecentEthRouter::onOFTReceived
which would evaluate its own weth
balance, detect that it does not have a sufficient one, and simply transfer the credited DcntEth
to the intended _to
recipient.
While overall flawed, the described execution paths behave as expected and cannot be exploited to force fund loss for the system maliciously. Cross-chain transfers of the DcntEth
token will execute "correctly" as the DecentEthRouter::onOFTReceived
function will end early and simply transfer the DcntEth
to their intended recipient.
As a result of all this, I consider the submission invalid but the Sponsor should definitely re-evaluate how they have approached multi-chain deployments of their system as they are presently illegible and permit reverting functionality that should be inaccessible due to a more descriptive error message. I also invite the Sponsor to re-evaluate and whether they have detected a valid exploitation path based on the data supplied by the Warden.
#5 - c4-judge
2024-02-01T13:53:47Z
alex-ppg marked the issue as unsatisfactory: Invalid
#6 - alex-ppg
2024-02-02T09:26:35Z
After discussions with the Sponsor, I stand by my original invalidation judgment due to the impossibility of exploiting the vulnerability. To note, for a user to be attacked the attacker would have to sacrifice an equal value of the funds they wish to damage the user of. This is not acceptable as a vulnerability of HM risk but rather a QA (L) issue of code misbehavior that should be prevented. Based on this, I will proceed with downgrading the submission to QA (L) as it details a valid problem in the code concerning DecentEthRouter::onOFTReceived
.
#7 - c4-judge
2024-02-02T09:26:42Z
alex-ppg removed the grade
#8 - c4-judge
2024-02-02T09:26:59Z
alex-ppg changed the severity to QA (Quality Assurance)
#9 - alex-ppg
2024-02-04T22:59:38Z
I will award this submission a B
grade as it goes into great detail about a potentially valid issue in the code. The reason this does not constitute a medium risk vulnerability is out of "luck" due to how the system is laid out, and the submission goes into great detail about all functions affected as well as their potential impact.
#10 - c4-judge
2024-02-04T22:59:42Z
alex-ppg marked the issue as grade-b