Flex Protocol
Smart Contract Audit Report
Audit Summary
Flex Protocol is creating a new stablecoin, multiple staking contracts, and more.
For this audit, we reviewed Flex Protocol's project at commit cfbeaadb6c83bfaba429965c8f8d931e122e0511 on the team's GitHub repository.
Audit Findings
Please ensure trust in the team prior to investing as they have substantial control in the ecosystem.
Date: March 28th, 2022.
Updated: March 31st, 2022 to reflect changes from commit f713d6265eef57c67d42b47b63a414fc1c1d0893 to commit cfbeaadb6c83bfaba429965c8f8d931e122e0511.Finding #1 - Treasury - Medium
Description: If multiple periods have passed since allocateSeigniorage() is called, the epoch will only be incremented by one period when the checkEpoch modifier is executed.
Risk/Impact: Users will be able to call allocateSeignorage() multiple times in a row if it is not called for multiple periods.
Recommendation: The project team should properly update the next epoch time in the checkEpoch() modifier to prevent the potential for multiple calls of the allocateSeignorage() function to occur in a short period of time.
Update: The team has acknowledged and elected not to address this finding.Finding #2 - Epoch - Medium
Description: When updating the next epoch, the checkEpoch() modifier does not fully account for the point in time at which it was called.
Risk/Impact: This modifier is used in the OracleKlayswap contract's update() function to limit the time between updates; however, if update() is called right before the next epoch has been reached, it can be called again shortly after instead of requiring the length of a period to have passed.
Recommendation: The project team should restructure the checkEpoch() modifier logic to ensure that a full period has passed between function calls with the checkEpoch() modifier.
Update: The team has acknowledged and elected not to address this finding.Finding #3 - Epoch - Low
Description: The Operator can bypass the period requirements in the checkEpoch() modifier.
Risk/Impact: The Operator of the OracleKlayswap contract will be able to call update() mulitple times within a period, which can lead to price manipulation.
Recommendation: The project team should not allow the Operator to bypass the checkEpoch() restrictions.
Update: The team has acknowledged and elected not to address this finding.Finding #4 - Epoch - Low
Description: The "period" variable is missing a check in the constructor to ensure that it is within the intended window of 1 and 48 hours.
Risk/Impact: A short period length can lead to price manipulation as it is used in the OracleKlayswap's update() method.
Recommendation: The project team should add a require statement to the constructor to ensure that the passed period length is within the intended bounds.
Update: The team has acknowledged and elected not to address this finding.Finding #5 - ContractGuard - Low
Description: In the onlyOneBlock() modifier, the body of the function is executed before the user is marked to have called the function:Risk/Impact: At the time of writing this report, the project does not contain any functions that can be reentered if the intended tokens are used. However, if this modifier is used with any reentrant functions, they can be called multiple times within the same block despite the intended functionality. If the team uses an ERC-777 token or any token which allows users to execute a fallback function as the dollar or share token, functions transferring tokens to users will enable them to call functions multiple times in one block._; _status[block.number][tx.origin] = true; _status[block.number][msg.sender] = true;
Recommendation: The project team should update the code as shown below in order to prevent any potential reentrancy from occurring:Update: The team has acknowledged and elected not to address this finding._status[block.number][tx.origin] = true; _status[block.number][msg.sender] = true; _;
Finding #6 - Boardroom, Treasury, & Epoch - Informational (Resolved)
Description: The following functions are declared public, but are never called internally:Recommendation: These functions can be declared external for additional gas savings on each call.Boardroom.rewardPerShare(), Treasury.getDollarUpdatedPrice(), Epoch.getCurrentEpoch(), Epoch.getPeriod(), Epoch.getStartTime(), Epoch.getLastEpochTime().
Resolution: The team has declared the above functions external.
Contracts Overview
USDF Contract:
- As the contracts are implemented with SafeMath, they are safe from any possible overflows/underflows.
FlexToken Contract:
- This ERC-20 token is intended to be a stablecoin and to be used as the "dollar" token referenced in the contracts below.
- Upon deployment, 100,000 $USDF tokens are minted to the owner; they are also granted the "Operator" Role.
- The Operator can mint any amount of tokens to any address at any time.
- The Operator can also burn any user's tokens at any time.
- The owner can transfer the Operator Role to any address at any time; there can only be one address with the Operator Role at a time.
PreMint Contract:
- This ERC-20 token is intended to be used as the "share" token referenced in the contracts below.
- Upon deployment, 5,000 $FLEX tokens are minted to the owner; they are also granted the "Operator" Role.
- The Operator can call the distributeReward() function a single time to mint 2.4 million tokens to a "Farming Incentive Fund", 5,000 tokens to a "Premint" address, and 590,000 tokens to an "Ecosystem Fund".
- The Operator can burn any user's tokens at any time.
- The owner can transfer the Operator Role to any address at any time; there can only be one address with the Operator Role at a time.
EcosystemFund Contract:
- This contract allows users to deposit USDT in exchange for dollar tokens and rewards over time in the form of share tokens.
- Upon initialization, a start time is specified; an end time is then set to be 7 days after the start time.
- A treasury address, dollar address, USDT address, share address, and DAO fund address are also set.
- If the current time is after or equal to the start time and before the end time, users can deposit USDT to have 99% of the amount in dollar tokens minted to them.
- A user's tracked deposited amount will be incremented by the amount of dollar tokens that they are minted.
- The deposited USDT is sent to the DAO fund address.
- Users will earn a share of the accumulated rewards based on their share of deposited tokens, the amount of time passed, and reward rate.
- The pools total reward amount is set to 5,000 tokens (assuming the reward token has 18 decimals).
- As rewards are distributed over 7 days, the calculated reward rate is ~29.76 tokens per hour.
- Rewards will stop being generated once the end time has passed.
- The team should not use ERC-777 tokens as the dollar token.
- Users can only claim or mint once per block.
- This contract is intended to be granted ownership and the Operator role of the dollar token until the end time is reached.
- After the end time has been reached, anyone can call the transferDollarOwnershipToTreasury() function, which will transfer the Operator Role and ownership to the treasury address.
Boardroom Contract:
- This contract allows authorized "Partners" to earn portions of distributed share token rewards over time.
- Upon initialization, the share token address and start time are specified; an end time is then set to be 2 years after the start time.
- The Admin, which is set to the deployer of this contract, can add or remove an address from the list of Partners while the current timestamp is within the start and end times.
- When adding a Partner, the Admin will specify a point allocation to determine their share of total rewards.
- Partners can only claim rewards once per block.
- The project team should not use ERC-777 tokens or any token with a fallback function as the reward token, as it will allow users to claim multiple times within the same block.
- The pools total reward amount is set to 590,000 tokens (assuming the reward token has 18 decimals).
- As rewards are distributed over 2 years, the calculated reward rate is ~808.21 tokens per day.
DaoFund Contract:
- This contract allows users to stake "share" tokens in order to earn rewards in reward tokens.
- When a user stakes, their stake time is updated to the Treasury contract's current epoch.
- The Operator can deposit reward tokens to be distributed as rewards at any time, but cannot deposit more than once per block.
- In order to claim rewards, users must wait until the Treasury's epoch is greater than or equal to their stake time plus a specified reward period.
- In order to withdraw their staked tokens, users must wait until they are eligible to claim rewards and the Treasury's epoch is greater than or equal to their stake time plus a specified lockup period.
- An emergency withdraw function also exists, allowing users to forfeit their rewards to withdraw their tokens before the unlock times have passed.
- Users can only stake or withdraw once per block.
- The owner can update the lockup period or reward lockup period at any time.
- The Operator can deposit reward tokens to be distributed as rewards at any time.
- The Operator can withdraw any token from the contract at any time, excluding reward or share tokens.
- The owner can transfer the Operator Role to any address at any time; there can only be one address with the Operator Role at a time.
- The Operator Role is intended to be set to the Treasury contract.
- The project team should not use fee-on-transfer tokens as the share token or reward token unless the proper exemptions are made.
- The team should not use ERC-777 tokens as the share token or reward token, as it will allow users to stake and withdraw multiple times within the same block.
Treasury Contract:
- This contract is used by the Operator and Strategist Roles to interact with Klayswap Pools and Klay Exchanges to earn rewards.
- The Strategist or Operator can harvest rewards from a specified Klayswap Pool at any time in the form of $KSP.
- A platform fee is taken from the rewards and sent to a dev address.
- Another percentage of the $KSP is then swapped for dollar tokens along a specified path to be used for rewards and buybacks.
- The reward percentage is sent to the treasury address.
- The buyback percentage is swapped for share tokens and subsequently sent to the 0x..dead address.
- The Strategist or Operator can update the Treasury contract allowance, reward percentage, platform fee percentage, buyback percentage, maximum allowed amount of a specified token to swap at once, $KSP to dollar path, strategist address, and dev fund address at any time.
- The Operator can swap an allowed amount of the contract's USDT for dollar tokens at any time.
- The Strategist or Operator can transfer any amount of the contract's share tokens to the 0x..dead address at any time.
- The Strategist or Operator can swap up to the specified maximum allowed amount of any token or KLAY in the contract for another specified token at any time using Klayswap.
- The Strategist or Operator can add liquidity to any specified token pair or token/KLAY pair from this contract at any time. The resulting LP tokens are stored in this contract.
- The Strategist or Operator can also remove liquidity from a specified KlayExchange at any time. The resulting funds are stored in this contract.
- The Strategist or Operator can deposit or withdraw liquidity to/from a specified KlayswapSinglePool from/to this contract at any time.
- The owner can transfer the Operator Role to any address at any time; there can only be one address with the Operator Role at a time.
- The project team should exercise caution and limit the maximum number of dollar tokens that can be swapped at once to a small amount in order to reduce the risk of frontrunning.
TimeLock Contract:
- This contract is used to manage the price of the specified dollar token through minting and burning.
- Upon initialization, the dollar token address, share token address, and start time are set.
- The allocateSeignorage() function, used for the main functionality of the contract, can only be called after the start time has been reached. In addition, the function can only be called once per block.
- Any time this function is called, the price of the dollar token is checked using the OracleKlayswap contract.
- As the period can be set to any length upon initialization, the project team should ensure that the OracleKlayswap's period is set to at least 20 minutes to reduce the risk of inaccurate pricing.
- Once initialized, the Operator of the OracleKlayswap contract can update the period to any length between 1 and 48 hours.
- If the current price is above a specified price ceiling, dollar tokens are minted to the DaoFund contract.
- The amount of tokens minted is calculated by 30% of the difference between the price and 1, multiplied by the total supply.
- If the amount to be minted is above a certain threshold, the threshold amount will be minted instead.
- The threshold used is determined by the current total supply of dollar tokens.
- If the current price is below a specified price floor, USDT from the DaoFund contract is used to buyback dollar tokens.
- The amount bought back is determined by the a percentage of the difference between the 1 and the current price, multiplied by the total supply; this amount may be reduced if it exceeds the maximum buyback amount specified in the DaoFund contract.
- The resulting dollar tokens are then burned.
- At the end of the function, any dollar tokens held in this address are transferred to the BoardRoom contract to be distributed as rewards; this contract's epoch is then incremented.
- The Operator can set the BoardRoom contract's reward lockup and lockup periods through this contract at any time.
- The Operator can withdraw any token from this contract or the BoardRoom contract at any time, excluding reward or share tokens.
- The Operator can also manually burn a specified amount of dollar tokens from the DaoFund contract through this contract at any time.
- The Operator can transfer the Operator Role to any address at any time; there can only be one address with the Operator Role at a time.
- The Operator can update the Price Oracle address, DaoFund address, and Boardroom address at any time.
- The Operator can update the dollar token price floor and ceiling at any time.
- The Operator can update the maximum supply expansion percent at any time.
- The Operator can update the minting thresholds for specified total supply amounts at any time.
- The Operator can update the buyback percent at any time.
- This contract is used to execute arbitrary logic approved by an admin, after a specified delay period has elapsed.
- The admin, which is initially declared upon deployment, may submit transactions and a specified unlock time where they are subsequently added to a queue.
- Once the delay time has passed and the current time is still within the grace period of 2 weeks, the admin may execute the transaction.
- The admin may also elect to cancel a transaction at any time, making it unable to be executed.
- The admin address can be updated through a transaction from the queue.
- Once the pending admin has been set, they may claim the admin role at anytime.
- After deployment, the minimum and maximum delays allowed can only be updated through the use of a transaction from the queue.
- The delay can only be set to be between 1 and 30 days, inclusive.
- We advise users who have invested in the project to set up email alerts for the Timelock contract's activity to stay up to date on any proposed admin activity.
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 | The Oracle can be updated multiple times within one period length. | WARNING |
Outdated Compiler Version | N/A | PASS |
Race Conditions | N/A | PASS |
Reentrancy | N/A | PASS |
Signature Issues | N/A | PASS |
Unbounded Loops | N/A | PASS |
Unused Code | N/A | PASS |
Overall Contract Safety | WARNING |
Contract Source Summary and Visualizations
Name | Address/Source Code | Visualized |
USDF Contract | GitHub | |
FlexToken Contract | GitHub | |
PreMint Contract | GitHub | |
EcosystemFund Contract | GitHub | |
Boardroom Contract | GitHub | |
DaoFund Contract | GitHub | |
Treasury Contract | GitHub | |
Timelock Contract | GitHub |
About SourceHat
SourceHat (formerly Solidity Finance - founded in 2020) has quickly grown to have one of the most experienced and well-equipped smart contract auditing teams in the industry. Our team has conducted 1700+ 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.