Security Audit
Nov 18th, 2024
Version 1.0.0
Presented by 0xMacro
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 from Nov 12th to Nov 15th, 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.
The following is an aggregation of issues found by the Macro Audit team:
Severity | Count | Acknowledged | Won't Do | Addressed |
---|---|---|---|---|
High | 2 | - | - | 2 |
Medium | 1 | - | - | 1 |
Low | 2 | - | - | 2 |
Code Quality | 5 | - | - | 5 |
Informational | 2 | - | - | - |
Superstate was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
The following source code was reviewed during the audit:
0be7d99b289ef36d878dcfcb0551c69546229850
add3883b24ffe8bfbc561ab5907657898064484d
fe49472d9574111bdb7cf3239cba381376c32aae
598e49d2e6090224a93a68224ae01c87a45a154d
Specifically, we audited the following contracts within the ustb repository:
Source Code | SHA256 |
---|---|
src/SuperstateToken.sol |
|
src/allowlist/AllowList.sol |
|
Additionally, the following contracts within onchain-redemptions repository were also reviewed:
Source Code | SHA256 |
---|---|
src/Redemption.sol |
|
src/RedemptionIdle.sol |
|
src/RedemptionYield.sol |
|
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.
Click on an issue to jump to it, or scroll down to see them all.
redemptionFee
variable type to uint256
We quantify issues in three parts:
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. |
A new feature in the Redemption
contract introduces a redemption fee. The redemption fee is initially set through the initializer, can be updated using setRedemptionFee()
, and can be calculated using calculateFee()
. In addition, in the calculateUstbIn()
, the redemption fee is considered when calculating how much USTB token the user may need to provide to obtain the desired USDC amount.
However, the redemption process flow, which includes RedemptionIdle.redeem()
and RedemptionYield.redeem()
together with underlying Redemption.calculateUsdcOut()
, does not consider the redemption fee at all. As a result, a redemption fee would not be charged in contradiction to the system specification and contract configuration.
Remediations to Consider
Redemption.calculateUsdcOut()
.In the SuperstateToken
contract, a new feature allows an end user to mint superstate tokens using subscribe()
. This function takes in stablecoins and mints SuperstateToken in the proper amount for the msg.sender
, depending on the current Net Asset Value per Share. SuperstateToken
, part of a regulation-compliant system, has extra token access constraints that allow only preapproved addresses to obtain tokens. These constraints are enforced in the internal mint wrapper function _mintLogic()
. This function is used by mint()
and bulkMint()
owner-accessible functions.
function _mintLogic(address dst, uint256 amount) internal {
if (!hasSufficientPermissions(dst)) revert InsufficientPermissions();
_mint(dst, amount);
emit Mint(msg.sender, dst, amount);
}
However, subscribe()
, which is publicly accessible, does not call _mintLogic()
but calls directly the underlying _mint()
function, which skips permission checking. As a result, non-approved addresses may obtain SuperstateToken in contradiction with system specifications.
Remediations to Consider
subscribe()
call to _mint()
with a call to the _mintLogic()
.In the Redemption
contract, calculateUstbIn()
function calculates how much superstate token should be provided to obtain the desired amount of USDC tokens in return. As part of the calculation, the redemption fee is also considered.
function calculateUstbIn(uint256 usdcOutAmount)
public
view
returns (uint256 ustbInAmount, uint256 usdPerUstbChainlinkRaw)
{
if (usdcOutAmount == 0) revert BadArgs();
**uint256 usdcOutAmountWithFee = usdcOutAmount + calculateFee(usdcOutAmount);**
(bool isBadData,, uint256 usdPerUstbChainlinkRaw_) = _getChainlinkPrice();
if (isBadData) revert BadChainlinkData();
usdPerUstbChainlinkRaw = usdPerUstbChainlinkRaw_;
ustbInAmount = (usdcOutAmountWithFee * CHAINLINK_FEED_PRECISION * SUPERSTATE_TOKEN_PRECISION)
/ (usdPerUstbChainlinkRaw * USDC_PRECISION);
}
However, the redemption fee is incorrectly calculated. Currently, a fee is applied to the net amount of USDC instead of the gross amount of USDC. As a result, the final result underestimates the redemption fee amount. In addition, when invoking redemption with the result of calculateUstbIn()
, the user will receive less than the desired usdcOutAmount
.
Remediations to Consider
calculateUstbIn()
implementation to apply fee on gross USDC amount.In the Redemption
contract, the calculateUstbIn()
function calculates the necessary superstate token amount to be provided to receive the desired amount of USDC.
ustbInAmount = (usdcOutAmountWithFee * CHAINLINK_FEED_PRECISION * SUPERSTATE_TOKEN_PRECISION)
/ (usdPerUstbChainlinkRaw * USDC_PRECISION);
However, due to integer division and rounding ustbInAmount
may underestimate the necessary superstate token amount to be provided to retrieve the desired amount of USDC. As a result, users may receive less than they initially asked for. On the user side, this would require a more complex integration implementation to handle this edge case.
Remediations to Consider
calculateUstbIn()
so that the user receives the desired amount of USDC when redeeming.In the SuperstateToken
contract, the _requireOnchainSubscriptionsEnabled()
function verifies necessary dependencies for enabling subscriptions. This includes verifying that the superstateOracle
, which provides real-time NAVS price, is configured and set.
function _requireOnchainSubscriptionsEnabled() internal view {
if (superstateOracle == address(0)) revert OnchainSubscriptionsDisabled();
}
However, this check skips to validate that maximumOracleDelay
is also set and has a value greater than 0. In cases when maximumOracleDelay
is not configured or has a default value of 0, subscriptions will also not work, as Oracle data will always be treated as stale data.
Remediations to Consider
_requireOnchainSubscriptionsEnabled()
check that maximumOracleDelay
is appropriately configured.The latest, version 2 of Allowlist
system component, diverges significantly in the implementation from the previous version. Storage layout, permission handling, and function interface differ. In addition, v2 of Allowlist is upgradeable, while version 1 wasn’t.
However, version 2 of Allowlist
tries to maintain backward interface compatibility even though it isn’t an end user-facing system component but rather a wrapped component used by the Superstate token contract and by the internal management system.
Due to the above, maintaining backward compatibility seems unnecessary. Consider removing unnecessary Allowlist version 1 features and code for the benefit of a more clean version 2 Allowlist implementation.
redemptionFee
variable type to uint256
In the Redemption
contract, the redemptionFee
is defined as uint96. If, in the next upgrade, a new variable is smaller than uint256 and can fit in the same slot, it may not require an update to the storage gap. However, if it is bigger, it will require a new slot and storage gap update.
To reduce potential storage layout slot misalignment in future upgrades, consider using uint256
for redemptionFee
instead of uint96
.
The Redemption
and Allowlist
contracts inherit from OwnableUpgredeable
. As a result, they contain the unnecessary functionality of renounceOwnership()
.
Consider overriding it and reverting whenever it is called so it is clear that it is not supported.
Additional changes in commit: https://github.com/superstateinc/onchain-redemptions/commit/7187bdfa59dab79cfba58dd341aa7a946b45771e
Consider updating natspec documentation in the following cases:
SuperstateToken
, the natspec for the setOracle()
function refers to the old, now obsolete, updateOracle()
function. Consider updating the natspec to keep it up to date.Redemption
contract, withdraw()
is an abstract function, and withdrawToSweepDestination()
relies on it. Since these functions are critical, add natspec documentation that concrete implementation of withdraw()
in child functions should implement proper access control.Additional changes in commit: https://github.com/superstateinc/onchain-redemptions/commit/233128c10fd54852d7bbdcefb522e45085a59bab
In the Redemption
contract, the sweepDestination
variable represents the address where the contract owner-controlled withdrawToSweepDestination()
will send the requested amount of USDC tokens. This variable is configured in initialize()
and can be updated using setSweepDestination()
.
However, the underlying implementation in _setSweepDestination()
does not have a check that the provided address value for sweepDestination
is valid and not 0 address.
To prevent asset loss, consider adding a zero address check for sweepDestination
, similar to what is done in SuperstateToken
.
The Superstate
system is a centralized system with various regulatory compliance requirements. A multi-sig account that is an authorized party in the system has extensive privileges that control all assets through configuration and privileged operations.
Various contracts within the Superstate
system, such as Superstate.sol
and Management.sol
, are implemented with hardcoded peg between USD and configured stablecoins. This is an explicit assumption within the system and an acceptable risk to the system owners. Even though potential malicious end users may attempt to exploit de-peg events, all system users are KYC, and the likelihood of this happening is minimal.
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.