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: 14/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/main/contracts/StakedUSDe.sol#L225-L238
donation attack is still possible in the beginning of stakedUSDe deployment; such that the attacker can Ddos the withdrawal
_checkMinShares is only effective when shares > 0 and share is lower than 1 ether
after a deposit. However the first depositor can still launch an inflation/donation attack, by:
donate a massive amount of stUSDe to the contract.
deposit to get 1 ether
unit of shares depending on how much he donates in step 1.). Basically he/she still owns the whole pool since attacker is the first depositor.
Whoever deposit next, would get only a small shares (possibly 0.000x), in this case, if the depositor would like to withdraw, the attacker can frontrun the withdrawal such that the share is reduced to "just 1 ether". Then the victim would not be able to withdraw since the withdraw tx would fail to pass _checkMinShares
due to redicing the totalSupply to lower than 1 ether
/// @notice ensures a small non-zero amount of shares does not remain, exposing to donation attack function _checkMinShares() internal view { uint256 _totalSupply = totalSupply(); if (_totalSupply > 0 && _totalSupply < MIN_SHARES) revert MinSharesViolation(); }
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(); }
Consider remove the check in _withdraw.
DoS
#0 - c4-pre-sort
2023-10-31T00:03:45Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T00:04:11Z
raymondfam marked the issue as duplicate of #32
#2 - c4-judge
2023-11-10T20:56:21Z
fatherGoose1 marked the issue as satisfactory
🌟 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/main/contracts/StakedUSDe.sol#L191-L194
stakedUSDe can be Ddos by donation attack in initial deployment to brick deposit
attacker can Ddos the stakedUSDe contract by donate a small amount to the contract when there is no depositor. Since there is a checkMinShare of 1 ether
in any deposit, if the attacker donates, say 1 USDe to the contract, then anyone would need to deposit a minimum of 1e18 * 1e18
or 1e36 asset to get 1e18 shares, which is not possible since 1e36 exceeds any meaningful supply.
OpenZeppelin ERC4626 share conversion. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol#L236-L238
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); } function _decimalsOffset() internal view virtual returns (uint8) { return 0; }
run in StakedUSDe.t.sol
function testDonateDosStake() public { uint256 amount = 100 ether; // donate attack to make anyone unable to pass the minShare check address staker = alice; usdeToken.mint(address(stakedUSDe), 1 ether); usdeToken.mint(staker, amount); vm.startPrank(staker); usdeToken.approve(address(stakedUSDe), amount); vm.expectRevert(IStakedUSDe.MinSharesViolation.selector); stakedUSDe.deposit(amount, staker); }
during constructor establish an initial conversion rate before shares get inflated.
Context
#0 - c4-pre-sort
2023-10-31T00:22:16Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T00:22:31Z
raymondfam marked the issue as duplicate of #32
#2 - c4-judge
2023-11-10T20:57:16Z
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
[L-1] silo can be made immutable.
Silo is deployed in constructor, and there is no setter in the StakedUSDeV2.
constructor(IERC20 _asset, address initialRewarder, address owner) StakedUSDe(_asset, initialRewarder, owner) { silo = new USDeSilo(address(this), address(_asset)); cooldownDuration = MAX_COOLDOWN_DURATION; }
[L-2] _deduplicateOrder returning a true and checks is redundant since it never returns a false valid
from verifyNonce, which simply revert.
if (!_deduplicateOrder(order.benefactor, order.nonce)) revert Duplicate(); function _deduplicateOrder(address sender, uint256 nonce) private returns (bool) { (bool valid, uint256 invalidatorSlot, uint256 invalidator, uint256 invalidatorBit) = verifyNonce(sender, nonce); mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender]; invalidatorStorage[invalidatorSlot] = invalidator | invalidatorBit; return valid; } function verifyNonce(address sender, uint256 nonce) public view override returns (bool, uint256, uint256, uint256) { if (nonce == 0) revert InvalidNonce(); uint256 invalidatorSlot = uint64(nonce) >> 8; uint256 invalidatorBit = 1 << uint8(nonce); mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender]; uint256 invalidator = invalidatorStorage[invalidatorSlot]; if (invalidator & invalidatorBit != 0) revert InvalidNonce(); return (true, invalidatorSlot, invalidator, invalidatorBit); }
#0 - raymondfam
2023-11-02T03:42:07Z
No instance links were given.
#1 - c4-pre-sort
2023-11-02T03:42:16Z
raymondfam marked the issue as low quality report