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: 42/147
Findings: 3
Award: $130.12
🌟 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-L238
Full restricted staker can redeem their stUSDe regardless of their restriction.
StakedUSDe.withdraw()
and StakedUSDe.redeem()
can be called by any one to redeem their shares stUSDes for assets USDes._withdraw()
will be called by StakedUSDe.withdraw()
or StakedUSDe.redeem()
to check if the caller is allowed to access these functions. Full restricted staker should be forbidden to redeem their stUSDe for USDe.
However, this check will be bypassed if an unrestricted caller withdraws assets/redeems shares on behalf of the full restricted staker.Copy the below test codes into StakedUSDe.blacklist.t.sol and run forge test --match-test test_fullBlacklist_bob_withdraw_on_behalf_of_alice_pass
:
function test_fullBlacklist_bob_withdraw_on_behalf_of_alice_pass() public { _mintApproveDeposit(alice, _amount, false); vm.prank(owner); stakedUSDe.grantRole(FULL_RESTRICTED_STAKER_ROLE, alice); //@audit-info make sure alice is full restricted assertEq(stakedUSDe.hasRole(FULL_RESTRICTED_STAKER_ROLE, alice), true); uint256 balBefore = usdeToken.balanceOf(bob); vm.prank(alice); stakedUSDe.approve(bob, type(uint).max); vm.prank(bob); //@audit-info bob redeem _amount of stUSDe on behalf of alice, and send USDe to bob stakedUSDe.redeem(_amount, bob, alice); uint256 balAfter = usdeToken.balanceOf(bob); //@audit-info check if bob received _amount of USDe assertApproxEqAbs(_amount, balAfter - balBefore, 1); }
Manual review
Modifies the function _withdraw()
. It should also be reverted if owner
is full restricted:
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, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver) || hasRole(FULL_RESTRICTED_STAKER_ROLE, _owner)) { revert OperationNotAllowed(); } super._withdraw(caller, receiver, _owner, assets, shares); _checkMinShares(); }
Access Control
#0 - c4-pre-sort
2023-10-31T02:35:55Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T02:36:09Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:44:59Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:31:48Z
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/StakedUSDeV2.sol#L95-L106 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L111-L122
Caller who requested asset withdrawing or redeeming on behalf of owner
is not be able to access asset when cooldown period ends.
StakedUSDeV2
defines an unstake cooldown period cooldownDuration
.
cooldownDuration
is zero, caller can call withdraw()
or redeem()
on behalf of owner if they have enough allowance approved by owner. The allowance will be reduced correspondingly if it is not set to type(uint).max
.cooldownDuration
is not zero, caller can call cooldownAssets()
or cooldownShares()
on behalf of owner
if they have enough allowance approved by owner
. Similarly, the allowance will be reduced if it is not set to type(uint).max
. Asset will be locked in silo
contract and can only be withdrawed after cooldown period.However, the caller can not withdraw asset when cooldown period ends because cooldowns
was set wrongly. Only owner
has right to withdraw asset from silo
, which is unreasonable.
Copy the below test codes into StakedUSDeV2.cooldownEnabled.t.sol and run run forge test --match-test testBobRedeemSharesOnBehalfAlice
:
function testBobRedeemSharesOnBehalfAlice() public { uint256 amount = 100 ether; _mintApproveDeposit(alice, amount); assertEq(usdeToken.balanceOf(alice), 0); assertEq(usdeToken.balanceOf(address(stakedUSDe)), amount); assertEq(stakedUSDe.balanceOf(alice), amount); uint256 balOfBobBefore = usdeToken.balanceOf(bob); uint256 balOfAliceBefore = usdeToken.balanceOf(alice); //@audit-info Sets type(uint).max` as the allowance of bob over alice's tokens. vm.prank(alice); stakedUSDe.approve(bob, type(uint).max); //@audit-info Bob redeem amount of shares on behalf of alice vm.prank(bob); stakedUSDe.cooldownShares(amount, alice); skip(stakedUSDe.cooldownDuration() + 1); //@audit-info Bob withdraw redeemed asset and send it to himself after cooldown period ends vm.prank(bob); stakedUSDe.unstake(bob); //@audit-info Alice withdraw redeemed asset and send it to herself after cooldown period ends vm.prank(alice); stakedUSDe.unstake(alice); uint256 balOfBobAfter = usdeToken.balanceOf(bob); uint256 balOfAliceAfter = usdeToken.balanceOf(alice); //@audit-info bob received none of asset assertEq(balOfBobBefore, balOfBobAfter, "balance of Bob didn't change"); //@audit-info However alice received amount of asset assertEq(balOfAliceBefore+amount, balOfAliceAfter, "Alice didn't receive asset"); }
Manual review
Asset redeemed / withdrawed should be assigned to caller
instead of owner
:
function cooldownAssets(uint256 assets, address owner) external ensureCooldownOn returns (uint256) { if (assets > maxWithdraw(owner)) revert ExcessiveWithdrawAmount(); uint256 shares = previewWithdraw(assets); - cooldowns[owner].cooldownEnd = uint104(block.timestamp) + cooldownDuration; - cooldowns[owner].underlyingAmount += assets; + cooldowns[_msgSender()].cooldownEnd = uint104(block.timestamp) + cooldownDuration; + cooldowns[_msgSender()].underlyingAmount += assets; _withdraw(_msgSender(), address(silo), owner, assets, shares); return shares; } function cooldownShares(uint256 shares, address owner) external ensureCooldownOn returns (uint256) { if (shares > maxRedeem(owner)) revert ExcessiveRedeemAmount(); uint256 assets = previewRedeem(shares); - cooldowns[owner].cooldownEnd = uint104(block.timestamp) + cooldownDuration; - cooldowns[owner].underlyingAmount += assets; + cooldowns[_msgSender()].cooldownEnd = uint104(block.timestamp) + cooldownDuration; + cooldowns[_msgSender()].underlyingAmount += assets; _withdraw(_msgSender(), address(silo), owner, assets, shares); return assets; }
Error
#0 - c4-pre-sort
2023-10-31T05:00:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T05:00:54Z
raymondfam marked the issue as duplicate of #62
#2 - c4-judge
2023-11-13T20:33:41Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-14T16:47:48Z
fatherGoose1 marked the issue as grade-b
#4 - c4-judge
2023-11-17T03:23:25Z
This previously downgraded issue has been upgraded by fatherGoose1
#5 - c4-judge
2023-11-17T03:24:01Z
fatherGoose1 changed the severity to QA (Quality Assurance)
🌟 Selected for report: 0xVolcano
Also found by: 0x11singh99, 0xAadi, 0xAnah, 0xgrbr, 0xhacksmithh, 0xhex, 0xpiken, 0xta, J4X, JCK, K42, Raihan, Rolezn, SAQ, SM3_SS, Sathish9098, SovaSlava, ThreeSigma, Udsen, arjun16, aslanbek, brakelessak, castle_chain, evmboi32, hunter_w3b, lsaudit, naman1778, niser93, nuthan2x, oakcobalt, pavankv, petrichor, phenom80, radev_sw, shamsulhaq123, tabriz, thekmj, unique, yashgoel72, ybansal2403
6.4563 USDC - $6.46
transferInRewards()
is unnecessory because getUnvestedAmount()
is always zero:function transferInRewards(uint256 amount) external nonReentrant onlyRole(REWARDER_ROLE) notZero(amount) { if (getUnvestedAmount() > 0) revert StillVesting(); - uint256 newVestingAmount = amount + getUnvestedAmount(); - vestingAmount = newVestingAmount; + vestingAmount = amount; lastDistributionTimestamp = block.timestamp; // transfer assets from rewarder to this contract IERC20(asset()).safeTransferFrom(msg.sender, address(this), amount); - emit RewardsReceived(amount, newVestingAmount); + emit RewardsReceived(amount, amount); }
MAX_COOLDOWN_DURATION
as constant:- uint24 public MAX_COOLDOWN_DURATION = 90 days; + uint24 public constant MAX_COOLDOWN_DURATION = 90 days;
token.safeTransferFrom()
in EthenaMinting#_transferCollateral()
can be reduced by on because the last amount in iteration and the remaining balance (if not zero) will be transferred to the same address:function _transferCollateral( uint256 amount, address asset, address benefactor, address[] calldata addresses, uint256[] calldata ratios ) internal { // cannot mint using unsupported asset or native ETH even if it is supported for redemptions if (!_supportedAssets.contains(asset) || asset == NATIVE_TOKEN) revert UnsupportedAsset(); IERC20 token = IERC20(asset); uint256 totalTransferred = 0; - for (uint256 i = 0; i < addresses.length; ++i) { + for (uint256 i = 0; i < addresses.length - 1; ++i) { uint256 amountToTransfer = (amount * ratios[i]) / 10_000; token.safeTransferFrom(benefactor, addresses[i], amountToTransfer); totalTransferred += amountToTransfer; } uint256 remainingBalance = amount - totalTransferred; - if (remainingBalance > 0) { token.safeTransferFrom(benefactor, addresses[addresses.length - 1], remainingBalance); - } }
#0 - c4-pre-sort
2023-11-01T15:18:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-11-10T20:24:08Z
fatherGoose1 marked the issue as grade-b