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: 20/147
Findings: 2
Award: $297.65
🌟 Selected for report: 1
🚀 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#L217-L238 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol#L267-L269 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L297-L315 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L284-L295
Users assigned the FULL_RESTRICTED_STAKER_ROLE
, intended to be restricted from certain actions, can bypass these restrictions. They can use the _spendAllowance
function (as inherited from ERC20.sol via ERC4626.sol) to grant allowances, potentially leading to unintended asset transfers, undermining the security measures put in place, and eroding trust in the system.
Evidently, StakedUSDeV2.cooldownAssets
and StakedUSDeV2.cooldownShares
that has been commented below:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L94 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L110
/// @param owner address to redeem and start cooldown, owner must allowed caller to perform this action
facilitates an owner of stUSDe
to allow a caller to invoke StakedUSDe._withdraw
as _msgSender()
:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L103 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L119
_withdraw(_msgSender(), address(silo), owner, assets, shares);
Here's the scenario:
FULL_RESTRICTED_STAKER_ROLE
to a user after they've staked their assets._approve()
to grant a full allowance to another address.function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { if (owner == address(0)) { revert ERC20InvalidApprover(address(0)); } if (spender == address(0)) { revert ERC20InvalidSpender(address(0)); } _allowances[owner][spender] = value; if (emitEvent) { emit Approval(owner, spender, value); } }
super._withdraw()
is invoked from StakedUSDe._withdraw()
.https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L225-L238
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(); }
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); }
Note: This finding (being flawed at the code logic) is distinctly different from L-01 (merely touching on dodging through frontrunning) on Pashov's audit report.
Manual
Refactor the check that will also have the owner checked for the role:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L232-L233
- if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { + if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, owner) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed();
Access Control
#0 - c4-pre-sort
2023-10-31T16:10:29Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T16:10:37Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:14Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:33:57Z
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-
178.5137 USDC - $178.51
When intending to emit both the old and new values, there isn't a need to cache the old value that will only be used once. Simply emit both values before assigning a new value to the state variable. For example, the following setter function may be refactored as follows:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L436-L440
function _setMaxMintPerBlock(uint256 _maxMintPerBlock) internal { + emit MaxMintPerBlockChanged(maxMintPerBlock, _maxMintPerBlock); - uint256 oldMaxMintPerBlock = maxMintPerBlock; maxMintPerBlock = _maxMintPerBlock; - emit MaxMintPerBlockChanged(oldMaxMintPerBlock, maxMintPerBlock); }
All other instances entailed:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L442-L447
/// @notice Sets the max redeemPerBlock limit function _setMaxRedeemPerBlock(uint256 _maxRedeemPerBlock) internal { uint256 oldMaxRedeemPerBlock = maxRedeemPerBlock; maxRedeemPerBlock = _maxRedeemPerBlock; emit MaxRedeemPerBlockChanged(oldMaxRedeemPerBlock, maxRedeemPerBlock); }
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L126-L134
function setCooldownDuration(uint24 duration) external onlyRole(DEFAULT_ADMIN_ROLE) { if (duration > MAX_COOLDOWN_DURATION) { revert InvalidCooldown(); } uint24 previousDuration = cooldownDuration; cooldownDuration = duration; emit CooldownDurationUpdated(previousDuration, cooldownDuration); }
By convention, it's recommended emitting the old value followed by the new one in the same event instead of the other way round.
Here's one instance found:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/USDe.sol#L23-L26
function setMinter(address newMinter) external onlyOwner { - emit MinterUpdated(newMinter, minter); + emit MinterUpdated(minter, newMinter); minter = newMinter; }
Users should be cautioned about the impermanent losses entailed arising from the delta-neutral stability strategy adopted by the protocol, specifically if the short positions were to encounter hefty losses. Apparently, the users could have held on to their collateral, e.g. stETH or WETH
, and ended up a lot richer with the equivalent amount of USDe
. I suggest all minting entries to begin with stable coins like USDC, DAI etc
that could be converted to stETH
to generate yield if need be instead of having users depositing stETH
from their wallet reserves. Psychologically, this will make the users feel better as the mentality has been fostered more on preserving the 1:1 peg of USDe
at all times.
As indicated on the contest description, users intending to mint/redeem a large amount will need to mint/redeem over several blocks due to maxMintPerBlock
or maxRedeemPerBlock
. However, these RFQ's are prone to DoS because mintedPerBlock[block.number] + mintAmount > maxMintPerBlock
or redeemedPerBlock[block.number] + redeemAmount > maxRedeemPerBlock
could revert by only 1 wei in excess.
While these issues could be sorted by the backend to make a full use of maxMintPerBlock
or maxRedeemPerBlock
per block, it will make the intended logic a lot more efficient by auto reducing the RFQ amount to perfectly fill up the remaining quota for the current block. Better yet, set up a queue system where request amount running in hundreds of thousands or millions may be auto split up with multiple orders via only one signature for batching.
In the function below, the if block already dictates that getUnvestedAmount() == 0
manages to avoid a revert. Hence, consider refactoring the following code lines as it makes no sense adding 0 value getUnvestedAmount()
of to the addend, amount
:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L89-L99
function transferInRewards(uint256 amount) external nonReentrant onlyRole(REWARDER_ROLE) notZero(amount) { if (getUnvestedAmount() > 0) revert StillVesting(); - uint256 newVestingAmount = amount + getUnvestedAmount(); - vestingAmount = newVestingAmount; + vestingAmount = amount; // The rest of the codes }
Under the context of the above/preceding recommendation, transferInRewards()
should also have its emit
refactored below. Otherwise, you are practically emitting two identical values that defeat the purpose of contrasting the old and the values.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L89-L99
function transferInRewards(uint256 amount) external nonReentrant onlyRole(REWARDER_ROLE) notZero(amount) { if (getUnvestedAmount() > 0) revert StillVesting(); - uint256 newVestingAmount = amount + getUnvestedAmount(); - vestingAmount = newVestingAmount; + emit RewardsReceived(vestingAmount, amount); + vestingAmount = amount; lastDistributionTimestamp = block.timestamp; // transfer assets from rewarder to this contract IERC20(asset()).safeTransferFrom(msg.sender, address(this), amount); - emit RewardsReceived(amount, newVestingAmount); }
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L94 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L110
- /// @param owner address to redeem and start cooldown, owner must allowed caller to perform this action + /// @param owner address to redeem and start cooldown, owner must allow caller to perform this action
The function below is meant to be used only for minting. Hence, redeeming has got nothing to do with this view function. Consider refactoring the first if block so mint()
could revert earlier if need be:
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L351-L374
function verifyRoute(Route calldata route, OrderType orderType) public view override returns (bool) { // routes only used to mint - if (orderType == OrderType.REDEEM) { - return true; - } + if (orderType =! OrderType.MINT) { + 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; }
Per Pashov's audit report, L-04 mentioned that the unused EthenaMinting::encodeRoute
has been removed by Ethena. However, this erroneous function still exists in EthenaMinting.sol.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L334-L336
- function encodeRoute(Route calldata route) public pure returns (bytes memory) { - return abi.encode(ROUTE_TYPE, route.addresses, route.ratios); - }
Consider removing this controversial function where possible.
#0 - c4-pre-sort
2023-11-02T02:15:19Z
raymondfam marked the issue as high quality report
#1 - c4-sponsor
2023-11-09T16:44:13Z
FJ-Riveros (sponsor) acknowledged
#2 - c4-judge
2023-11-14T17:07:29Z
fatherGoose1 marked the issue as grade-a
#3 - c4-judge
2023-11-14T17:24:41Z
fatherGoose1 marked the issue as selected for report
#4 - liveactionllama
2023-11-27T22:29:14Z
Per discussion with the judge (@fatherGoose1), all findings within this submission are valid and will be included in the audit report.