Array Capital

Smart Contract Audit Report

Audit Summary

Array Capital Audit Report Array Capital is building a new IDO platform where users can purchase the team's token in exchange for ETH.

For this audit, we reviewed the LaunchpadIDO contract provided to us by the project team.

Audit Findings

High findings were identified and the team must resolve these issues.
Date: April 14th, 2023.
Date: April 15th, 2023 to reflect the team's update regarding Finding #1.

Finding #1 - LaunchpadIDO - High

Description: The platform intends to allow users to purchase tokens using the fundToken set by the team, but the internalBuyTokens() function requires the transaction's ETH amount to be greater than or equal to the required price.
require(msg.value >= requiredValue, "Sale: low balance.");
Risk/Impact: Users will not be able to initiate a purchase using the contract's fundToken as the relevant buyTokens() function is not payable.
Recommendation: In the internalBuyTokens() function, the team should calculate a separate required value if the purchase is with the fundToken and check that the value parameter is greater than or equal to this value. Any excess tokens should later be returned to the user. Alternatively, the team could remove the relevant buyTokens() function to only allow purchases to be made with ETH.
Update: The team intends to only allow ETH as the contract's payment method.

Finding #2 - LaunchpadIDO - Low

Description: The internalBuyTokens() function transfers any excess ETH from the contract to the caller using the transfer() function instead of the call() function.
Risk/Impact: The transfer() function uses a hardcoded gas amount of 2300, meaning transactions could run out of gas as the caller is transferred ETH.
Recommendation: The team should use .call.value{...}("") instead as it does not have a gas limitation.
Resolution: The team has not yet addressed this issue.

Finding #3 - LaunchpadIDO - Informational

Description: The second if-statement below in checkAccountAllowedToBuy() is redundant as it will always return true because if levelsEnabled were false, the function's return statement would have already been executed.
if (!levelsEnabled) {
   return (maxSell, minSell);  
}

if (levelsEnabled) {
Recommendation: The second if-condition above could be removed from the checkAccountAllowedToBuy() function as follows for additional gas savings on each call:
if (!levelsEnabled) {
   return (maxSell, minSell);  
}
   if (levelsOpenAll()) {
         (, uint256 fcfsAllocation) = getUserLevelState(userWeight);
         require(fcfsAllocation > 0, "Sale: user does not have FCFS allocation");

         return (fcfsAllocation, minSell);
   }

Finding #4 - LaunchpadIDO - Informational

Description: The setTimeline() function is redundant as it calls the external setFCFSDuration() function which also uses the onlyOwnerOrAdmin() modifier.
function setFCFSDuration(uint256 newDuration) public onlyOwnerOrAdmin {
	fcfsDuration = newDuration;
	emit FCFSDurationChanged(duration);
}

function setTimeline(uint256 _fcfsDuration) external onlyOwnerOrAdmin {
	setFCFSDuration(_fcfsDuration);
}
Recommendation: The setTimeline() function could be removed to reduce contract size and deployment costs.

Finding #5 - LaunchpadIDO - Informational

Description: Although the SafeMath library is utilized, the contract is implemented with Solidity v0.8.x which has built-in overflow checks.
Recommendation: SafeMath could be safely removed to reduce contract size, deployment costs, and gas costs on all transactions that utilize it.

Contract Overview

  • This contract is used to facilitate an IDO for a token set by the team.
  • The team will set the start time, duration of the sale, price rate, and number of tokens for sale upon initialization.
  • Users can initiate a purchase when the sale is active by specifying an amount of ETH, a desired amount of tokens to receive, and a weight value.
  • The contract uses an off-chain generated Merkle tree provided by the team to store and verify addresses that are whitelisted for purchases.
  • If the user's address along with the specified weight value is not verified, the user's weight value for the transaction is set to zero.
  • If the "Levels" functionality is currently disabled by the team, any user can initiate a purchase and the contract's minimum and maximum buy amounts will be applied to the transaction.
  • If the "Open-all-levels" functionality is enabled by the team or the "FCFS" duration of the sale has passed, any user can initiate a purchase if the contract's maximum sell amount is set to any value other than zero. Otherwise, the user must have been added to the team's whitelist.
  • Whitelisted users are not subject to a minimum buy amount, and their maximum buy amount is determined by their weight.
  • The number of tokens allocated to the user is calculated based on the current ETH price and the price rate set by the team.
  • The contract uses a Chainlink Aggregator to fetch the current price off-chain based on the latest "round data" of the contract. Our team has not reviewed the off-chain logic so we cannot provide an assessment with regard to security.
  • The supplied ETH for payment must at least cover the calculated cost of the transaction. Any excess ETH is returned to the user.
  • The number of tokens allocated to the user must exceed the minimum amount for the transaction.
  • The total number of tokens allocated to the user must not exceed the maximum amount for the transaction if the contract's maximum buy amount is currently set to any value other than zero.
  • Tokens can no longer be purchased once the total number of tokens for sale has been reached.
  • The owner or Admin can add new claiming rounds at any time. Each round contains an unlock time and a percentage of tokens that each user is eligible to claim. This process should only be performed by the team one time.
  • Upon adding a claiming round, the team will also set the address of the token that can be claimed.
  • Once a claiming round's unlock time has elapsed, any user can specify an account that has bought tokens, and the specified account will receive tokens based on the round's claim percentage.

  • The owner can unlock any existing claiming round at any time.
  • The owner can extend the unlock time of a claiming round at any time.
  • The owner or Admin can pause/unpause claiming at any time.
  • The owner or Admin can update the claim token before the first claiming round unlock time has been reached.
  • The owner or Admin can set the Minimum sell value to any value up to the current Maximum sell value. If the Maximum sell value is zero, the Minimum sell value can be set to any value.
  • The owner or Admin can set the Maximum sell value to any value greater than or equal to the current Minimum sell value. If the Minimum sell value is zero, the Maximum sell value can be set to any value.
  • The owner or Admin can update the start time of the sale to any value at any time.
  • The owner or Admin can update the sale duration to any value at any time.
  • The owner or Admin can update FCFS duration to any value at any time.
  • The owner or Admin can update the total number of tokens for sale to any value when the sale is not live. If the sale is live, the team can only increase the total number of tokens for sale.
  • The owner or Admin can update the price rate to any value when the sale is not live.
  • The owner or Admin can update the price feed Aggregator contract at any time.
  • The owner or Admin can update the Merkle root to any value when the sale is not live.
  • The owner or Admin can enable/disable the Levels functionality at any time.
  • The owner or Admin can enable/disable the Open-all-levels functionality at any time.
  • The owner or Admin can toggle whether purchases can be made with ETH or the "FundToken" at any time.
  • The owner or Admin can update the "FundToken" address at any time.
  • The owner or Admin can send all of the tokens or ETH from the contract to the Withdraw address at any time.
  • The owner or Admin can cancel the sale any time before the start time has passed.
  • The contract utilizes ReentrancyGuard to prevent reentrancy attacks in applicable functions.
  • As the contract is implemented with Solidity v0.8.x, it is safe from any possible overflows/underflows.

Audit Results

Vulnerability Category Notes Result
Arbitrary Jump/Storage Write N/A PASS
Centralization of Control
  • The owner or Admin can pause/unpause claiming at any time.
  • The team can upgrade the contract at any time.
WARNING
Compiler Issues N/A PASS
Delegate Call to Untrusted Contract N/A PASS
Dependence on Predictable Variables N/A PASS
Ether/Token Theft N/A PASS
Flash Loans N/A PASS
Front Running N/A PASS
Improper Events N/A PASS
Improper Authorization Scheme N/A PASS
Integer Over/Underflow N/A PASS
Logical Issues The platform intends to allow users to purchase tokens using the fundToken set by the team, but the internalBuyTokens() function always requires the transaction's ETH amount to be greater than or equal to the required price. WARNING
Oracle Issues N/A PASS
Outdated Compiler Version N/A PASS
Race Conditions N/A PASS
Reentrancy N/A PASS
Signature Issues N/A PASS
Sybil Attack N/A PASS
Unbounded Loops N/A PASS
Unused Code N/A PASS
Overall Contract Safety   WARNING

Inheritance Chart

Smart Contract Audit - Inheritance

Function Graph

Smart Contract Audit - Graph

Functions Overview


 ($) = payable function
 # = non-constant function
 
 Int = Internal
 Ext = External
 Pub = Public

 +  ReentrancyGuard 
    - [Pub]  #
    - [Prv] _nonReentrantBefore #
    - [Prv] _nonReentrantAfter #
    - [Int] _reentrancyGuardEntered

 +  Context 
    - [Int] _msgSender
    - [Int] _msgData

 +  Ownable (Context)
    - [Pub]  #
    - [Pub] owner
    - [Int] _checkOwner
    - [Pub] renounceOwnership #
       - modifiers: onlyOwner
    - [Pub] transferOwnership #
       - modifiers: onlyOwner
    - [Int] _transferOwnership #

 + [Int] IERC20 
    - [Ext] totalSupply
    - [Ext] balanceOf
    - [Ext] transfer #
    - [Ext] allowance
    - [Ext] approve #
    - [Ext] transferFrom #

 + [Int] IERC20Permit 
    - [Ext] permit #
    - [Ext] nonces
    - [Ext] DOMAIN_SEPARATOR

 + [Lib] Address 
    - [Int] isContract
    - [Int] sendValue #
    - [Int] functionCall #
    - [Int] functionCall #
    - [Int] functionCallWithValue #
    - [Int] functionCallWithValue #
    - [Int] functionStaticCall
    - [Int] functionStaticCall
    - [Int] functionDelegateCall #
    - [Int] functionDelegateCall #
    - [Int] verifyCallResultFromTarget
    - [Int] verifyCallResult
    - [Prv] _revert

 + [Lib] SafeERC20 
    - [Int] safeTransfer #
    - [Int] safeTransferFrom #
    - [Int] safeApprove #
    - [Int] safeIncreaseAllowance #
    - [Int] safeDecreaseAllowance #
    - [Int] forceApprove #
    - [Int] safePermit #
    - [Prv] _callOptionalReturn #
    - [Prv] _callOptionalReturnBool #

 + [Lib] MerkleProof 
    - [Int] verify
    - [Int] verifyCalldata
    - [Int] processProof
    - [Int] processProofCalldata
    - [Int] multiProofVerify
    - [Int] multiProofVerifyCalldata
    - [Int] processMultiProof
    - [Int] processMultiProofCalldata
    - [Prv] _hashPair
    - [Prv] _efficientHash

 + [Lib] SafeMath 
    - [Int] tryAdd
    - [Int] trySub
    - [Int] tryMul
    - [Int] tryDiv
    - [Int] tryMod
    - [Int] add
    - [Int] sub
    - [Int] mul
    - [Int] div
    - [Int] mod
    - [Int] sub
    - [Int] div
    - [Int] mod

 + [Lib] EnumerableSet 
    - [Prv] _add #
    - [Prv] _remove #
    - [Prv] _contains
    - [Prv] _length
    - [Prv] _at
    - [Prv] _values
    - [Int] add #
    - [Int] remove #
    - [Int] contains
    - [Int] length
    - [Int] at
    - [Int] values
    - [Int] add #
    - [Int] remove #
    - [Int] contains
    - [Int] length
    - [Int] at
    - [Int] values
    - [Int] add #
    - [Int] remove #
    - [Int] contains
    - [Int] length
    - [Int] at
    - [Int] values

 + [Lib] AddressUpgradeable 
    - [Int] isContract
    - [Int] sendValue #
    - [Int] functionCall #
    - [Int] functionCall #
    - [Int] functionCallWithValue #
    - [Int] functionCallWithValue #
    - [Int] functionStaticCall
    - [Int] functionStaticCall
    - [Int] verifyCallResultFromTarget
    - [Int] verifyCallResult
    - [Prv] _revert

 +  Initializable 
    - [Int] _disableInitializers #
    - [Int] _getInitializedVersion
    - [Int] _isInitializing

 +  ContextUpgradeable (Initializable)
    - [Int] __Context_init #
       - modifiers: onlyInitializing
    - [Int] __Context_init_unchained #
       - modifiers: onlyInitializing
    - [Int] _msgSender
    - [Int] _msgData

 +  OwnableUpgradeable (Initializable, ContextUpgradeable)
    - [Int] __Ownable_init #
       - modifiers: onlyInitializing
    - [Int] __Ownable_init_unchained #
       - modifiers: onlyInitializing
    - [Pub] owner
    - [Int] _checkOwner
    - [Pub] renounceOwnership #
       - modifiers: onlyOwner
    - [Pub] transferOwnership #
       - modifiers: onlyOwner
    - [Int] _transferOwnership #

 + [Int] IAccessControlUpgradeable 
    - [Ext] hasRole
    - [Ext] getRoleAdmin
    - [Ext] grantRole #
    - [Ext] revokeRole #
    - [Ext] renounceRole #

 + [Lib] MathUpgradeable 
    - [Int] max
    - [Int] min
    - [Int] average
    - [Int] ceilDiv
    - [Int] mulDiv
    - [Int] mulDiv
    - [Int] sqrt
    - [Int] sqrt
    - [Int] log2
    - [Int] log2
    - [Int] log10
    - [Int] log10
    - [Int] log256
    - [Int] log256

 + [Lib] SignedMathUpgradeable 
    - [Int] max
    - [Int] min
    - [Int] average
    - [Int] abs

 + [Lib] StringsUpgradeable 
    - [Int] toString
    - [Int] toString
    - [Int] toHexString
    - [Int] toHexString
    - [Int] toHexString
    - [Int] equal

 + [Int] IERC165Upgradeable 
    - [Ext] supportsInterface

 +  ERC165Upgradeable (Initializable, IERC165Upgradeable)
    - [Int] __ERC165_init #
       - modifiers: onlyInitializing
    - [Int] __ERC165_init_unchained #
       - modifiers: onlyInitializing
    - [Pub] supportsInterface

 +  AccessControlUpgradeable (Initializable, ContextUpgradeable, IAccessControlUpgradeable, ERC165Upgradeable)
    - [Int] __AccessControl_init #
       - modifiers: onlyInitializing
    - [Int] __AccessControl_init_unchained #
       - modifiers: onlyInitializing
    - [Pub] supportsInterface
    - [Pub] hasRole
    - [Int] _checkRole
    - [Int] _checkRole
    - [Pub] getRoleAdmin
    - [Pub] grantRole #
       - modifiers: onlyRole
    - [Pub] revokeRole #
       - modifiers: onlyRole
    - [Pub] renounceRole #
    - [Int] _setupRole #
    - [Int] _setRoleAdmin #
    - [Int] _grantRole #
    - [Int] _revokeRole #

 +  AdminableUpgradeable (Initializable, OwnableUpgradeable, AccessControlUpgradeable)
    - [Pub] __initialize #
       - modifiers: initializer

 +  WithLimits (AdminableUpgradeable)
    - [Ext] getMinMaxLimits
    - [Pub] setMin #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] setMax #
       - modifiers: onlyOwnerOrAdmin

 +  Timed (AdminableUpgradeable)
    - [Int] _Timed_Init #
    - [Pub] isLive
    - [Pub] isFcfsTime
    - [Pub] getEndTime
    - [Pub] setStartTime #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] setDuration #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] setFCFSDuration #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] setTimeline #
       - modifiers: onlyOwnerOrAdmin

 + [Int] AggregatorV3Interface 
    - [Ext] decimals
    - [Ext] description
    - [Ext] version
    - [Ext] getRoundData
    - [Ext] latestRoundData

 +  GeneralIDO (AdminableUpgradeable, Timed)
    - [Int] _General_Ido_Init #
    - [Ext] balanceOf
    - [Ext] setTokensForSale #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] calculatePurchaseAmount
    - [Pub] calculatePurchaseAmountUSD
    - [Pub] calculateNativeRequiredAmount
    - [Pub] setRate #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] setPriceAggregator #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] getRateNative
    - [Pub] getNativePriceUSD
    - [Int] stringsEqual

 +  WithLevelsSale (AdminableUpgradeable, Timed, GeneralIDO, WithLimits)
    - [Pub] levelsOpenAll
    - [Pub] setTierUsersRoot #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] getUserLevelState
    - [Ext] toggleLevels #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] openForAllLevels #
       - modifiers: onlyOwnerOrAdmin

 +  Withdrawable (AdminableUpgradeable)
    - [Int] _Withdraw_Init #
    - [Ext] setFundToken #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] withdrawAll #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] withdrawBalance #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] withdrawToken #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] getWithdrawAddress

 +  WithCancelable (AdminableUpgradeable, Timed)
    - [Pub] cancelIdo #
       - modifiers: onlyOwnerOrAdmin

 +  WithClaimer (AdminableUpgradeable, GeneralIDO)
    - [Pub] setClaimInfo #
       - modifiers: onlyOwnerOrAdmin
    - [Pub] getAllClaims
    - [Ext] getRemainingAccountAmount
    - [Ext] getClaimableAccountAmount
    - [Int] isClaimable
    - [Int] getClaimAmount
    - [Ext] pauseClaiming #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] setToken #
       - modifiers: onlyOwnerOrAdmin
    - [Ext] delayClaim #
       - modifiers: onlyOwner
    - [Ext] releaseClaim #
       - modifiers: onlyOwner
    - [Ext] claim #
    - [Ext] getClaims

 +  LaunchpadIDO (AdminableUpgradeable, ReentrancyGuard, Timed, GeneralIDO, Withdrawable, WithLimits, WithLevelsSale, WithCancelable, WithClaimer)
    - [Pub] initialize #
    - [Pub] buyTokens ($)
       - modifiers: ongoingSale,nonReentrant,notCanceled
    - [Pub] buyTokens #
       - modifiers: ongoingSale,nonReentrant,notCanceled
    - [Prv] internalBuyTokens #
    - [Prv] checkAccountAllowedToBuy
    - [Pub] getParticipants
    - [Prv] max

About SourceHat

SourceHat has quickly grown to have one of the most experienced and well-equipped smart contract auditing teams in the industry. Our team has conducted 1800+ solidity smart contract audits covering all major project types and protocols, securing a total of over $50 billion U.S. dollars in on-chain value!
Our firm is well-reputed in the community and is trusted as a top smart contract auditing company for the review of solidity code, no matter how complex. Our team of experienced solidity smart contract auditors performs audits for tokens, NFTs, crowdsales, marketplaces, gambling games, financial protocols, and more!

Contact us today to get a free quote for a smart contract audit of your project!

What is a SourceHat Audit?

Typically, a smart contract audit is a comprehensive review process designed to discover logical errors, security vulnerabilities, and optimization opportunities within code. A SourceHat Audit takes this a step further by verifying economic logic to ensure the stability of smart contracts and highlighting privileged functionality to create a report that is easy to understand for developers and community members alike.

How Do I Interpret the Findings?

Each of our Findings will be labeled with a Severity level. We always recommend the team resolve High, Medium, and Low severity findings prior to deploying the code to the mainnet. Here is a breakdown on what each Severity level means for the project:

  • High severity indicates that the issue puts a large number of users' funds at risk and has a high probability of exploitation, or the smart contract contains serious logical issues which can prevent the code from operating as intended.
  • Medium severity issues are those which place at least some users' funds at risk and has a medium to high probability of exploitation.
  • Low severity issues have a relatively minor risk association; these issues have a low probability of occurring or may have a minimal impact.
  • Informational issues pose no immediate risk, but inform the project team of opportunities for gas optimizations and following smart contract security best practices.