Factor
Smart Contract Audit Report
Audit Summary
Factor is building a new staking and investing platform with governance.
For this audit, we reviewed the following contracts on the team's private GitHub repositories:
- Factor and BasketVault contracts at commit 2b5e5cb360077d31c86f0394fc4e536eafb23e20.
- VotingEscrow and Blocklist contracts at commit 921dc6967e2def2b885232b27ef3484c041d46e9.
Audit Findings
All findings have been resolved, though some centralized aspects are present.
Date: February 15th, 2023.
Updated: February 16th, 2023 to reflect changes to the Factor and BasketVault contracts from commit 921dc6967e2def2b885232b27ef3484c041d46e9 to commit 2b5e5cb360077d31c86f0394fc4e536eafb23e20.Finding #1 - BasketVault - Medium (Resolved)
Description: While a small number of BasketVault shares exist, the share price can be inflated to an extremely large value through a "donation" of a Basket token. If the share price is inflated to a large enough amount, users will not receive any shares for a donation.
Risk/Impact: Deposits can be stolen through frontrunning when a low number of BasketVault shares exist through the use of a donation.
Example Scenario:Recommendation: A precision multiplier should be added to the initial number of minted shares to ensure a low share price, or the team can make a non-trivial deposit once the contract is deployed to prevent any share price manipulation.
- Alice deposits 1 wei of the asset token for 1 wei of a share, the total supply is now 1 wei.
- Alice donates 100,000 of any Basket Token, with a 1:1 Basket Token to Asset Token example conversion rate for simplicity.
- Bob then deposits 1,000 asset tokens, expecting to receive 1000 shares.
- Bob's shares to be minted are calculated as (1000e18 * 1) / 100,000e18, which is rounded down to 0. Alice can then withdraw Bob's funds as she still holds the share supply.
Resolution: The Vault must now be "started" by the Admin before users can deposit, which involves a deposit into the Vault in exchange for shares. The team should ensure that the deposit amount is significant.
Finding #2 - BasketVault - Medium (Resolved)
Description: In the _getPrice() function, the following line containing division before multiplication may return inaccurate values due to truncation.
Risk/Impact: Users may receive a different number of shares or assets than intended due to truncated price values.uint256 priceUSDC = (uint256(basePrice) / uint256(quotePrice)) * 10 ** assetTokenDecimals;
Recommendation: The team should update the mentioned line to the following:
Resolution: The team has implemented the above recommendation.uint256 priceUSDC = uint256(basePrice) * (10 ** assetTokenDecimals) / uint256(quotePrice);
Contracts Overview
Factor Contract:
- ReentrancyGuard is utilized to prevent against reentrancy attacks in applicable functions.
- As the contracts are implemented with Solidity v0.8.x, they are safe from any possible overflows/underflows.
- The team must exercise caution and avoid using a fee-on-transfer token as the staking token or as a Basket token the BasketVault contract.
- The team must exercise caution when assigning the VotingEscrow staking token to avoid using a fee-on-transfer token or a token with less than 6 decimals.
VotingEscrow Contract:
- The initializer is minted 100 million Factor tokens upon initialization.
- No further minting functionality is present.
- Users can burn their tokens at any time to decrease the total supply.
- Each Factor token additionally represents votes intended to be used in a DAO where one token represents one vote.
- Users may delegate their votes to another address allowing them to vote on behalf of the user.
- Once votes are delegated, the user must explicitly delegate back to themselves to regain their votes.
- Users also have the option to delegate through the use of a signed message, allowing for a gasless delegation for the user.
- This contract complies with the ERC20 Upgradeable standard.
Blocklist Contract:
- A user's VotingEscrow balance is used to represent voting weight in a DAO system.
- Users receive a VotingEscrow balance by locking specified tokens in the contract.
- Users' voting power is weighted where the longer tokens are locked the greater the voting power.
- The maximum amount of time tokens can be locked is 4 years.
- After tokens have been locked, their voting power will decrease over time.
- Users' votes are stored once per checkpoint.
- Users may deposit additional tokens into an existing lock or extend the unlock time of an existing lock.
- Users may delegate their lock and voting power to a lock with a longer duration if desired.
- Once delegated, the lock's expiration is updated to the delegatee lock's expiration time.
- Users may withdraw tokens from an expired lock as long as their lock has been "undelegated".
- If withdrawing before a lock is expired, the user must pay a penalty, which is stored in the contract for the Penalty Recipient address.
- The penalty amount decreases linearly over the course of the span of the lock.
- Any address can withdraw accumulated penalties to the Penalty Recipient at any time.
- A contract cannot create, extend, delagate, or contribute to a lock if it has been blocked using the associated Blocklist contract.
- The Blocklist contract can remove delegation from any address at any time.
- The owner can permanently remove the unlock penalty at any time.
- The owner can update the penalty fee recipient address at any time.
- The owner can update the Blocklist contract at any time.
- Ownership can be transferred at any time.
- The team should not set the staking token as a fee-on-transfer token or a token with less than 6 decimals.
BasketVault Contract:
- This contract allows a Manager to block specific contracts from interacting with the VotingEscrow contract.
- The Manager and VotingEscrow addresses are set upon deployment and cannot be updated.
- The Manager can add a contract to the Block List at any time. This will also undelegate the specified contract address.
- Once blocked, a contract cannot be unblocked.
- This contract allows users to deposit any amount of the contract's specified asset token in exchange for minted BasketVault shares.
- Upon deposit, the amount is divided and swapped for tokens corresponding to each "Basket".
- The amount swapped for each Basket token is determined by the Basket's ratio.
- A 1% slippage is permitted in relation to the expected amount.
- The number of shares minted to the user is determined by the user's deposit amount in proportion to the total asset token value held by the contract.
- Each Basket's value is determined using its assigned "Base" and "Quote" Chainlink Oracles. These values are summed to determine the total asset token value.
- Users can alternatively input a desired share amount to mint, where the required deposit amount is calculated.
- Users can redeem their BasketVault shares at any time, or redeem other users' tokens if they have been granted an allowance.
- This swaps amounts of each Basket token back to the original asset token based on the user's redemption amount.
- BasketVault shares are burned from the user they are redeemed from.
- The deployer is granted the Manager and Admin Roles upon deployment.
- The Manager can pause the contract at any time.
- Deposits, withdrawals, redemptions, mints and BasketVault share transfers are all disabled while the contract is paused.
- This contract is intended to be used behind an upgradeable proxy.
Audit Results
Vulnerability Category | Notes | Result |
---|---|---|
Arbitrary Jump/Storage Write | N/A | PASS |
Centralization of Control |
|
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 | N/A | PASS |
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 | PASS |
Factor Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ [Int] IERC20Upgradeable
- [Ext] totalSupply
- [Ext] balanceOf
- [Ext] transfer #
- [Ext] allowance
- [Ext] approve #
- [Ext] transferFrom #
+ [Int] IERC20MetadataUpgradeable (IERC20Upgradeable)
- [Ext] name
- [Ext] symbol
- [Ext] decimals
+ [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
+ ERC20Upgradeable (Initializable, ContextUpgradeable, IERC20Upgradeable, IERC20MetadataUpgradeable)
- [Int] __ERC20_init #
- modifiers: onlyInitializing
- [Int] __ERC20_init_unchained #
- modifiers: onlyInitializing
- [Pub] name
- [Pub] symbol
- [Pub] decimals
- [Pub] totalSupply
- [Pub] balanceOf
- [Pub] transfer #
- [Pub] allowance
- [Pub] approve #
- [Pub] transferFrom #
- [Pub] increaseAllowance #
- [Pub] decreaseAllowance #
- [Int] _transfer #
- [Int] _mint #
- [Int] _burn #
- [Int] _approve #
- [Int] _spendAllowance #
- [Int] _beforeTokenTransfer #
- [Int] _afterTokenTransfer #
+ ERC20BurnableUpgradeable (Initializable, ContextUpgradeable, ERC20Upgradeable)
- [Int] __ERC20Burnable_init #
- modifiers: onlyInitializing
- [Int] __ERC20Burnable_init_unchained #
- modifiers: onlyInitializing
- [Pub] burn #
- [Pub] burnFrom #
+ Factor (Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable)
- [Pub] #
- [Pub] initialize #
- modifiers: initializer
VotingEscrow Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ ReentrancyGuard
- [Pub] #
- [Prv] _nonReentrantBefore #
- [Prv] _nonReentrantAfter #
+ [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] safePermit #
- [Prv] _callOptionalReturn #
+ [Int] IERC20Metadata (IERC20)
- [Ext] name
- [Ext] symbol
- [Ext] decimals
+ [Int] IVotingEscrow
- [Ext] createLock #
- [Ext] increaseAmount #
- [Ext] increaseUnlockTime #
- [Ext] withdraw #
- [Ext] delegate #
- [Ext] quitLock #
- [Ext] balanceOf
- [Ext] balanceOfAt
- [Ext] totalSupply
- [Ext] totalSupplyAt
- [Ext] forceUndelegate #
+ [Int] IBlocklist
- [Ext] isBlocked
+ [Int] IVotes
- [Ext] getVotes
- [Ext] getPastVotes
- [Ext] getPastTotalSupply
- [Ext] delegates
- [Ext] delegate #
- [Ext] delegateBySig #
+ VotingEscrow (IVotingEscrow, IVotes, ReentrancyGuard)
- [Pub] #
- [Ext] transferOwnership #
- modifiers: onlyOwner
- [Ext] updateBlocklist #
- modifiers: onlyOwner
- [Ext] updatePenaltyRecipient #
- modifiers: onlyOwner
- [Ext] unlock #
- modifiers: onlyOwner
- [Ext] forceUndelegate #
- [Ext] lockEnd
- [Ext] getLastUserPoint
- [Int] _checkpoint #
- [Ext] checkpoint #
- [Ext] createLock #
- modifiers: nonReentrant,checkBlocklist
- [Ext] increaseAmount #
- modifiers: nonReentrant,checkBlocklist
- [Ext] increaseUnlockTime #
- modifiers: nonReentrant,checkBlocklist
- [Ext] withdraw #
- modifiers: nonReentrant
- [Ext] delegate #
- modifiers: nonReentrant,checkBlocklist
- [Int] _undelegate #
- [Int] _delegate #
- [Ext] quitLock #
- modifiers: nonReentrant
- [Ext] getPenaltyRate
- [Int] _calculatePenaltyRate
- [Ext] collectPenalty #
- [Int] _copyLock
- [Int] _floorToWeek
- [Int] _findBlockEpoch
- [Int] _findUserBlockEpoch
- [Pub] balanceOf
- [Pub] balanceOfAt
- [Int] _supplyAt
- [Pub] totalSupply
- [Pub] totalSupplyAt
- [Ext] getVotes
- [Ext] getPastVotes
- [Ext] getPastTotalSupply
- [Ext] delegates
- [Ext] delegateBySig
Blocklist Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ [Int] IVotingEscrow
- [Ext] createLock #
- [Ext] increaseAmount #
- [Ext] increaseUnlockTime #
- [Ext] withdraw #
- [Ext] delegate #
- [Ext] quitLock #
- [Ext] balanceOf
- [Ext] balanceOfAt
- [Ext] totalSupply
- [Ext] totalSupplyAt
- [Ext] forceUndelegate #
+ Blocklist
- [Pub] #
- [Ext] blockContract #
- [Ext] isBlocked
BasketVault Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ [Int] IERC20Upgradeable
- [Ext] totalSupply
- [Ext] balanceOf
- [Ext] transfer #
- [Ext] allowance
- [Ext] approve #
- [Ext] transferFrom #
+ [Int] IERC20MetadataUpgradeable (IERC20Upgradeable)
- [Ext] name
- [Ext] symbol
- [Ext] decimals
+ [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
+ ERC20Upgradeable (Initializable, ContextUpgradeable, IERC20Upgradeable, IERC20MetadataUpgradeable)
- [Int] __ERC20_init #
- modifiers: onlyInitializing
- [Int] __ERC20_init_unchained #
- modifiers: onlyInitializing
- [Pub] name
- [Pub] symbol
- [Pub] decimals
- [Pub] totalSupply
- [Pub] balanceOf
- [Pub] transfer #
- [Pub] allowance
- [Pub] approve #
- [Pub] transferFrom #
- [Pub] increaseAllowance #
- [Pub] decreaseAllowance #
- [Int] _transfer #
- [Int] _mint #
- [Int] _burn #
- [Int] _approve #
- [Int] _spendAllowance #
- [Int] _beforeTokenTransfer #
- [Int] _afterTokenTransfer #
+ [Int] IERC4626Upgradeable (IERC20Upgradeable, IERC20MetadataUpgradeable)
- [Ext] asset
- [Ext] totalAssets
- [Ext] convertToShares
- [Ext] convertToAssets
- [Ext] maxDeposit
- [Ext] previewDeposit
- [Ext] deposit #
- [Ext] maxMint
- [Ext] previewMint
- [Ext] mint #
- [Ext] maxWithdraw
- [Ext] previewWithdraw
- [Ext] withdraw #
- [Ext] maxRedeem
- [Ext] previewRedeem
- [Ext] redeem #
+ ERC20BurnableUpgradeable (Initializable, ContextUpgradeable, ERC20Upgradeable)
- [Int] __ERC20Burnable_init #
- modifiers: onlyInitializing
- [Int] __ERC20Burnable_init_unchained #
- modifiers: onlyInitializing
- [Pub] burn #
- [Pub] burnFrom #
+ PausableUpgradeable (Initializable, ContextUpgradeable)
- [Int] __Pausable_init #
- modifiers: onlyInitializing
- [Int] __Pausable_init_unchained #
- modifiers: onlyInitializing
- [Pub] paused
- [Int] _requireNotPaused
- [Int] _requirePaused
- [Int] _pause #
- modifiers: whenNotPaused
- [Int] _unpause #
- modifiers: whenPaused
+ [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] StringsUpgradeable
- [Int] toString
- [Int] toHexString
- [Int] toHexString
- [Int] toHexString
+ [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 #
+ ReentrancyGuardUpgradeable (Initializable)
- [Int] __ReentrancyGuard_init #
- modifiers: onlyInitializing
- [Int] __ReentrancyGuard_init_unchained #
- modifiers: onlyInitializing
- [Prv] _nonReentrantBefore #
- [Prv] _nonReentrantAfter #
+ [Int] IERC20
- [Ext] totalSupply
- [Ext] balanceOf
- [Ext] transfer #
- [Ext] allowance
- [Ext] approve #
- [Ext] transferFrom #
+ [Int] IERC20Decimals
- [Ext] decimals
+ [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] safePermit #
- [Prv] _callOptionalReturn #
+ [Int] IChainlinkAggregatorV3
- [Ext] latestRoundData
- [Ext] decimals
+ [Int] IUniswapV3SwapCallback
- [Ext] uniswapV3SwapCallback #
+ [Int] ISwapRouter (IUniswapV3SwapCallback)
- [Ext] exactInputSingle ($)
- [Ext] exactInput ($)
- [Ext] exactOutputSingle ($)
- [Ext] exactOutput ($)
+ [Lib] TransferHelper
- [Int] safeTransferFrom #
- [Int] safeTransfer #
- [Int] safeApprove #
- [Int] safeTransferETH #
+ BasketVault (Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, IERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, AccessControlUpgradeable)
- [Pub] #
- [Pub] initialize #
- modifiers: initializer
- [Pub] pause #
- modifiers: onlyRole
- [Pub] unpause #
- modifiers: onlyRole
- [Int] _beforeTokenTransfer #
- modifiers: whenNotPaused
- [Ext] asset
- [Pub] totalAssets
- [Ext] convertToShares
- [Ext] deposit #
- modifiers: nonReentrant,whenNotPaused
- [Ext] mint #
- modifiers: nonReentrant,whenNotPaused
- [Int] _deposit #
- [Ext] maxDeposit
- [Pub] maxMint
- [Ext] previewDeposit
- [Ext] previewMint
- [Ext] withdraw #
- modifiers: nonReentrant,whenNotPaused
- [Ext] redeem #
- modifiers: nonReentrant,whenNotPaused
- [Int] _withdraw #
- [Pub] maxWithdraw
- [Pub] maxRedeem
- [Ext] previewWithdraw
- [Ext] previewRedeem
- [Ext] convertToAssets
- [Pub] _convertToShares
- [Pub] _convertToAssets
- [Int] _swap #
- [Pub] _getPrice
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.