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: 6/147
Findings: 3
Award: $1,555.84
🌟 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#L232
The project requirements clearly state that a fully restricted account should not be able stake or unstake in the StakedUSDe
contract.
However, in cases where an account becomes restricted after staking, it can easily bypass the restriction by granting approval to another account to withdraw on its behalf.
The restriction has no effect, and a restricted account can withdraw its stake without any obstacles.
Alice
stakes 100 USDe.Alice
account is fully restricted by the BLACKLIST_MANAGER_ROLE
.Alice
approves Bob
to withdraw all her stUSDe
balance.Bob
proceeds to redeem/withdraw all of Alice
allowed stUSDe
from the staking contract.This test demonstrates how to use the redeem()
function to redeem all stUSDe
belonging to Alice
when her account is fully restricted.
To run this test, add it to the test/foundry/staking/StakedUSDe.blacklist.t.sol
file and execute forge test --match-test test_PoC_fullBlacklist_user_can_bypassed_withdraw_restriction
.
function test_PoC_fullBlacklist_user_can_bypassed_withdraw_restriction() public { usdeToken.mint(alice, _amount); // Alice deposit vm.startPrank(alice); usdeToken.approve(address(stakedUSDe), _amount); uint256 receivedStUSDe = stakedUSDe.deposit(_amount, alice); vm.stopPrank(); // Alice fully blacklisted vm.prank(owner); stakedUSDe.grantRole(FULL_RESTRICTED_STAKER_ROLE, alice); // Alice approves Bob vm.prank(alice); stakedUSDe.approve(bob, receivedStUSDe); uint256 balBefore = usdeToken.balanceOf(bob); // Bob is able to withdraw for Alice - with no restriction vm.startPrank(bob); vm.expectEmit(true, true, true, true); emit Withdraw(bob, bob, alice, _amount, receivedStUSDe); stakedUSDe.redeem(receivedStUSDe, bob, alice); vm.stopPrank(); uint256 balAfter = usdeToken.balanceOf(bob); assertApproxEqAbs(_amount, balAfter - balBefore, 1); }
Manual review
To address this issue, update the _withdraw()
function in the StakedUSDe
contract by adding a check to ensure that _owner
is not 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-31T18:47:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T18:47:20Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:18Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:34:23Z
fatherGoose1 marked the issue as satisfactory
#4 - c4-judge
2023-11-14T15:20:53Z
fatherGoose1 changed the severity to 2 (Med Risk)
🌟 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#L210
Project requirements clearly state that a fully restricted account should not be able to transfer, stake, or unstake.
However, in cases where an account is restricted, such an account can easily bypass the restriction by staking with a non-restricted account as the receiver for minted stUSDe
.
The restriction has no effect, and a restricted account can deposit their USDe
and receive yield.
Alice
account is fully restricted by the BLACKLIST_MANAGER_ROLE
.Alice
deposits 100 USDe, setting Bob
as the receiver for stUSDe
.Bob
receives stUSDe
from Alice
stake.This test illustrates how to use deposit()
to stake USDe
belonging to Alice
in a situation when Alice
was fully restricted.
Add this test to the test/foundry/staking/StakedUSDe.blacklist.t.sol
and run forge test --match-test test_PoC_fullBlacklist_user_can_bypassed_deposit_restriction
.
function test_PoC_fullBlacklist_user_can_bypassed_deposit_restriction() public { usdeToken.mint(alice, _amount); // Alice full blacklisted vm.prank(owner); stakedUSDe.grantRole(FULL_RESTRICTED_STAKER_ROLE, alice); uint256 sharesBefore = stakedUSDe.balanceOf(bob); // Alice is able to deposit with Bob as receiver - with no restriction vm.startPrank(alice); usdeToken.approve(address(stakedUSDe), _amount); vm.expectEmit(true, true, true, true); emit Deposit(alice, bob, _amount, _amount); stakedUSDe.deposit(_amount, bob); vm.stopPrank(); uint256 sharesAfter = stakedUSDe.balanceOf(bob); assertApproxEqAbs(sharesAfter - sharesBefore, _amount, 1); }
Manual review
Update the _deposit()
function in the StakedUSDe
contract with a check to ensure that the receiver
is not fully restricted (caller
full restriction is checked in the _beforeTokenTransfer()
function, duplication is not needed):
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) // @audit-ok internal override nonReentrant notZero(assets) notZero(shares) { - if (hasRole(SOFT_RESTRICTED_STAKER_ROLE, caller) || hasRole(SOFT_RESTRICTED_STAKER_ROLE, receiver)) { + if (hasRole(SOFT_RESTRICTED_STAKER_ROLE, caller) || hasRole(SOFT_RESTRICTED_STAKER_ROLE, receiver) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); } super._deposit(caller, receiver, assets, shares); _checkMinShares(); }
Access Control
#0 - c4-pre-sort
2023-10-31T18:50:58Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T18:51:08Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:19Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:34:31Z
fatherGoose1 marked the issue as satisfactory
🌟 Selected for report: adeolu
Also found by: Eeyore, Madalad, Mike_Bello90, Shubham, jasonxiale, josephdara, peanuts
1432.1788 USDC - $1,432.18
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L82
In the unstake()
function, the mechanism to claim staked assets after a cooldown relies on comparing the current block's timestamp with the cooldownEnd
timestamp associated with the user:
... if (block.timestamp >= userCooldown.cooldownEnd) { ...
However, there's no provision in place to account for situations where the global cooldownDuration
is set to 0. Consequently, users cannot instantly claim all their cooldowning assets under these circumstances.
If the global cooldownDuration
is set to 0, the staking contract should behave like a standard ERC4626 vault, and users should be able to retrieve their full staked amount instantly upon withdrawal. Given that this is not the current behavior and users with active cooldowns face penalties compared to others, it could erode trust in the protocol. Users expect to access their assets seamlessly.
Bob
stakes a certain amount, initiating a cooldown period.cooldownDuration
to 0.Bob
tries to call the unstake()
function to access his staked assets.Bob
from accessing his assets, despite the expectation that he should be able to claim them instantly since the cooldown is effectively nullified.Manual review
It's suggested to introduce a condition to check whether the global cooldownDuration
is set to 0, thereby enabling users to claim their staked assets immediately. This can be achieved with the code:
... if (block.timestamp >= userCooldown.cooldownEnd || cooldownDuration == 0) { ...
Incorporating this condition ensures that users can access their staked assets without any issues when the global cooldown duration is set to zero.
Invalid Validation
#0 - c4-pre-sort
2023-10-31T19:26:00Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T19:26:16Z
raymondfam marked the issue as duplicate of #29
#2 - c4-judge
2023-11-13T19:00:29Z
fatherGoose1 marked the issue as satisfactory
#3 - c4-judge
2023-11-17T02:45:07Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-11-17T16:47:08Z
This previously downgraded issue has been upgraded by fatherGoose1
#5 - c4-judge
2023-11-27T19:57:15Z
fatherGoose1 marked the issue as not a duplicate
#6 - c4-judge
2023-11-27T19:57:25Z
fatherGoose1 marked the issue as duplicate of #198
🌟 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/StakedUSDe.sol#L106-L109 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L120-L123
Both of these issues stem from the same set of functions (addToBlacklist()
and removeFromBlacklist()
), which handle blacklisting operations in the contract.
Misleading NatSpec comments for roles
In the provided code for the addToBlacklist()
and removeFromBlacklist()
functions, the NatSpec comments imply that the DEFAULT_ADMIN_ROLE
can directly interact with these functions. However, in reality, the DEFAULT_ADMIN_ROLE
has the ability to control blacklisting indirectly by manipulating roles using the grantRole()
and revokeRole()
functions.
This misleading documentation also hides the potential risk mentioned in the second issue where the DEFAULT_ADMIN_ROLE
might inadvertently blacklist itself.
Potential for DEFAULT_ADMIN_ROLE
to blacklist itself
An oversight exists where the DEFAULT_ADMIN_ROLE
might blacklist itself, via the grantRole()
function. While the addToBlacklist()
function does have the notOwner()
protection modifier, if it is genuinely intended that the DEFAULT_ADMIN_ROLE
should also be able to blacklist but simultaneously be protected from blacklisting itself, additional logic is required. This could be achieved by overriding the _grantRole()
function to include checks ensuring the owner does not blacklist themselves.
DEFAULT_ADMIN_ROLE
can blacklist itself, it could render the admin ineffective, potentially stopping essential functions of the contract.DEFAULT_ADMIN_ROLE
attempts to use the addToBlacklist()
and removeFromBlacklist()
functions, but the calls are reverted.DEFAULT_ADMIN_ROLE
employs the grantRole()
function and sets themselves as fully restricted.Manual review
DEFAULT_ADMIN_ROLE
.DEFAULT_ADMIN_ROLE
from blacklisting themselves._grantRole()
function and incorporate the necessary checks to make sure only the intended addresses can be assigned specific roles.Invalid Validation
#0 - c4-pre-sort
2023-10-31T19:34:19Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-10-31T19:34:30Z
raymondfam marked the issue as duplicate of #136
#2 - c4-judge
2023-11-13T20:42:35Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-14T17:02:02Z
fatherGoose1 marked the issue as grade-b