Ethena Labs - tnquanghuy0512's results

Enabling The Internet Bond

General Information

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

Ethena Labs

Findings Distribution

Researcher Performance

Rank: 46/147

Findings: 2

Award: $123.66

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

119.1406 USDC - $119.14

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
edited-by-warden
duplicate-499

External Links

Lines of code

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

Vulnerability details

Impact

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

Proof of Concept

  1. Wallet A got full restricted by the protocol and have 1000 stUSDe in their wallet
  2. Wallet A approve to wallet B to gain permission to use wallet A's stUSDe
  3. Wallet B now can call to stUSDe.redeem(1000, B, A), burning wallet A's stUSDe and receiving USDe successfully

This scenario holds true whether cooldown toggle is on or off

Tools Used

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();
  }

Assessed type

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

Lines of code

https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDeV2.sol#L78-L90

Vulnerability details

Impact

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:

  • Step 1: Execute cooldownAssets() or cooldownShares() to withdraw their USDe to the silo
  • Step 2: Wait for the cooldown, can be up to 90 days
  • Step 3: Execute unstake() to finally transfer USDe from silo to their wallet

If 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()

Proof of Concept

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();
    }
  }

Tools Used

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);

  }

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter