Smart Security Practices From The Best
What do Lido, Red Stone, YieldNest, and Braintrust have in common? They’ve developed effective methods for improving security without drastically increasing costs. Top-tier protocol […]
When a holder has the SOFT_RESTRICTED_STAKER_ROLE, they can exchange their
stUSDe for USDe using StakedUSDeV2 despite the protocol requirement.
The Ethena readme has the following description of legal requirements for the SOFT_RESTRICTED_STAKER_ROLE:
Due to legal requirements, there's a SOFT_RESTRICTED_STAKER_ROLE and FULL_RESTRICTED_STAKER_ROLE.
The former is for addresses based in countries we are not allowed to provide yield to, for example USA.
Addresses under this category will be soft restricted. They cannot deposit USDe to get stUSDe or withdraw stUSDe for USDe.
However they can participate in earning yield by buying and selling stUSDe on the open market.
In summary, legal requires are that a SOFT_RESTRICTED_STAKER_ROLE:
As StakedUSDeV2 is a ERC4626, the stUSDe is a share on the underlying USDe asset. There are two distinct entry points for a user to exchange their share for their claim on the underlying the asset, withdraw and redeem.
Each cater for a different input (withdraw being by asset, redeem being by share), however both invoked the same
internal _withdraw function, hence both entry points are affected.
There are two cases where a user with SOFT_RESTRICTED_STAKER_ROLE may have acquired stUSDe:
In both cases the user can call either withdraw their holding by calling withdraw or redeem (when cooldown is off),
or unstake (if cooldown is on) and successfully exchange their stUSDe for USDe.
The following two tests demonstrate the use case of a user staking, then being granted the SOFT_RESTRICTED_STAKER_ROLE, then exchanging their stUSDe for USDe (first using redeem function, the second using withdraw).
The use case for acquiring on the open market, only requiring a different setup, however the exchange behavior is identical and
the cooldown enabled cooldownAssets and cooldownShares function still use the same _withdraw as redeem and withdraw, which leads to the same outcome.
bytes32 public constant SOFT_RESTRICTED_STAKER_ROLE = keccak256("SOFT_RESTRICTED_STAKER_ROLE");
bytes32 private constant BLACKLIST_MANAGER_ROLE = keccak256("BLACKLIST_MANAGER_ROLE");
function test_redeem_while_soft_restricted() public {
// Set up Bob with 100 stUSDe
uint256 initialAmount = 100 ether;
_mintApproveDeposit(bob, initialAmount);
uint256 stakeOfBob = stakedUSDe.balanceOf(bob);
// Alice becomes a blacklist manager
vm.prank(owner);
stakedUSDe.grantRole(BLACKLIST_MANAGER_ROLE, alice);
// Blacklist Bob with the SOFT_RESTRICTED_STAKER_ROLE
vm.prank(alice);
stakedUSDe.addToBlacklist(bob, false);
// Assert that Bob has staked and is now has the soft restricted role
assertEq(usdeToken.balanceOf(bob), 0);
assertEq(stakedUSDe.totalSupply(), stakeOfBob);
assertEq(stakedUSDe.totalAssets(), initialAmount);
assertTrue(stakedUSDe.hasRole(SOFT_RESTRICTED_STAKER_ROLE, bob));
// Rewards to StakeUSDe and vest
uint256 rewardAmount = 50 ether;
_transferRewards(rewardAmount, rewardAmount);
vm.warp(block.timestamp + 8 hours);
// Assert that only the total assets have increased after vesting
assertEq(usdeToken.balanceOf(bob), 0);
assertEq(stakedUSDe.totalSupply(), stakeOfBob);
assertEq(stakedUSDe.totalAssets(), initialAmount + rewardAmount);
assertTrue(stakedUSDe.hasRole(SOFT_RESTRICTED_STAKER_ROLE, bob));
// Bob withdraws his stUSDe for USDe
vm.prank(bob);
stakedUSDe.redeem(stakeOfBob, bob, bob);
// End state being while being soft restricted Bob redeemed USDe with rewards
assertApproxEqAbs(usdeToken.balanceOf(bob), initialAmount + rewardAmount, 2);
assertApproxEqAbs(stakedUSDe.totalAssets(), 0, 2);
assertTrue(stakedUSDe.hasRole(SOFT_RESTRICTED_STAKER_ROLE, bob));
}
function test_withdraw_while_soft_restricted() public {
// Set up Bob with 100 stUSDe
uint256 initialAmount = 100 ether;
_mintApproveDeposit(bob, initialAmount);
uint256 stakeOfBob = stakedUSDe.balanceOf(bob);
// Alice becomes a blacklist manager
vm.prank(owner);
stakedUSDe.grantRole(BLACKLIST_MANAGER_ROLE, alice);
// Blacklist Bob with the SOFT_RESTRICTED_STAKER_ROLE
vm.prank(alice);
stakedUSDe.addToBlacklist(bob, false);
// Assert that Bob has staked and is now has the soft restricted role
assertEq(usdeToken.balanceOf(bob), 0);
assertEq(stakedUSDe.totalSupply(), stakeOfBob);
assertEq(stakedUSDe.totalAssets(), initialAmount);
assertTrue(stakedUSDe.hasRole(SOFT_RESTRICTED_STAKER_ROLE, bob));
// Rewards to StakeUSDe and vest
uint256 rewardAmount = 50 ether;
_transferRewards(rewardAmount, rewardAmount);
vm.warp(block.timestamp + 8 hours);
// Assert that only the total assets have increased after vesting
assertEq(usdeToken.balanceOf(bob), 0);
assertEq(stakedUSDe.totalSupply(), stakeOfBob);
assertEq(stakedUSDe.totalAssets(), initialAmount + rewardAmount);
assertTrue(stakedUSDe.hasRole(SOFT_RESTRICTED_STAKER_ROLE, bob));
// Bob withdraws his stUSDe for USDe (-1 as dust is lost in asset to share rounding in ERC4626)
vm.prank(bob);
stakedUSDe.withdraw(initialAmount + rewardAmount - 1, bob, bob);
// End state being while being soft restricted Bob redeemed USDe with rewards
assertApproxEqAbs(usdeToken.balanceOf(bob), initialAmount + rewardAmount, 2);
assertApproxEqAbs(stakedUSDe.totalAssets(), 0, 2);
assertTrue(stakedUSDe.hasRole(SOFT_RESTRICTED_STAKER_ROLE, bob));
}MEDIUM – The holder with the SOFT_RESTRICTED_STAKER_ROLE can exchange their stUSDe for USDe using StakedUSDeV2.
With the function overriding present, to prevent the SOFT_RESTRICTED_STAKER_ROLE from being able to exchange their
stUSDs for USDe, make the following change in StakedUSDe
- 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(SOFT_RESTRICTED_STAKER_ROLE, caller)) {
revert OperationNotAllowed();
}Meet Composable Security
Get throughly tested by the creators of Smart Contract Security Verification Standard
Let us help
Get throughly tested by the creators of Smart Contract Security Verification Standard