Rena Finance V2
Smart Contract Audit Report
Audit Summary
Rena Finance is building a new single sided liquidity platform and staking platform where users can deposit tokens and earn rewards.
For this audit, we reviewed the project team's contracts at commit 86e4d8103fc65153969ef47a8aa34428dac5469a on the team's private GitHub repository.
Audit Findings
An Informational finding was identified and the team may want to review it.
Date: September 18th, 2023.
Updated: September 21st, 2023 to reflect updates made to the RenaV2Pair contract that resolves Finding #1.
Updated: December 20th, 2023 with additional Virtual Reserves Analysis and changes from commit 510ffde14ea35ed077e2b16d54e6cee3839e6240 to commit 86e4d8103fc65153969ef47a8aa34428dac5469a.Finding #1 - RenaV2Pair - High (Resolved)
Description: Any user can transfer an amount of token B to the contract and subsequently call the swap() function and specify any arbitraryamount0Out
that does not exceed the_reserve0
value. This arbitrary value will be allocated to the recipient in the rBond contract.
Risk/Impact: Any user can drain the RenaV2Pair contract of its token A balance at any time.function swap(uint amount0Out, address to) external nonReentrant { require(amount0Out > 0, 'RenaV2: INVALID_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1) = getReserves(); require(amount0Out < _reserve0, 'RenaV2: INSUFFICIENT_LIQUIDITY'); require(to != token0 && to != token1, 'RenaV2: INVALID_TO'); // optimistically lock tokens in a bond IERC20(token0).safeApprove(address(bond), amount0Out); bond.createBond(amount0Out, to); uint amount1In = IERC20(token1).balanceOf(address(this)); require(amount1In > 0, 'RenaV2: INSUFFICIENT_INPUT_AMOUNT'); uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = uint(_reserve1) + amount1In;
Recommendation: The team should modify the swap() function to ensure that it can only be called from the RenaV2SwapHelper contract preventing arbitrary values from being passed in for theamount0Out
parameter.
Resolution: The team has implemented an adjusted balance calculation and a subsequent K value check to ensure liquidity invariants are maintained.
Finding #2 - RenaFinanceV2 - 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.
Contracts Overview
LPMigration Contract:rBond Contract:
- This contract can be used by stakers to migrate their staked LP tokens from the V1 staking contract to the V2 staking contract.
- The deployer will specify the V2 Staking contract address upon deployment.
- Any user that has staked tokens in the V1 Staking contract can initiate a migration by specifying a minimum Rena amount to receive after swapping, a lock duration, and the transaction's deadline time.
- The full number of staked tokens are withdrawn on behalf of the caller in the V1 Staking contract and transferred to the contract.
- Liquidity is removed from the Pair contract for the fully withdrawn LP token amount and the LP tokens are burned.
- The received ETH is swapped for Rena tokens. The contract's full Rena balance must exceed the minimum value specified by the caller after the swap occurs.
- The contract's full Rena token balance is staked in the V2 Staking contract on behalf of the caller.
rDistributor Contract:
- A new instance of this contract is created when a new Pair is generated in the RenaV2Factory contract.
- The deployer will specify the bond token address used in the contract upon deployment.
- The contract owner can create a bond by specifying a number of bond tokens to lock and the owner that will be assigned to the bond.
- A new lock is created on behalf of the specified bond owner and is assigned a unique lock ID.
- The specified number of tokens are transferred from the caller to the contract. The caller must grant the contract a sufficient allowance in order for the transaction to successfully occur.
- The bond token address should not be set to a fee-on-transfer. If a fee-on-transfer token is used, the contract must be exempt from the token's fee mechanism.
- A bond owner can unlock a specific bond using its ID once the predetermined lock duration, established at the time of the bond's creation, has elapsed.
- The full number of tokens are transferred from the contract to the caller.
- The owner can set the bond duration to any value at any time.
RenaV2Factory Contract:
- A new instance of this contract is created when a new Pair is generated in the RenaV2Factory contract.
- The RenaV2Pair address, Balancer address, and Discounter address are set upon deployment.
- Any Discounter address can set the version of the target Pair used in the contract, choosing between V2 or V3 at any time.
- Any user with the Balancer role is authorized to initiate a rebalance transaction to alter the reserves in the RenaV2Pair contract based on the existing discount rate.
- The targeted rToken and token reserves are determined by referencing the present reserves in the RenaV2Pair and the corresponding target pair.
- If the targeted rToken reserve is lower than the existing reserve, the excess number of rTokens are decreased to meet the targeted amount.
- The current token reserves are assessed to ensure they match the set discount rate. If that is not the case, the necessary number of tokens are transferred to the RenaV2Pair to maintain the targeted balance.
- The sync() function is subsequently triggered in the RenaV2pair to update its reserves, ensuring they are correctly represented after the balances are adjusted.
- Any Discounter address can set the Discount rate to any value up to 100% at any time.
RenaV2Pair Contract:
- Any user can use this contract to create a RenaV2Pair contract by specifying two underlying token assets.
- A new rDistributor contract and rBond contract are created when a new Pair is generated.
- The owner can assign a custom fee percentage to any address up to 8% at any time.
- The owner can set the contract's default fee to any value up to 8% at any time.
- The owner can withdraw any tokens from the contract to any address at any time.
RenaV2PoolManager Contract:
- Any user can initiate a swap by specifying an amount of token A and a recipient address.
- A new bond is automatically created in the rBond contract on behalf of the recipient address for the specified token amount, temporarily locking these tokens.
- A protocol fee is charged and is transferred to the RenaV2Factory contract. The contract's remaining token B balance is transferred to the Destination address set by the owner.
- The owner can use the mint() function to add liquidity to the RenaV2Pair pool.
- The caller will specify the calculated amount of token B to add to the pool.
- The first time liquidity is added, a minimum liquidity barrier is set to prevent minuscule liquidity additions in the future.
- The destination address is minted LP tokens in return for the contribution, representing their share of the liquidity pool.
- Any user can initiate a burn to remove liquidity from the pool and retrieve the underlying asset.
- The caller will specify a recipient address to receive the tokens withdrawn from the liquidity pool.
- The function calculates the amount of each token to be returned based on the user’s LP token balance and the current pool reserves.
- Liquidity tokens are burned in the process, reducing the total supply and removing the user’s share from the liquidity pool.
- Any user can initiate a skim transaction which transfers the difference of the contract's token A balance and token A reserve value to the caller.
- Any user can initiate a sync transaction which sets the the contract's token A reserve value to the current token A balance of the contract.
- The owner can set the bond duration in the rBond contract to any value at any time.
- The owner can update the contract's Destination address at any time.
RenaV2SwapHelper Contract:
- Any user can use this contract to add liquidity to a specified pair of underlying tokens at any time.
- A Pair is automatically created for the two specified tokens if one does not already exist.
- Upon adding liquidity, the user specifies the desired minimum amount of each token in the pair to add to the liquidity pool, and a calculated amount of token A is transferred from the caller to the Pair address. The user is minted RenaV2Pair LP tokens representing their share of ownership of the liquidity pool.
- Upon removing liquidity, the user specifies the desired minimum amount of each token asset to receive from the liquidity pool; the user is transferred an amount of token A and their LP tokens are burned in the process.
rRewarderV2 Contract:
- Any user can use this contract to swap one token asset or ETH for any other supported token asset.
- The user will specify a minimum number of tokens to receive, a threshold that must be met after the swap is executed.
- Users have the option to exchange their tokens for a fixed number of another token; they'll specify the exact number of tokens to receive and the maximum amount of their current tokens they are willing to exchange.
rBalancer Contract:
- Any address granted the Rewarder role may initiate a reward distribution after the contract's reward frequency time has passed since the last distribution was executed. Any address granted the Rebalancer role can initiate a reward distribution at any time.
- If the contract has an accumulated balance of token B, half of the balance is exchanged for token A. The contract's full token A amount and the calculated amount of token B are then transferred to the Uniswap Pair, and LP tokens are minted to the contract.
- The number of generated LP tokens must exceed the minimum liquidity value specified by the caller.
- The newly minted LP tokens are split between the Treasury address and the Staking address based on the respective share percentage set by the team.
- A "forward" transaction is executed in the Staking contract distributing the reward tokens to the Distribute contract.
- Any address granted the Config Role can set the reward cooldown time to any value at any time.
- Any address granted the Config Role can set the Treasury share percentage to any value up to 100% at any time.
- Any address granted the Config Role can set the Treasury address to any address at any time.
rStaking Contract
- Any address granted the Balancer role can initiate a rebalance by specifying a list of distributor addresses, rewarder addresses, and corresponding minimum expected liquidity values for each rewarder.
- The contract's frequency time must have passed since the last rebalance in order for the transaction to successfully occur.
- The Reward functionality is triggered in each specified Rewarder address applying the corresponding minimum liquidity value to the transaction.
- The balance functionality is subsequently triggered in each specified Distributor address.
- Any address granted Frequency role can set the frequency time to any value at any time.
- The deployer will set the contract's staking token and reward token upon deployment and a new Distribute contract is created.
- Any user can initiate a deposit by specifying a number of tokens to stake, the address that will be set as the owner of the stake, and a lock duration of up to 1 year.
- The specified number of tokens are transferred from the caller to the contract. The caller must grant the contract a sufficient allowance in order for the deposit to successfully occur.
- A boost amount is calculated based on the number of tokens being staked and the lock duration. Its value is added to the stake amount and stored as "shares".
- The deposit is finalized in the Distribute contract for the transaction's calculated number of shares.
- A stake owner can specify a number of their staked tokens to unstake from the contract for a specified stake ID after the expire time associated with the ID has been reached.
- Any pending rewards are transferred from the Distribute contract to the caller.
- Any user can initiate a withdrawal which will unstake and immediately restake a specified number of tokens and transfer any pending rewards to the caller in the process.
- Any user can specify a number of reward tokens to distribute as rewards in the Distribute contract. The caller must grant the contract a sufficient allowance in order for the transaction to successfully occur.
- Any reward tokens erroneously sent to this contract can be distributed as rewards in the Distribute contract by any user at any time.
- The team must ensure that the reward token is not set to the 0x00 address representing rewards in ETH as the platform does not support the ability to distribute ETH rewards.
- The team must ensure that the reward token and staking token are not set to fee-on-transfer tokens. If a fee-on-transfer token is used, the contract must be exempt from the token's fee mechanism.
Virtual Reserves Analysis
In the course of this audit additional analysis was performed on the method the team uses to approximate the current ratio of reserves in a UniswapV3 liquidity pool. The following proof is used to demonstrate the validity of the calculation used in this platform. First, the \(X_{reserves}\) will be calculated.
The liquidity \(L\) in the platform is defined by the Uniswap team as \[\sqrt{(X_{reserves}*Y_{reserves})}\] The team fetches the \(sqrtPriceX96\) from the Pool which is defined by the Uniswap team as \[\sqrt{price}*2^{96}\] The team then calculates the amount of \(tokenX\) as \[\frac{L*2^{96}}{sqrtPrice96}\] Substituting in the above definitions we are left with \[\frac{\sqrt{(X_{reserves}*Y_{reserves})}*2^{96}}{\sqrt{price}*2^{96}}\] We can cancel the two factors of \( 2^{96}\) from the numerator and denominator. We are then left with \[\frac{\sqrt{(X_{reserves}*Y_{reserves})}}{\sqrt{price}}\] The Uniswap team defines \(price\) as the ratio of \[\frac{token_{Y}}{token_{X}}\] Substituting this in for the price we are left with \[\frac{\sqrt{(X_{reserves}*Y_{reserves})}}{\sqrt{(\frac{Y}{X})}}\] This can then be simplified to leave us with just the \(X_{reserves}\).
A similar proof can be employed to calculate the \(Y_{reserves}\). All of the above definitions will be utilized in the same manner. The team calculates the \(Y_{reserves}\) as \[L*sqrtPrice96>>96\] The bit shifting operation (\(>> 96)\) is equivalent to dividing by \(2^{96}\). We can rewrite the above equation to \[\frac{L*sqrtPrice}{2^{96}}\] Substituting in the previous definitions leaves \[\frac{\sqrt{(X_{reserves}*Y_{reserves})}*\sqrt{price}*2^{96}}{2^{96}}\] As before the two factors of \( 2^{96}\) can be cancelled. And substituting in for price again results in \[\sqrt{(X_{reserves}*Y_{reserves})}*\sqrt{(\frac{Y}{X})}\] This can be simplified to leave only \(Y_{reserves}\).
Audit Results
Vulnerability Category | Notes | Result |
---|---|---|
Arbitrary Jump/Storage Write | N/A | PASS |
Centralization of Control | The owner can set the bond duration to any value at any time in the rBond contract. | PASS |
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 |
Contract Source Summary and Visualizations
Name |
Address/Source Code |
Visualized |
LPMigration |
||
rBalancer |
||
rDistributor |
||
RenaV2Factory, RenaV2Pair, rBond |
||
RenaV2PoolManager |
||
RenaV2SwapHelper |
||
rRewarderV2 |
||
rStaking, Distribute |
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.