Prepared by:
HALBORN
Last Updated 04/25/2024
Date of Engagement by: May 25th, 2023 - June 22nd, 2023
100% of all REPORTED Findings have been addressed
All findings
9
Critical
0
High
0
Medium
2
Low
5
Informational
2
Substance Exchange is a Perpetual Decentralized Exchange
where users can interact with futures
and options
and also can be Liquidity Providers
earning from traders.
\client engaged Halborn
to conduct a security assessment on their smart contracts beginning on 2023-05-25 and ending on 2023-06-22. The security assessment was scoped to the smart contracts provided in the Substance Exchange V3 GitHub repository. Commit hashes and further details can be found in the Scope section of this report.
Halborn was provided 4 weeks for the engagement and assigned a full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.
The purpose of the assessment is to:
Identify potential security issues within the smart contracts.
Ensure that smart contract functionality operates as intended.
In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were addressed and accepted by \client. The most concerning issues were found in the Chainlink integration:
Chainlink latestrounddata
might be stale or incorrect
Chainlink Arbitrum sequencer is not verified to be online
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:
Research into architecture and purpose
Smart contract manual code review and walkthrough
Graphing out functionality and contract logic/connectivity/functions (solgraph
)
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes
Manual testing by custom scripts
Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX
)
Static Analysis of security for scoped contract, and imported functions. (Slither
)
Testnet deployment (Foundry
, Brownie
)
Code repositories:
Substance Exchange V1
Repository: SubstanceExchangeV1
Commit ID: 136369e88c04d21a25fecbcf8a4f25d6363ee035
Remediation Plan Commit ID: 7717277a15aef6b703a5cf9670509b2f0b1bd3fc
Smart contracts in scope:
Delegatable (src/core/Delegatable.sol
)
DelegationHub (src/core/DelegationHub.sol
)
ExchangeManager (src/core/ExchangeManager.sol
)
LiquidityPool (src/core/LiquidityPool.sol
)
SLPToken (src/core/SLPToken.sol
)
SubstanceProxy (src/core/SubstanceProxy.sol
)
SubstanceUSD (src/core/SubstanceUSD.sol
)
UserBalance (src/core/UserBalance.sol
)
BaseFuture (src/core/future/BaseFuture.sol
)
FutureFactory (src/core/future/FutureFactory.sol
)
FutureLong (src/core/future/FutureLong.sol
)
FutureLongV2 (src/core/future/FutureLongV2.sol
)
FutureShort (src/core/future/FutureShort.sol
)
FutureManager (src/core/future/FutureManager.sol
)
Option (src/core/option/Option.sol
)
OptionFactory (src/core/option/OptionFactory.sol
)
OptionManager (src/core/option/OptionManager.sol
)
Swap (src/core/swap/Swap.sol
)
SwapManager (src/core/swap/SwapManager.sol
)
GradualVester (src/core/token/GradualVester.sol
)
StakingReward (src/core/token/StakingReward.sol
)
SubstanceXToken (src/core/token/SubstanceXToken.sol
)
Struct (src/core/libraries/Struct.sol
)
TransferHelper (src/core/libraries/TransferHelper.sol
)
Delegatable (src/core/Delegatable.sol
)
DelegationHub (src/core/DelegationHub.sol
)
ExchangeManager (src/core/ExchangeManager.sol
)
LiquidityPool (src/core/LiquidityPool.sol
)
SLPToken (src/core/SLPToken.sol
)
SubstanceProxy (src/core/SubstanceProxy.sol
)
SubstanceUSD (src/core/SubstanceUSD.sol
)
UserBalance (src/core/UserBalance.sol
)
BaseFuture (src/core/future/BaseFuture.sol
)
FutureFactory (src/core/future/FutureFactory.sol
)
FutureLong (src/core/future/FutureLong.sol
)
FutureLongV2 (src/core/future/FutureLongV2.sol
)
FutureShort (src/core/future/FutureShort.sol
)
FutureManager (src/core/future/FutureManager.sol
)
Option (src/core/option/Option.sol
)
OptionFactory (src/core/option/OptionFactory.sol
)
OptionManager (src/core/option/OptionManager.sol
)
Swap (src/core/swap/Swap.sol
)
SwapManager (src/core/swap/SwapManager.sol
)
GradualVester (src/core/token/GradualVester.sol
)
StakingReward (src/core/token/StakingReward.sol
)
SubstanceXToken (src/core/token/SubstanceXToken.sol
)
Struct (src/core/libraries/Struct.sol
)
TransferHelper (src/core/libraries/TransferHelper.sol
)
Out-of-scope:
third-party libraries and dependencies
economic attacks
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Confidentiality (C) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
2
Low
5
Informational
2
Security analysis | Risk level | Remediation Date |
---|---|---|
CHAINLINK LATESTROUNDDATA MIGHT BE STALE OR INCORRECT | Medium | Solved - 07/11/2023 |
MISSING CHAINLINK ARBITRUM SEQUENCER HEALTH CHECK | Medium | Risk Accepted - 07/15/2023 |
USING TRANSFER INSTEAD OF SAFETRANSFER | Low | Risk Accepted - 07/15/2023 |
FEEONTRANSFER AND BURNONTRANSFER TOKENS ARE NOT SUPPORTED | Low | Risk Accepted - 07/15/2023 |
CENTRALIZATION RISK: PRODUCT MANAGER CAN WITHDRAW ARBITRARY AMOUNTS FROM LIQUIDITY POOLS | Low | Risk Accepted - 07/15/2023 |
CENTRALIZATION RISK: PRODUCT MANAGER CAN ALTER TOKEN RESERVES INDICATORS | Low | Risk Accepted - 07/15/2023 |
POTENTIAL ACCESS CONTROL BYPASS | Low | Risk Accepted - 07/15/2023 |
USING ERC721A INSTEAD OF ERC721 FOR MINTING ONLY 1 NFT AT A TIME | Informational | Solved - 07/10/2023 |
MISSING FEE RATES SANITY CHECKS | Informational | Acknowledged - 07/15/2023 |
// Medium
Substance Exchange uses Chainlink as its price oracle. When buying or selling sUSD
, the SubstanceUSD
contract queries Chainlink for the underlying token price using the latestRoundData()
function. This function returns uint80 roundId
, int256 answer
, uint256 startedAt
, uint256 updatedAt
and uint80 answeredInRound
. roundId
denotes the identifier of the most recent update round, answer
is the price of the asset, startedAt
is the timestamp at which the round started and updatedAt
is the timestamp at which the feed was updated. The getPrice()
function does not check if the feed was updated at the most recent round nor does it verify the update timestamp against the current time, and this can result in accepting stale data which may threaten the stability of the exchange in a volatile market.
function getPrice(address token, bool min) public view returns (uint256 price) {
address oracle = underlyingToken[token].oracle;
if (oracle == address(0)) {
revert SubstanceUSD__InvalidToken();
}
(, int256 oraclePrice, , , ) = AggregatorV3Interface(oracle).latestRoundData();
if (oraclePrice <= 0) {
revert SubstanceUSD__InvalidOraclePrice();
}
uint8 pDecimals = AggregatorV3Interface(oracle).decimals();
price = (uint256(oraclePrice) * PRECISION) / (10**pDecimals);
price = min ? Math.min(PRECISION, price) : Math.max(PRECISION, price);
}
SOLVED: The \client team solved this issue in commit 7717277a.
// Medium
Arbitrum is a L2 blockchain leveraging Optimistic Rollups to integrate with the underlying L1. A node called sequencer
is tasked with submitting user transactions to the L1 and if it fails, communication between the two is impossible. The exchange does not verify if the sequencer is online, which may lead to unexpected behavior if submitting transactions to the Ethereum mainnet is blocked.
function getPrice(address token, bool min) public view returns (uint256 price) {
address oracle = underlyingToken[token].oracle;
if (oracle == address(0)) {
revert SubstanceUSD__InvalidToken();
}
(, int256 oraclePrice, , , ) = AggregatorV3Interface(oracle).latestRoundData();
if (oraclePrice <= 0) {
revert SubstanceUSD__InvalidOraclePrice();
}
uint8 pDecimals = AggregatorV3Interface(oracle).decimals();
price = (uint256(oraclePrice) * PRECISION) / (10**pDecimals);
price = min ? Math.min(PRECISION, price) : Math.max(PRECISION, price);
}
RISK ACCEPTED: The \client team accepted the risk of this issue.
// Low
Using transfer()
instead of safeTransfer()
when interacting with ERC20
tokens is not recommended because transfer()
does not provide the same level of error handling and safety measures.
The following contracts use transfer()
function:
RISK ACCEPTED: The \client team accepted the risk of this issue.
// Low
Whenever a transfer of tokens is executed (in a swap
, a deposit
, or while adding liquidity
), there's no check if the amount sent is equal to the amount actually received by the contract.
The safeTransferFrom
function calls transferFrom
internally in the token contract to execute the transfer. However, the balance is not verified before and after the transfer and the actual amount transferred may not be the same as the amount received in the case of a fee applied in the token contract. In the case of using a token of this kind, the liquidity providers may not be able to withdraw all of their liquidity.
function userDeposit(address _token, uint256 _amount) external {
_validTokenAddress(_token);
address user = msgSender();
IERC20(_token).safeTransferFrom(user, address(this), _amount);
userBalance[user][_token] += _amount;
emit Deposit(user, _token, _amount);
}
RISK ACCEPTED: The \client team accepted the risk of this issue.
// Low
The LiquidityPool
contract implements the external transfer
function, which allows any account with the ProductManger
role to withdraw arbitrary amounts of tokens from existing liquidity pools. In case such an account is compromised, the entire protocol liquidity is at risk.
function transfer(address _token, address _dist, uint256 _amount) external isProductManager {
poolAmount[_token] -= _amount;
IERC20(_token).transfer(_dist, _amount);
}
RISK ACCEPTED: The \client team accepted the risk of this issue.
// Low
The LiquidityPool
contract implements the increaseLiquidity()
and decreaseLiquidity()
functions. If called by the Product Manager
(which is a role assigned by the contract owner) they can alter the values reported by the token reserves trackers without actually touching the reserves, putting the exchange out of balance. This directly affects protocol accounting and may have negative consequences on the protocol and its users.
A good example of this could be a liquidity provider
withdrawing their liquidity
from the pool, where the transaction would revert.
function increaseLiquidity(address _token, uint256 _amount) external isProductManager {
_validTokenCheck(_token);
poolAmount[_token] += _amount;
}
function decreaseLiquidity(address _token, uint256 _amount) external isProductManager {
_validTokenCheck(_token);
poolAmount[_token] -= _amount;
}
RISK ACCEPTED: The \client team accepted the risk of this issue.
// Low
The ProductManager
role is used to grant access from some contracts to other contract's functions. There exists a scenario in which any user can be the owner of the entire protocol by means of using the hub contract's
delegate calls to call any other protocol contract if the Delegation Hub
contract is assigned the ProductManager
role for those other contracts, effectively granting anyone privileged access to many sensitive functions.
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
RISK ACCEPTED: The \client team accepted the risk of this issue.
// Informational
The StakingReward
contract uses the ERC721A
standard to store the users' staking weight and the corresponding rewards. ERC721A
is designed to allow multiple mints at the same time with so-called batch transfers
(the more tokens minted at the same time, the more gas efficient the operation is) and Substance Exchange is not implementing this core functionality in their protocol. In ERC721A
, NFT
transfers are more expensive because of the way NFT
owner accounts are stored.
contract StakingReward is Ownable, Delegatable, ERC721A {
For the purposes of this PoC, two different types of NFTs were created, one based on the ERC721A standard and one based on ERC721.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "../lib/erc721a/contracts/ERC721A.sol";
contract HalbornERC721A is ERC721A{
constructor() ERC721A("Substance Exchange Stake Azuki", "SEXSTAKE") {}
function mint(uint256 _quantity) external payable {
_mint(msg.sender, _quantity);
}
function transfer(address _to, uint256 tokenId) external {
transferFrom(msg.sender, _to, tokenId);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
contract HalbornERC721 is ERC721{
constructor() ERC721("Substance Exchange Stake OZ", "SEXSTAKE") {}
function mint(uint256 _tokenId) public{
_mint(msg.sender, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public {
_transfer(msg.sender, _to, _tokenId);
}
}
The following scenario was simulated:
SOLVED: The \client team solved this issue by replacing the ERC721A
standard with ERC721
in commit e45003fe.
// Informational
The exchange charges handling fees on certain operations. Fee rates are set by the contract owner and can be updated anytime. None of the setter functions does the sanity check of the provided fee rates, which may lead to contract owners introducing prohibitive or zero fees accidentally or by design.
function setFee(uint256 _mintFee, uint256 _burnFee) external onlyOwner {
mintFee = _mintFee;
burnFee = _burnFee;
}
function setMinExecutionFee(uint256 _minExecutionFee) external onlyOwner {
minExecutionFee = _minExecutionFee;
}
function setMinExecutionFee(uint256 _minExecutionFee) external onlyOwner {
minExecutionFee = _minExecutionFee;
}
function setMinExecutionFee(uint256 _minExecutionFee) external onlyOwner {
minExecutionFee = _minExecutionFee;
}
ACKNOWLEDGED: The \client team acknowledged this issue.
The manual testing phase included isolated testing and integration testing to assure the correct functionality of the whole protocol. Whether it is an isolated test or an integration test, all of them are focused on checking a particular component, feature or functionality is working as expected. They can be summarized and categorized as follows:
Tokens used in the protocol:
buy()
and sell()
returning the correct value from the oracles.Core contracts:
UserBalance
contract is properly handling deposits and users balances.DelegationHub
contract is properly making the delegateCalls
handling the delegations and keeps the entire protocol stable.LiquidityPool
contract was tested to make sure the liquidity providers
have their liquidity stored securely, and that users cannot act maliciously against any of the stakeholders. Furthermore, that the user's balances are correctly calculated by the protocol.Futures and Options:
Swaps:
Substance USD (sUSD): That users can use safely buy()
and sell()
returning the correct value from the oracles.
Substance Exchange (SEX): That the governance is working properly while vesting and staking.
It was checked that the UserBalance
contract is properly handling deposits and users balances.
DelegationHub
contract is properly making the delegateCalls
handling the delegations and keeps the entire protocol stable.The LiquidityPool
contract was tested to make sure the liquidity providers
have their liquidity stored securely, and that users cannot act maliciously against any of the stakeholders. Furthermore, that the user's balances are correctly calculated by the protocol.
That all the future trades are handled correctly there in the correct epoch and cannot be doubled or double spent.
That slippage tolerance is correctly handled
That swaps are working correctly with stable prices
For the tests 3 types of users were created:
- Owner / Deployer (who has the ownership of and permissions for the protocol)
- Liquidity Providers: The users interested on providing liquidity and obtaining yield passively (liqPr1
, liqPr2
, liqPr3
, and liqPr4
in the protocol)
- Traders: The users interested interacting with options and futures to obtain earnings actively (alice
, bobby
, carla
, and edgar
in the protocol)
The tests always start at the following state:
Below are given some examples of the tests performed:
liqPr1
is buying USD with all their stablecoins.liqPr2
is buying USD with half of their stablecoins.alice
deposits all her balance in the UserBalance
contract but is only buying USD with 7000 USDT, 3000 USDC, 1000 DAI and 1000 FRAXSTRATEGY:
Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repository and was able to compile them correctly into their abis and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base. The security team assessed all findings identified by the Slither software, however, findings with severity Information
and Optimization
are not included in the below results for the sake of report readability.
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for Delegatable.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \multicolumn{2}{| c |}{End of table for Delegatable.sol}\ \hline \endlastfoot\hline \textcolor{green}{Delegatable._checkOperator() (src/core/Delegatable.sol#25-30) is never used and should be removed } & \textcolor{green}{Low} \ \hline \end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for DelegationHub.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for DelegationHub.sol}\ \hline \endlastfoot\hline \textcolor{red}{Reentrancy in DelegationHub._aggregate(address,DelegationHub.Call[]) (src/core/DELEGATIONHUB.sol#109-148): External calls: (success,retData) = calli.target.call{value: val}(calli.payload) (src/core/DELEGATIONHUB.sol#127) State variables written after the call(s): senderOverride = address(0) (src/core/DELEGATIONHUB.sol#147) DelegationHub.senderOverride (src/core/DELEGATIONHUB.sol#21) can be used in cross function reentrancies: DelegationHub._aggregate(address,DelegationHub.Call[]) (src/core/DELEGATIONHUB.sol#109-148) DelegationHub.msgSender() (src/core/DELEGATIONHUB.sol#80-86) DelegationHub.receiveEthFromUserBalance() (src/core/DELEGATIONHUB.sol#49-54) } & \textcolor{red}{High} \
\hline \textcolor{red}{DelegationHub (src/core/DELEGATIONHUB.sol#16-151) is an upgradeable contract that does not protect its initialize functions: DelegationHub.initialize() (src/core/DELEGATIONHUB.sol#41-43). Anyone can delete the contract with: UUPSUpgradeable.upgradeTo(address) (lib/openzeppelin-contracts/contracts-upgradeable/proxy/utils/ UUPSUpgradeable.sol#74-77) UUPSUpgradeable.upgradeToAndCall(address,bytes) (lib/openzeppelin-contracts/contracts-upgradeable/proxy/utils/ UUPSUpgradeable.sol#89-92) } & \textcolor{red}{High} \
\hline \textcolor{yellow}{DelegationHub.setOperator(address[],bool[]).i (src/core/DELEGATIONHUB.sol#61) is a local variable never initialized ERC1967UpgradeUpgradeable._upgradeToAndCallUUPS(address,bytes,bool).slot (lib/openzeppelin-contracts/contracts-upgradeable/proxy/ERC1967/ ERC1967UpgradeUpgradeable.sol#84) is a local variable never initialized } & \textcolor{yellow}{Medium} \
\hline \textcolor{green}{DelegationHub.setOperator(address[],bool[]).i (src/core/DELEGATIONHUB.sol#61) is a local variable never initialized ERC1967UpgradeUpgradeable._upgradeToAndCallUUPS(address,bytes,bool).slot (lib/openzeppelin-contracts/contracts-upgradeable/proxy/ERC1967/ ERC1967UpgradeUpgradeable.sol#84) is a local variable never initialized } & \textcolor{green}{Low} \ \hline \end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for ExchangeManager.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for ExchangeManager.sol}\ \hline
\endlastfoot\hline \textcolor{red}{ExchangeManager (src/core/ExchangeManager.sol#16-100) is an upgradeable contract that does not protect its initialize functions: ExchangeManager.initialize(UserBalance, LiquidityPool, OptionManager, FutureManager, SwapManager) (src/core/ExchangeManager.sol#30-44). } & \textcolor{red}{High} \ \hline
\textcolor{yellow}{ExchangeManager.userClaimWithdrawLiquidity(uint256).i (src/core/ExchangeManager.sol#89) is a local variable never initialized } & \textcolor{yellow}{Medium} \ \hline
\textcolor{green}{ExchangeManager.setHub(address).hub (src/core/ExchangeManager.sol#46) shadows: Delegatable.hub (src/core/Delegatable.sol#10) (state variable) } & \textcolor{green}{Low} \ \hline
\end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for LiquidityPool.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for LiquidityPool.sol}\ \hline
\endlastfoot\hline \textcolor{red}{LiquidityPool.adminWithdrawBurnFees(address) (src/core/LiquidityPool.sol#183-190) ignores return value by IERC20(token).transfer(_collection,feesAvailable[token]) (src/core/LiquidityPool.sol#187) } & \textcolor{red}{High} \ \hline
\textcolor{red}{LiquidityPool.globalWithdrawToken(int256) (src/core/LiquidityPool.sol#315-384) ignores return value by IERC20(token_scope_1).transfer(userBalance,withdrawAmount) (src/core/LiquidityPool.sol#377) } & \textcolor{red}{High} \ \hline
\textcolor{red}{LiquidityPool.transfer(address,address,uint256) (src/core/LiquidityPool.sol#510-513) ignores return value by IERC20(_token).transfer(_dist,_amount) (src/core/LiquidityPool.sol#512) } & \textcolor{red}{High} \ \hline
\textcolor{yellow}{LiquidityPool.getSLPDiscountRatio(uint256,uint256,uint256) (src/core/LiquidityPool.sol#574-594) performs a multiplication on the result of a division: averageDiff = (initDiff + nextDiff) / 2 (src/core/LiquidityPool.sol#583) taxBps = (taxBasisPoints * averageDiff) / targetRatio (src/core/LiquidityPool.sol#589) } & \textcolor{yellow}{Medium} \ \hline
\textcolor{yellow}{Reentrancy in LiquidityPool.adminWithdrawBurnFees(address) (src/core/LiquidityPool.sol#183-190): External calls: IERC20(token).transfer(_collection,feesAvailable[token]) (src/core/LiquidityPool.sol#187) State variables written after the call(s): feesAvailable[token] = 0 (src/core/LiquidityPool.sol#188) LiquidityPool.feesAvailable (src/core/LiquidityPool.sol#61) can be used in cross function reentrancies: LiquidityPool.adminWithdrawBurnFees(address) (src/core/LiquidityPool.sol#183-190) LiquidityPool.feesAvailable (src/core/LiquidityPool.sol#61) LiquidityPool.globalWithdrawToken(int256) (src/core/LiquidityPool.sol#315-384) } & \textcolor{yellow}{Medium} \ \hline
\textcolor{yellow}{LiquidityPool.claimCurrentEpochLiquidityTokenPrices(uint256[]).i (src/core/LiquidityPool.sol#250) is a local variable never initialized } & \textcolor{yellow}{Medium} \ \hline
\textcolor{yellow}{LiquidityPool.globalWithdrawToken(int256).i (src/core/LiquidityPool.sol#331) is a local variable never initialized } & \textcolor{yellow}{Medium} \ \hline
\textcolor{green}{LiquidityPool._addValidLiquidityToken(address,uint8) (src/core/LiquidityPool.sol#161-169) has external calls inside a loop: IERC20(_tokenAddress).totalSupply() (src/core/LiquidityPool.sol#168) } & \textcolor{green}{Low} \ \hline
\end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for SLPToken.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for SLPToken.sol}\ \hline
\endlastfoot\hline
\textcolor{green}{SLPToken.setPool(address)._pool (src/core/SLPToken.sol#15) lacks a zero-check on : - pool = _pool (src/core/SLPToken.sol#16) } & \textcolor{green}{Low} \ \hline
\end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for SubstanceProxy.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for SubstanceProxy.sol}\ \hline
\endlastfoot\hline
\textcolor{yellow}{ERC1967Upgrade._upgradeToAndCallUUPS(address,bytes,bool).slot (lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol#78) is a local variable never initialized } & \textcolor{yellow}{Medium} \ \hline
\textcolor{yellow}{ERC1967Upgrade._upgradeToAndCall(address,bytes,bool) (lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol#59-64) ignores return value by Address.functionDelegateCall(newImplementation,data) (lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol#62) } & \textcolor{yellow}{Medium} \ \hline
\textcolor{green}{ERC1967Upgrade._upgradeToAndCallUUPS(address,bytes,bool) (lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol#71-85) ignores return value by IERC1822Proxiable(newImplementation).proxiableUUID() (lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol#78-82) } & \textcolor{green}{Low} \ \hline
\end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for SubstanceUSD.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for SubstanceUSD.sol}\ \hline
\endlastfoot\hline \textcolor{red}{SubstanceUSD (src/core/SubstanceUSD.sol#23-151) is an upgradeable contract that does not protect its initialize functions: SubstanceUSD.initialize(IUserBalance,uint256,uint256,address) (src/core/SubstanceUSD.sol#44-52). } & \textcolor{red}{High} \ \hline
\textcolor{yellow}{Math.mulDiv(uint256,uint256,uint256) (lib/openzeppelin-contracts/contracts/utils/math/Math.sol#55-134) performs a multiplication on the result of a division: - denominator = denominator / twos (lib/openzeppelin-contracts/contracts/utils/math/Math.sol#101) } & \textcolor{yellow}{Medium} \ \hline
\textcolor{green}{SubstanceUSD.setHub(address).hub (src/core/SubstanceUSD.sol#78) shadows: - Delegatable.hub (src/core/Delegatable.sol#10) (state variable) } & \textcolor{green}{Low} \ \hline
\end{longtable}
\small \begin{longtable}{| >{\raggedright}p{13cm} | c |} \hline \rowcolor{darkgray} \multicolumn{2}{| c |}{Slither results for UserBalance.sol}\ \hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endfirsthead
\hline \multicolumn{1}{| c |}{\textbf{Finding}} & \multicolumn{1}{| c |}{\textbf{Impact}}\ \hline \endhead
\hline \endfoot
\hline \multicolumn{2}{| c |}{End of table for UserBalance.sol}\ \hline
\endlastfoot\hline \textcolor{red}{UserBalance.transfer(address,address,address,uint256) (src/core/UserBalance.sol#148-156) ignores return value by IERC20(_token).transfer(_to,_amount) (src/core/UserBalance.sol#155) } & \textcolor{red}{High} \ \hline
\end{longtable}
The findings obtained as a result of the Slither scan were reviewed. The majority of Slither findings were determined false-positives.
Halborn used automated security scanners to assist with detection of well-known security issues and to identify low-hanging fruits on the targets for this engagement. Among the tools used was MythX, a security analysis service for Ethereum smart contracts. MythX performed a scan on the smart contracts and sent the compiled results to the analyzers in order to locate any vulnerabilities.
src/core/DelegationHub.sol
src/core/ExchangeManager.sol
src/core/LiquidityPool.sol
src/core/SLPToken.sol
src/core/SubstanceProxy.sol
src/core/SubstanceUSD.sol
src/core/UserBalance.sol
src/core/future/BaseFuture.sol
src/core/future/FutureLong.sol
src/core/future/FutureLongV2.sol
src/core/future/FutureShort.sol
src/core/future/FutureManager.sol
src/core/option/Option.sol
src/core/option/OptionFactory.sol
src/core/option/OptionManager.sol
src/core/swap/Swap.sol
src/core/swap/SwapManager.sol
src/token/GradualVester.sol
src/token/SubstanceXToken.sol
src/libraries/Struct.sol
src/libraries/TransferHelper.sol
MythX did not identify any vulnerabilities in the contracts.
The findings obtained as a result of the MythX scan were examined, and they were not included in the report as they were determined false positives.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed