Ethena Labs - nuthan2x'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: 81/147

Findings: 2

Award: $10.98

QA:
grade-b
Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

IssueInstances
LOW-1ownership transfer in constructor is not implemented on Ownable2Step way1
LOW-2transaction fails on one of the variant of token & to1
LOW-3FULL_RESTRICTED_STAKER_ROLE can be frontran1

<a name="LOW-1"></a>[LOW-1] USDe::constructor ownership transfer for first time in constructor is not implemented on Ownable2Step method

  • Initial ownership transfer should be done in Ownable2Step method, but directly transferred in constructor
  • First call transferOwnership(admin) then from admin key, call acceptOwnership()
  • Severity : Low
  • Likelihood : Medium
-  _transferOwnership(admin);
+  transferOwnership(admin);

<a name="LOW-2"></a>[LOW-2] StakedUSDe.rescueTokens() transaction fails if token == stUSDE && to == address with FULL_RESTRICTED_STAKER_ROLE

  • Severity : low
  • Likelihood : low
  • So either improve your backend to do all the checks, signature replays etc., Or change the way EthenaMinting.setDelegatedSigner(address) is implemented in 2 step process like ownable2step.
  • So the delegated EOA sould accept the delegation and then only can be a delegate.
  • Second mitigation requires introducing new storage and a new function to accept delegation.
  function rescueTokens(address token, uint256 amount, address to) external onlyRole(DEFAULT_ADMIN_ROLE) {
    if (address(token) == asset()) revert InvalidToken();
-    IERC20(token).safeTransfer(to, amount);
  }

  function rescueTokens(address token, uint256 amount, address to) external onlyRole(DEFAULT_ADMIN_ROLE) {
    if (address(token) == asset()) revert InvalidToken();
+   if (address(token) != address(this) && !hasRole(FULL_RESTRICTED_STAKER_ROLE, to)) {   
+     IERC20(token).safeTransfer(to, amount);
+   }
  }

<a name="LOW-3"></a>[LOW-3] StakedUSDe._beforeTokenTransfer can be frontran by a bot.

  • Scenario:
    • whenever a BLACKLIST_MANAGER_ROLE makes a tx, it can be searched via mempool and a bot with approval of the (to be blacklisted)'s address will transfer the funds to the delegate already set. Or a owner with key can run a bot to frontrun it.
    • Low likelyhood, should be soled when tx'ns of blacklist manager are sent to flashbots builder.
  • Severity : low
  • Likelihood : Med

#0 - c4-pre-sort

2023-11-02T03:26:26Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-11-14T16:48:27Z

fatherGoose1 marked the issue as grade-b

Awards

6.4563 USDC - $6.46

Labels

bug
G (Gas Optimization)
grade-b
sufficient quality report
edited-by-warden
G-35

External Links

IssueInstances
GAS-1additional warm sload and memory expansion2
GAS-2update the state after validating the returned bool1

<a name="GAS-1"></a>[GAS-1] EthenaMinting::_setMaxRedeemPerBlock() Gas inefficient due to additional warm sload and memory expansion.

Impact

  • In EthenaMinting._setMaxMintPerBlock(), there is a new local variable oldMaxMintPerBlock is declared to cache the storage to emit later after updating it.
  • But it can be avoided and by emitting the params and storage before modifyng the storage. It is implemented at gas efficiently at SingleAdminAccessControl._grantRole, but missing at EthenaMinting._setMaxRedeemPerBlock() & EthenaMinting._setMaxRedeemPerBlock()

  function _setMaxRedeemPerBlock(uint256 _maxRedeemPerBlock) internal {
-    uint256 oldMaxRedeemPerBlock = maxRedeemPerBlock;
    maxRedeemPerBlock = _maxRedeemPerBlock;
-    emit MaxRedeemPerBlockChanged(oldMaxRedeemPerBlock, maxRedeemPerBlock);
  }

  function _setMaxRedeemPerBlock(uint256 _maxRedeemPerBlock) internal {
+   emit MaxRedeemPerBlockChanged(maxRedeemPerBlock, _maxRedeemPerBlock);
    maxRedeemPerBlock = _maxRedeemPerBlock;
  }

<a name="GAS-2"></a>[GAS-2] EthenaMinting._deduplicateOrder(), update the state only if valid== true.

  • valid is validated after updating storage, so if valid == false, then its a gas inefficient.

  function _deduplicateOrder(address sender, uint256 nonce) private returns (bool) {
    (bool valid, uint256 invalidatorSlot, uint256 invalidator, uint256 invalidatorBit) = verifyNonce(sender, nonce);
-    mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender];
-    invalidatorStorage[invalidatorSlot] = invalidator | invalidatorBit;
    return valid;
  }

  function _deduplicateOrder(address sender, uint256 nonce) private returns (bool) {
    (bool valid, uint256 invalidatorSlot, uint256 invalidator, uint256 invalidatorBit) = verifyNonce(sender, nonce);
+    if(valid) {
+        mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender];
+        invalidatorStorage[invalidatorSlot] = invalidator | invalidatorBit;
+    }
    return valid;
  }

#0 - c4-pre-sort

2023-11-01T15:39:25Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-11-10T20:11:03Z

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