Reach out for an audit or to learn more about Macro
or Message on Telegram

Superstate A-1

Security Audit

July 1st, 2024

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Superstate's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team on June 18, 2024.

The purpose of this audit is to review the source code of certain Superstate Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.

Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.

Overall Assessment

The following is an aggregation of issues found by the Macro Audit team:

Severity Count Acknowledged Won't Do Addressed
Medium 2 - 1 1
Code Quality 4 - 1 3
Informational 2 - - -

Superstate was quick to respond to these issues.

Specification

Our understanding of the specification was based on the following sources:

Source Code

The following source code was reviewed during the audit:

Specifically, we audited the following contracts within this repository:

Source Code SHA256

Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.

Issue Descriptions and Recommendations

Click on an issue to jump to it, or scroll down to see them all.

Security Level Reference

We quantify issues in three parts:

  1. The high/medium/low/spec-breaking impact of the issue:
    • How bad things can get (for a vulnerability)
    • The significance of an improvement (for a code quality issue)
    • The amount of gas saved (for a gas optimization)
  2. The high/medium/low likelihood of the issue:
    • How likely is the issue to occur (for a vulnerability)
  3. The overall critical/high/medium/low severity of the issue.

This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:

Severity Description
(C-x)
Critical

We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost.

(H-x)
High

We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec.

(M-x)
Medium

We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner.

(L-x)
Low

The risk is small, unlikely, or may not relevant to the project in a meaningful way.

Whether or not the project wants to develop a fix is up to the goals and needs of the project.

(Q-x)
Code Quality

The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design.

(I-x)
Informational

Warnings and things to keep in mind when operating the protocol. No immediate action required.

(G-x)
Gas Optimizations

The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it.

Issue Details

M-1

USDC exchange rate assumed to be 1:1 with USD

Topic
Protocol Design
Status
Wont Do
Impact
High
Likelihood
Low

In the redeem() function, when calculating the USDC amount out in return for the ustbInAmount input, the chainlink feed price for the USTB/USD pair is directly used as the USDC price, assuming a pegged exchange ratio with USD.

(bool isBadData,, uint256 usdPerUstbChainlinkRaw) = _getChainlinkPrice();
if (isBadData) revert BadChainlinkData();

// converts from a USTB amount to a USD amount, and then scales back up to a USDC amount
uint256 usdcOutAmount =
    (ustbInAmount * usdPerUstbChainlinkRaw * USDC_PRECISION) / (CHAINLINK_FEED_PRECISION * USTB_PRECISION);

Reference: Redemption.sol#L177-182

However, in abnormal market conditions, this 1:1 ratio can change and has changed before, potentially allowing external actors to execute arbitrage operations and profit from this indirect hardcoded value. Depending on the direction of de-peg, actors could receive more USDC than they should, allowing them to profit from external opportunities or users could end up with a lower exchange rate than what they should have received.

Remediations to Consider

  • Consider implementing a price feed for USDC and use it as a reference to calculate the amount out in redeem() and maxUstbRedemptionAmount().
Response by Superstate

Superstate has a Circle mint account, where we can always redeem 1 USDC for 1 USD

M-2

Incorrect validation for USTB price feed data

Topic
Protocol Design
Status
Impact
Medium
Likelihood
High
This specific Chainlink Oracle feed uses 6 decimals. Since the logic now uses the `CHAINLINK_FEED_DECIMALS` the deployed contract will accrue for any decimal value correctly.

In the _getChainlinkPrice() function when performing validation, _isBadData is determined using following expression:

(, int256 _answer,, uint256 _chainlinkUpdatedAt,) =
            AggregatorV3Interface(CHAINLINK_FEED_ADDRESS).latestRoundData();

// If data is stale or below first price, set bad data to true and return
// 10_000_000 is $10.000000 in the oracle format, that was our starting NAV per Share price for USTB
// The oracle should never return a price much lower than this
_isBadData = _answer <= 7_000_000 || ((block.timestamp - _chainlinkUpdatedAt) > maximumOracleDelay);

Reference: Redemption.sol#L142-148

Part of the validation _answer <= 7_000_000 implicitly assumes that _answer is a number with 6 decimal places of precision.

However, most of the non-ETH pairs return values with 8 decimal places of precision, while price feeds with ETH pairs have 18 decimal places of precision. As a result, if the USTB chainlink feed is indeed 8 decimal places of precision due to the incorrect assumption, this validation will be incorrect. For example, even if the result from the price feed is $0.08, it will still pass the validation.

Remediations to consider

  • Take into account CHAINLINK_FEED_PRECISION for USTB feed when evaluating _answer price data.
Q-1

Contract could have a pause function

Topic
Protocol Design
Status
Quality Impact
Medium

Although the Redemption contract can be indirectly paused or stopped by setting the maximumOracleDelay to zero or withdrawing all assets as an admin, consider adding pause features and checks to allow admin control and user transparency on potential contract states and unwanted external financial scenarios where redemptions should not be executed.

Q-2

Using constant variables for magic numbers

Topic
Best Practice
Status
Quality Impact
Low

In the _getChainlinPrice() function, the answer value comparison is hard coded to 7_000_000 as safety insurance. Consider having a constant variable for this value to avoid having magic numbers.

Q-3

Withdrawing max USDC not possible

Topic
Protocol Design
Status
Quality Impact
Low

Compound supports withdrawing the whole balance when type(uint256).max is provided as an amount to withdraw. However, in the admin callable withdraw() function, there is a validation that prevents this feature from being utilized:

if (balance < amount) revert InsufficientBalance();

Reference: Redemption.sol#L206

As a result, withdrawals of max amounts may leave dust amounts of USDC in the Compound contract.

Remediations to consider

  • Skip token balance validation when the provided token is a COMPOUND token and leave it to underlying Compound contract implementation to generate an error in case the requested amount is too big.
Q-4

Admin is always the withdrawer and depositor

Topic
Protocol Design
Status
Wont Do
Quality Impact
Low

In withdraw() and deposit() functions, corresponding Withdraw() and Deposit() events are emitted. Both of these events, when emitted, set withdrawer and depositor values to msg.sender, which for both functions is unchangeable ADMIN. Since no added value exists in including fixed values within events, consider removing withdrawer and depositor arguments from these two events.

Response by Superstate

Not an issue for us and may change in future version.

I-1

Compound supply and withdrawal can be paused

Topic
Third-party Integration
Impact
Informational

The Redemption contract integrates with Compound Comet contracts to supply and withdraw assets, generating yield with the USDC deposited in the contract and withdrawing it when users execute redeem(). It is worth noticing that Compound contracts can pause both supply and withdrawal via the Pause guardian, a governance-elected entity. If paused, the contract will be stale, disallowing the user’s redemption and protocol’s deposit and withdrawals.

I-2

Onchain redemptions may provide a trading advantage

Topic
Protocol Design
Impact
Informational

Since on-chain redemptions are available even when traditional markets are closed (e.g., weekends, holidays), any market-generating event that could reduce USTB's NAV on the following trading day could be a front-ran on-chain by the users through this redemption contract.

Disclaimer

Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.

Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.

The scope of this report and review is limited to a review of only the code presented by the Superstate team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.