Platform: Code4rena
Start Date: 24/10/2023
Pot Size: $36,500 USDC
Total HM: 4
Participants: 147
Period: 6 days
Judge: 0xDjango
Id: 299
League: ETH
Rank: 22/147
Findings: 3
Award: $285.46
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: josephdara
Also found by: 0xAadi, 0xmystery, 0xpiken, Arz, Beosin, Eeyore, HChang26, J4X, KIntern_NA, Limbooo, RamenPeople, SpicyMeatball, Team_Rocket, Yanchuan, castle_chain, degensec, ge6a, lanrebayode77, mert_eren, sorrynotsorry, tnquanghuy0512
119.1406 USDC - $119.14
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L225 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L245 https://github.com/code-423n4/2023-10-ethena/blob/main/lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol#L230 https://github.com/code-423n4/2023-10-ethena/blob/main/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol#L277 https://github.com/code-423n4/2023-10-ethena/blob/main/lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol#L222
FULL_RESTRICTED_STAKER_ROLE
address can exploit a vulnerability that enables them to bypass checks and execute the _withdraw()
function, allowing them to withdraw funds when they should be restricted from doing so.
The protocol has the ability to freeze funds for individuals with the FULL_RESTRICTED_STAKER_ROLE
.
FULL_RESTRCITED_STAKER_ROLE is for sanction/stolen funds, or if we get a request from law enforcement to freeze funds. Addresses fully restricted cannot move their funds, and only Ethena can unfreeze the address. Ethena also have the ability to repossess funds of an address fully restricted
However, the current implementation allows addresses with the FULL_RESTRICTED_STAKER_ROLE
to bypass the freezing measures and still withdraw funds. The withdraw()
function invokes _withdraw()
, which, in the existing code, only checks whether the caller
and the receiver
have the FULL_RESTRICTED_STAKER_ROLE
. More importantly, it doesn't check if the owner holds this role.
function _withdraw(address caller, address receiver, address _owner, uint256 assets, uint256 shares) internal override nonReentrant notZero(assets) notZero(shares) { if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); } super._withdraw(caller, receiver, _owner, assets, shares); _checkMinShares(); }
This issue becomes evident when the owner is FULL_RESTRICTED_STAKER_ROLE
. In this scenario, the owner can easily circumvent the restrictions by approving an allowance to a new account that doesn't have the FULL_RESTRICTED_STAKER_ROLE
. This new account can then execute the withdraw()
function on behalf of the owner
. Furthermore, another account can be created to receive all the funds that are supposed to be "frozen."
The protocol also has security measures in place through the _beforeTokenTransfer()
function. This function is executed before any transfer of tokens, including minting and burning, and acts as the final security check for FULL_RESTRICTED_STAKER_ROLE
.
function _beforeTokenTransfer(address from, address to, uint256) internal virtual override { if (hasRole(FULL_RESTRICTED_STAKER_ROLE, from) && to != address(0)) { revert OperationNotAllowed(); } if (hasRole(FULL_RESTRICTED_STAKER_ROLE, to)) { revert OperationNotAllowed(); } }
However, these checks are not sufficient to prevent the owner from bypassing the restrictions, as the owner's shares are still burnt and sent to address(0) as shown in super._withdraw()
. _beforeTokenTransfer()
allows transfer from FULL_RESTRICTED_STAKER_ROLE
to address(0)
.
function _withdraw( address caller, address receiver, address owner, uint256 assets, uint256 shares ) internal virtual { if (caller != owner) { _spendAllowance(owner, caller, shares); } _burn(owner, shares); SafeERC20.safeTransfer(_asset, receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); }
Additionally, the newly created receiver
account is also capable of circumventing the checks implemented in _transfer()
as it has no restriction from receiving funds.
function _transfer(address from, address to, uint256 amount) internal virtual { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address"); _beforeTokenTransfer(from, to, amount); uint256 fromBalance = _balances[from]; require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); unchecked { _balances[from] = fromBalance - amount; _balances[to] += amount; } emit Transfer(from, to, amount); _afterTokenTransfer(from, to, amount); }
Manual Review
function _withdraw(address caller, address receiver, address _owner, uint256 assets, uint256 shares) internal override nonReentrant notZero(assets) notZero(shares) { - if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { + if (hasRole(FULL_RESTRICTED_STAKER_ROLE, owner) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); } super._withdraw(caller, receiver, _owner, assets, shares); _checkMinShares(); }
Access Control
#0 - c4-pre-sort
2023-10-31T05:30:47Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T05:30:57Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:03Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:32:21Z
fatherGoose1 marked the issue as satisfactory
161.7958 USDC - $161.80
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L225
Addresses with SOFT_RESTRICTED_STAKER_ROLE
can withdraw()
stUSDe.
As per the documentation, the role SOFT_RESTRICTED_STAKER_ROLE
is not supposed to have the ability to deposit USDe to obtain stUSDe or withdraw stUSDe in exchange for USDe.
Due to legal requirements, there's a SOFT_RESTRICTED_STAKER_ROLE and FULL_RESTRICTED_STAKER_ROLE. The former is for addresses based in countries we are not allowed to provide yield to, for example USA. Addresses under this category will be soft restricted. They cannot deposit USDe to get stUSDe or withdraw stUSDe for USDe. However they can participate in earning yield by buying and selling stUSDe on the open market.
The issue arises because the _withdraw()
function does not adequately check for the SOFT_RESTRICTED_STAKER_ROLE
. Instead, it only checks for the FULL_RESTRICTED_STAKER_ROLE
. This oversight allows addresses with SOFT_RESTRICTED_STAKER_ROLE
authorization, which should not be able to receive yield, to improperly withdraw stUSDe. User can buy stUSDe in open market and _withdraw()
USDe for yields. This unintended behavior could lead to non-compliance with legal requirements.
function _withdraw(address caller, address receiver, address _owner, uint256 assets, uint256 shares) internal override nonReentrant notZero(assets) notZero(shares) { if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); } super._withdraw(caller, receiver, _owner, assets, shares); _checkMinShares(); }
Manual Review
function _withdraw(address caller, address receiver, address _owner, uint256 assets, uint256 shares) internal override nonReentrant notZero(assets) notZero(shares) { - if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { + if (hasRole(SOFT_RESTRICTED_STAKER_ROLE, caller) || hasRole(SOFT_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); } super._withdraw(caller, receiver, _owner, assets, shares); _checkMinShares(); }
Invalid Validation
#0 - c4-pre-sort
2023-10-31T05:20:47Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-10-31T05:21:12Z
raymondfam marked the issue as duplicate of #110
#2 - c4-judge
2023-11-13T19:41:23Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-11-27T21:24:45Z
fatherGoose1 marked the issue as not a duplicate
#4 - c4-judge
2023-11-27T21:24:56Z
fatherGoose1 marked the issue as duplicate of #52
#5 - Henrychang26
2023-11-27T23:51:12Z
@fatherGoose1
Should I be concerned this is a dupe of satisfactory issue but this specific submission is marked as unsatisfactory? Thank you!
#6 - fatherGoose1
2023-11-27T23:57:33Z
@fatherGoose1
Should I be concerned this is a dupe of satisfactory issue but this specific submission is marked as unsatisfactory?
Thank you!
Thank you for flagging. I will look into it.
#7 - c4-judge
2023-11-28T01:18:25Z
fatherGoose1 marked the issue as satisfactory
🌟 Selected for report: 0xmystery
Also found by: 0x11singh99, 0xAadi, 0xAlix2, 0xG0P1, 0xStalin, 0xWaitress, 0x_Scar, 0xhacksmithh, 0xhunter, 0xpiken, Al-Qa-qa, Arz, Avci, Bauchibred, BeliSesir, Breeje, Bughunter101, DarkTower, Eeyore, Fitro, HChang26, Imlazy0ne, J4X, JCK, Kaysoft, Kral01, Madalad, Mike_Bello90, Noro, PASCAL, PENGUN, Proxy, Rickard, Shubham, SovaSlava, Strausses, Team_Rocket, ThreeSigma, Topmark, Udsen, Walter, Yanchuan, Zach_166, ZanyBonzy, adam-idarrha, adeolu, almurhasan, arjun16, ast3ros, asui, ayden, btk, cartlex_, castle_chain, cccz, chainsnake, codynhat, critical-or-high, cryptonue, csanuragjain, deepkin, degensec, dirk_y, erebus, foxb868, ge6a, hunter_w3b, jasonxiale, kkkmmmsk, lanrebayode77, lsaudit, marchev, matrix_0wl, max10afternoon, nuthan2x, oakcobalt, oxchsyston, pavankv, peanuts, pep7siup, pipidu83, pontifex, ptsanev, qpzm, radev_sw, rokinot, rotcivegaf, rvierdiiev, sorrynotsorry, squeaky_cactus, supersizer0x, tnquanghuy0512, twcctop, twicek, young, zhaojie, ziyou-
4.5226 USDC - $4.52
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L351
The verifyRoute()
function yields incorrect results, possibly leading to unexpected errors.
The verifyRoute()
function is specifically utilized in the mint()
process, where it confirms the validity of a route object based on its type. This function examines whether the route ratios add up to 100% and whether all input custodian addresses are supported. However, there is an issue in its implementation, as it erroneously returns true
when orderType == OrderType.REDEEM
.
function verifyRoute(Route calldata route, OrderType orderType) public view override returns (bool) { // routes only used to mint >if (orderType == OrderType.REDEEM) { return true; } uint256 totalRatio = 0; if (route.addresses.length != route.ratios.length) { return false; } if (route.addresses.length == 0) { return false; } for (uint256 i = 0; i < route.addresses.length; ++i) { if (!_custodianAddresses.contains(route.addresses[i]) || route.addresses[i] == address(0) || route.ratios[i] == 0) { return false; } totalRatio += route.ratios[i]; } if (totalRatio != 10_000) { return false; } return true; }
It's essential to note that although this issue exists within a view function and additional checks are present in mint()
to safeguard against erroneous transactions, the verifyRoute()
function is likely employed in the frontend when users sign transactions, particularly when they are selecting routes. This could raise concerns, particularly given the operation of the mint()
process in this specific protocol. The problem could potentially lead to unanticipated errors and unnecessary gas expenditure.
Ethena provides an RFQ of how much USDe can be minted for a stETH amount. If user agrees to price, user signs the EIP712 signature and submits it to Ethena. stETH is traded for newly minted USDe at the rate specified by Ethena's RFQ and user's signature.
Manual Review
function verifyRoute(Route calldata route, OrderType orderType) public view override returns (bool) { // routes only used to mint if (orderType == OrderType.REDEEM) { - return true; + return false; } uint256 totalRatio = 0; if (route.addresses.length != route.ratios.length) { return false; } if (route.addresses.length == 0) { return false; } for (uint256 i = 0; i < route.addresses.length; ++i) { if (!_custodianAddresses.contains(route.addresses[i]) || route.addresses[i] == address(0) || route.ratios[i] == 0) { return false; } totalRatio += route.ratios[i]; } if (totalRatio != 10_000) { return false; } return true; }
Context
#0 - c4-pre-sort
2023-10-31T16:46:01Z
raymondfam marked the issue as duplicate of #36
#1 - c4-pre-sort
2023-10-31T16:46:06Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2023-11-13T19:18:21Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-11-20T20:13:19Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-11-20T20:15:27Z
fatherGoose1 marked the issue as grade-b