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: 46/147
Findings: 2
Award: $123.66
🌟 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/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L232-L234 https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDeV2.sol#L78-L90
In the docs:
FULL_RESTRICTED_STAKER_ROLE - address with this role can't burn his stUSDe tokens to unstake his USDe tokens, neither to transfer stUSDe tokens
In reality: Full restricted user can still bypass unstaking USDe by approving permission to their alternative account
The reason causing this problem is because _withdraw()
only check if caller
or receiver
are full restricted, but not check owner
. Caller
is the user call to the function withdraw()
(msg.sender), and owner
is the user who will actual burn their stUSD
stUSDe.redeem(1000, B, A)
, burning wallet A's stUSDe and receiving USDe successfullyThis scenario holds true whether cooldown toggle is on or off
Manual Review
StakedUSDe._withdraw()
:
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(); }
ERC4626
#0 - c4-pre-sort
2023-10-31T18:38:38Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T18:38:46Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:16Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:34: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
As the docs said, FULL_RESTRICTED_STAKER_ROLE is for sanction/stolen funds, or if we get a request from law enforcement to freeze funds
When the cooldown toggle is on, users need to do 3 steps to withdraw USDe to their wallet:
cooldownAssets()
or cooldownShares()
to withdraw their USDe to the silounstake()
to finally transfer USDe from silo to their walletIf the stolen funds wallet got busted by the manager when the wallet waiting for the step 2 to be done, there's no way for manager prevent the wallet from doing step 3, hence failed to freeze or take back the funds. And if the malicious wallet steal the funds and then immediately do step 1, then the manager can't have enough time to recognize the malicious action and react. Because of that, chances of preventing malicious user is very low.
The reason for this issue is not have a requirement to check if the caller (msg.sender) is fully restricted when executing unstake()
Notice here that unstake()
function doesn't have a requirement to check if the caller is not malicious
function unstake(address receiver) external { UserCooldown storage userCooldown = cooldowns[msg.sender]; uint256 assets = userCooldown.underlyingAmount; if (block.timestamp >= userCooldown.cooldownEnd) { userCooldown.cooldownEnd = 0; userCooldown.underlyingAmount = 0; silo.withdraw(receiver, assets); } else { revert InvalidCooldown(); } }
Manual review
StakedUSDeV2.unstake()
:
function unstake(address receiver) external { + require(!hasRole(FULL_RESTRICTED_STAKER_ROLE, msg.sender)); UserCooldown storage userCooldown = cooldowns[msg.sender]; uint256 assets = userCooldown.underlyingAmount; if (block.timestamp >= userCooldown.cooldownEnd) { userCooldown.cooldownEnd = 0; userCooldown.underlyingAmount = 0; silo.withdraw(receiver, assets); } else { revert InvalidCooldown(); } }
Because of this fix, some of the funds can be stuck in silo contract. We need to have a method for admin to take out the fund of malicious wallet. The method can be look something like this:
StakedUSDeV2.unstakeBlacklist()
:
function unstakeBlacklist(address owner, address receiver) external onlyRole(DEFAULT_ADMIN_ROLE){ require(hasRole(FULL_RESTRICTED_STAKER_ROLE, owner)); UserCooldown storage userCooldown = cooldowns[owner]; uint256 assets = userCooldown.underlyingAmount; userCooldown.cooldownEnd = 0; userCooldown.underlyingAmount = 0; silo.withdraw(receiver, assets); }
Access Control
#0 - c4-pre-sort
2023-11-01T00:54:34Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-11-01T00:54:47Z
raymondfam marked the issue as duplicate of #110
#2 - c4-judge
2023-11-13T19:41:21Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - tnquanghuy0512
2023-11-16T03:53:58Z
@fatherGoose1 I don't think this issue is a duplicate of #110
My issue is about when the cooldown toggle is on, when the stolen/malicious user execute cooldownAssets()
or cooldownShares()
before the system/admin detect its malicious behaviour (which happens most of the time), then there's no way for admin to prevent that user to call to unstake()
to withdraw USDe from Silo.
Please reconsider this.
#4 - tnquanghuy0512
2023-11-16T07:43:45Z
Also, #671 is very similar to my issue but somehow got tagged dup-62, doesn't make any sense
#5 - fatherGoose1
2023-11-20T19:48:44Z
@tnquanghuy0512 #671 is analysis and not duped to #62. Can you please check and specify the correct report?
#6 - tnquanghuy0512
2023-11-21T07:21:50Z
@tnquanghuy0512 #671 is analysis and not duped to #62. Can you please check and specify the correct report?
@fatherGoose1 I'm sorry for mistaken. It's #674
#7 - c4-judge
2023-11-21T21:29:38Z
fatherGoose1 marked the issue as not a duplicate
#8 - fatherGoose1
2023-11-21T21:32:37Z
Same impact as #430, which is QA. Sponsor responded:
It's a good spot however I don't think it warrants a code change - if anything unstake is a bit of a misnomer - if a user has called cooldownAssets or cooldownShares their sUSDe has already been settled for USDe - they are no longer staked - they just need to wait to receive their USDe. Transfers of USDe are fully permissionless by design - note that there is no blacklisting in the USDe.sol. Therefore we are ok with the now blacklisted user being able to get the cooled down USDe.
And I responded:
The above comment is from the Ethena project team. Given that the funds have already been sent to the silo contract, the user is no longer sending or receiving funds directly from the vault contract, meaning that the restrictions are technically not bypassed.
#9 - c4-judge
2023-11-21T21:32:45Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#10 - c4-judge
2023-11-21T21:32:51Z
fatherGoose1 marked the issue as grade-b