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: 15/147
Findings: 2
Award: $524.94
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ayden
Also found by: 0xWaitress, Madalad, Yanchuan, cartlex_, ciphermarco, critical-or-high, d3e4, mert_eren, peanuts, pontifex, trachev, twcctop
520.4229 USDC - $520.42
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L190-L194 https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L138-L141
The StakedUSDe
contract can be accidentally blocked if the all shares will be redeemed before the VESTING_PERIOD
end. The _checkMinShares
function will then revert for any eligible deposits. The same result will be in case of asset transferring to the contract while the totalSupply()
equals zero.
There is the _checkMinShares
check at the _deposit
and _withdraw
functions which should prevent a donation attack.
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override nonReentrant notZero(assets) notZero(shares) { if (hasRole(SOFT_RESTRICTED_STAKER_ROLE, caller) || hasRole(SOFT_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); } super._deposit(caller, receiver, assets, shares); _checkMinShares(); } 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 check can not prevent case then accidental amount of the assets on the contract which was left after all shares were redeemed. It can easily happen if shares were redeemed before the VESTING_PERIOD
end.
function totalAssets() public view override returns (uint256) { return IERC20(asset()).balanceOf(address(this)) - getUnvestedAmount(); } function getUnvestedAmount() public view returns (uint256) { uint256 timeSinceLastDistribution = block.timestamp - lastDistributionTimestamp; if (timeSinceLastDistribution >= VESTING_PERIOD) { return 0; } return ((VESTING_PERIOD - timeSinceLastDistribution) * vestingAmount) / VESTING_PERIOD; }
If some assets left and at the same time totalSupply
is zero the rate between assets and shares become unbalanced and deposit
will revert for normal amounts of assets.
Let's look at the ERC4626
contract:
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); }
For example there is 1.0 * 10**18
of assets on the contract balance, totalSupply
is zero and an user want to deposit $100k:
100 000 * 10**18 * 10**0 / (1.0 * 10**18 + 1) < MIN_SHARES == 10**18
So the _checkMinShares
will revert:
function _checkMinShares() internal view { uint256 _totalSupply = totalSupply(); if (_totalSupply > 0 && _totalSupply < MIN_SHARES) revert MinSharesViolation(); }
These assets can not be withdrawn even by the DEFAULT_ADMIN_ROLE
because of check in the rescueTokens
. So the contract will have to be deployed again:
function rescueTokens(address token, uint256 amount, address to) external onlyRole(DEFAULT_ADMIN_ROLE) { if (address(token) == asset()) revert InvalidToken(); IERC20(token).safeTransfer(to, amount); }
Manual review
It is possible to let DEFAULT_ADMIN_ROLE
withdraw assets in case totalSupply
equals zero to prevent permanent DoS. Also possible to deposit $1 and as receiver
put this contract. Then the _checkMinShares
check will be redundant.
DoS
#0 - c4-pre-sort
2023-11-01T03:08:41Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-01T03:08:48Z
raymondfam marked the issue as duplicate of #32
#2 - c4-judge
2023-11-10T20:52:18Z
fatherGoose1 changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-11-10T21:02:38Z
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
getUnvestedAmount
functionThere is getUnvestedAmount()
call at the line #91 which always returns 0
because of the check at the line #90 in the transferInRewards
of the StakedUSDe
contract:
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L90-L91
89 function transferInRewards(uint256 amount) external nonReentrant onlyRole(REWARDER_ROLE) notZero(amount) { 90 if (getUnvestedAmount() > 0) revert StillVesting(); 91 uint256 newVestingAmount = amount + getUnvestedAmount();
Consider removing this call.
There is a check at the constructor if at least one asset exists in the EthenaMinting
contract. But the DEFAULT_ADMIN_ROLE
user can remove all assets addresses
120 if (_assets.length == 0) revert NoAssetsProvided(); 258 /// @notice Removes an asset from the supported assets list 259 function removeSupportedAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) { 260 if (!_supportedAssets.remove(asset)) revert InvalidAssetAddress(); 261 emit AssetRemoved(asset); 262 }
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/EthenaMinting.sol#L258-L262
Consider adding the check of the minimal _supportedAssets
length.
#0 - raymondfam
2023-11-02T01:17:59Z
L-02 is admin called after all.
#1 - c4-pre-sort
2023-11-02T01:18:07Z
raymondfam marked the issue as low quality report
#2 - c4-judge
2023-11-14T17:12:38Z
fatherGoose1 marked the issue as grade-b