Peapods Finance

Smart Contract Audit Report

Audit Summary

Peapods Finance is releasing a decentralized index fund product that allows users to have exposure to various underlying assets and yield potential earnings by providing LP to the product's indexes.

For this audit, we reviewed the contracts provided to us by the team.

Audit Findings

All findings have been resolved, though some centralized aspects are present.
Date: December 5th, 2023.
Updated: December 13, 2023, Peapods Finance experienced an exploit involving their WeightedIndex contract at 0xdbb20a979a92cccce15229e41c9b082d5b5d7e31. The attack utilized the flashloan functionality to drain the contract's liquidity. The team has implemented a locking mechanism on all functions to prevent this vulnerability and our team has verified these changes.
Updated: January 2nd, 2024 with updated fee structure.
Updated: January 5th, 2024 with updated WeightedIndex bond calculations and new ProtocolFeeRouter and ProtocolFees contracts.
Updated: January 11th, 2024 with resolutions to Findings and conversion to Solidity 0.8.19.

Finding #1 - WeightedIndex - High (Resolved)

Description: The bond() function relies on weights to determine the relative proportion of each asset that should be in the contract. Users then receive shares based on the amount of capital they contribute. Due to the fact a portion of the shares are taken as fees, the contract must compensate so that users who bond before shares are burned are not left with proportionally less shares than users who bond later. The team has implemented a ratio based implementation based on the proportional weight for each token relative to the proportional amount of tokens in the contract. While this will work for WeightedIndexes with only one underlying asset, the ratio result in an disproportionately small amount of tokens for WeightedIndexes with more than 1 asset.
Risk/Impact: Users who bond before any number of shares are minted will be left with proportionally less shares than users who bond after shares are burned. This will result in the user receiving less capital when withdrawing then they are actually due.
Recommendation: The team should calculate the shares based on the amount of tokens being deposited relative to the total amount of that specific token in the contract. This ratio can be used to determine what proportion of total capital they are depositing. Users are then due to be minted the same proportion of the total shares.
Resolution: The team has implemented the above solution.

Finding #2 - WeightedIndex - High (Resolved)

Description: The contract determines the amount of Index tokens to mint to users based on the contract's balance of the underlying asset being deposited. The contract will calculate the amount of Index tokens to mint using one method if there are not underlying assets in the contract (the first deposit) and another method if there are underlying assets. The second method relies on the Index token's totalSupply() when determining the amount of Index tokens to mint.
Risk/Impact: A malicious user can send the contract underlying assets directly and not through a deposit before the first bond() call. The calculation in the bond() function will always return 0 as a result of the following calculation:
uint256 _tokenCurSupply = IERC20(_token).balanceOf(address(this));
uint256 _tokenAmtSupplyRatioX96 = _tokenCurSupply == 0
							? FixedPoint96.Q96
							: (_amount * FixedPoint96.Q96) / _tokenCurSupply;
uint256 _tokensMinted = _tokenAmtSupplyRatioX96 == FixedPoint96.Q96
							? (_amount * FixedPoint96.Q96 * 10 ** decimals()) /
							  indexTokens[_tokenIdx].q1
							: (totalSupply() * _tokenAmtSupplyRatioX96) / FixedPoint96.Q96;
As the totalSupply() will be 0, the _tokensMinted will also be 0. All subsequent calls to bond() for the asset will mint 0 tokens as the totalSupply() remains 0. If all assets are sent to the contract users will never be able to successfully bond().
Recommendation: The team should determine which method to use when calculating the amount of tokens to mint based on the WeightedIndex totalSupply() rather than the contract's underlying asset supply:
uint256 _tokenAmtSupplyRatioX96 = totalSupply() == 0
							? FixedPoint96.Q96
							: (_amount * FixedPoint96.Q96) / _tokenCurSupply;
Resolution: The team has implemented the above solution.

Finding #3 - V3TwapUtilities - Low (Resolved)

Description: A reliable and manipulation resistant Oracle is required by several of the contracts throughout the protocol to provide accurate pricing for various assets. The team has chosen to implement a Time-Weighted Average Price (TWAP) Oracle based on UniswapV3 Liquidity Pools' Oracle functionality. The average price is determined over a 5 minute period.
Risk/Impact: The cost to manipulate a TWAP Oracle scales with the time period over which it is weighted. As a result a longer time period is more secure than a relatively short one. A 5 minute time period for an Oracle is on the shorter side and therefore there is more potential for manipulation.
Recommendation: The team should consider increasing this window to make potential manipulations less feasible.
Resolution: The team has increased the TWAP window to 10 minutes. This balances the cost to manipulate the Oracle with the speed in which the Oracle's reported price is updated to reflect the real time price.

Finding #4 - IndexUtils - Low (Resolved)

Description: The bondWeightedFromNative() function is used to swap ETH provided by a user into all of the underlying assets of a specified Index. The function uses a loop to give proper approval for the Index's assets. This loop runs from the 2nd asset in the array to the 2nd to last asset in the array.
Risk/Impact: The last asset in the array is never given proper approval for the subsequent call to the Index's bond() function. This will result in a denial of service when the Index attempts to transfer the last asset from the contract.
Recommendation: The team should change the loop to terminate at _assets.length rather than _assets.length-1.
Resolution: The team has implemented the above recommendation.

Finding #5 - IndexUtils - Low (Resolved)

Description: The bondWeightedFromNative() function is used to swap ETH provided by a user into all of the underlying assets of a specified Index. An "initial" asset is specified by the user as the place in the array as defined by the _assetIdx value. The initial asset is used to determine the required amount of other assets in the Index. The bondWeightedFromNative() function uses a loop to give proper approval for the Index's assets. This loop runs from the 2nd asset in the array to the 2nd to last asset in the array. This is under the assumption the 1st asset in the array will be given approval in the _bondToRecipient() function. However, the _bondToRecipient() function will give allowance to the asset at the _assetIdx place in the array which is not necessarily the 1st asset.
Risk/Impact: In any case where the _assetIdx is not the 1st item in the array, the 1st item will not get a proper allowance before the subsequent call to the Index's bond() function. This will result in a DOS when the Index attempts to transfer the 1st asset from the contract.
Recommendation: The team should change the loop to initialize start at the 1st item in the array rather than the 2nd. To prevent an unneeded allowance, the loop should also skip the asset at _assetIdx place in the array.
Resolution: The team has implemented the above recommendations.

Finding #6 - WeightedIndex & UnweightedIndex - Low (Resolved)

Description: The contracts inherit a rescueERC20() function that allows any user to withdraw errant tokens that are not a part of the Index's underlying assets. However, users are still able to transfer the actual WeightedIndex and UnweightedIndex tokens themselves.
Risk/Impact: Both contracts collect their own tokens as fees to be swapped for DAI as rewards. A malicious user could withdraw the collected fees from the contract preventing the swap for rewards and requiring the owner to transfer the tokens back to the contract.
Recommendation: The team should prevent users from calling the rescueERC20() function on address(this). Alternatively, the function can be access restricted to only trusted addresses.
Resolution: The team has implemented the above recommendation. They have chosen this solution rather than implementing an Ownable solution as part of the project team's desire to implement minimal centralization.

Finding #7 - WeightedIndex - Low (Resolved)

Description: The contract utilizes two methods for calculating the amount of WeightedIndex tokens a user should be minted when bonding. On the first bond, users are minted Index tokens based on the amount of asset tokens deposited and the Q1 value of the asset. For each subsequent deposit users are minted Index tokens based on the amount of asset tokens they are depositing relative to the contract's asset token balance and the total supply of Index tokens. As Index tokens collected from fees are burned, this ratio will yield less Index tokens minted to the user than the initial ratio. Due to the structuring of logic users can deposit an amount of asset tokens equal to the contract's asset token balance and receive Index tokens at the initial ratio. This will result in them receiving a larger amount of shares than they are due.
uint256 _tokenAmtSupplyRatioX96 = _tokenCurSupply == 0
						? FixedPoint96.Q96
						: (_amount * FixedPoint96.Q96) / _tokenCurSupply;
uint256 _tokensMinted = _tokenAmtSupplyRatioX96 == FixedPoint96.Q96
						? (_amount * FixedPoint96.Q96 * 10 ** decimals()) /
						  indexTokens[_tokenIdx].q1
						: (totalSupply() * _tokenAmtSupplyRatioX96) / FixedPoint96.Q96;
Risk/Impact: Users can exploit this to receive a larger proportion of shares when bonding. When debonding, the amount of asset tokens sent to the user is determined by the amount of Index tokens being debonded relative to the total supply of Index tokens. By debonding a disproportionate amount of Index tokens they will also receive a disproportionate amount of the underlying assets. This results in users who deposited prior to the exploiting user receiving less assets when debonding.
 uint256 _percAfterFeeX96 = (_amountAfterFee * FixedPoint96.Q96) / totalSupply();
					....
uint256 _debondAmount = (_tokenSupply * _percAfterFeeX96) /
					FixedPoint96.Q96;
Recommendation: The team should rely exclusively on the _tokenCurSupply to determine which method is used when calculating the amount of Index tokens to mint such as below:
uint256 _tokenMinted;
if (_tokenCurSupply == 0) {
	_tokensMinted = (_amount * FixedPoint96.Q96 * 10 ** decimals()) /
		indexTokens[_tokenIdx].q1
} else {
	_tokensMinted = (totalSupply() * ((_amount * FixedPoint96.Q96) / _tokenCurSupply)) / FixedPoint96.Q96;
}
...
for (uint256 _i; _i < indexTokens.length; _i++) {
	uint256 _transferAmt = _tokenCurSupply == 0
	? getInitialAmount(_token, _amount, indexTokens[_i].token)
	: (IERC20(indexTokens[_i].token).balanceOf(address(this)) *
		_tokenAmtSupplyRatioX96) / FixedPoint96.Q96;
		...
}
					
Resolution: The team has implemented the above recommendation and now relies on the Index token's total supply to determine the method used to calculate the amount of Index tokens minted to the user.

Finding #8 - UnweightedIndex & WeightedIndex - Informational (Resolved)

Description: The contracts contain IndexAssetInfo structs that have unused values.
Recommendation: The team should consider removing the unused values to save on contract size and operating costs.
Resolution: The team has implemented the above recommendation.

Contracts Overview

  • As the contracts are implemented with Solidity v0.8.19, they are safe from any possible overflows/underflows.
PEAS Contract:
  • This contract defines the Peapods Finance token.
  • The owner is minted 10 million [10,000,000] $PEAS tokens upon deployment.
  • No mint functions are accessible beyond deployment.
  • Any user can burn their own tokens to reduce the total supply.
  • There are no fees associated with transferring tokens.
  • No ownership-restricted functions are present.
TokenRewards Contract:
  • This contract is used to distribute rewards tokens to users.
  • Users earn rewards based on the amount of shares they have relative to the total shares in the contract.
  • The TrackingToken address may add and remove shares from users at any time.
  • Any rewards tokens in the contract will be processed when a user's shares are updated.
  • Users will be distributed rewards when their amount of shares is changed.
  • Users may also manually claim rewards for any address at any time.
  • Any user may deposit rewards tokens to be distributed as rewards.
  • The contracts rewards token balance will be burned or transferred to the dead address if there are currently no shares in the contract.
  • If there are shares in the contract a "yield burn fee" will be taken and burned or sent to the dead address if the fee has been set in the YieldFees contract.
  • The remaining tokens will be distributed as rewards to all users with shares.
  • Any user may deposit a specified token set upon deployment as rewards. A "yield admin fee" will be taken and sent to the V3TwapUtilities owner's address if the fee has been set in the YieldFees contract
  • The remaining tokens will be swapped for rewards tokens and later distributed as rewards the next time rewards are processed.
StakingPoolToken Contract:
  • This contract defines the StakingPool token. The contract is associated with a staking token.
  • A TokenRewards contract is created upon deployment and associated with this token.
  • The owner may choose to restrict addresses that may stake. If restricted only the "restricted" address may stake tokens.
  • If no restricted address has been set, any address may stake any amount of staking tokens at any time.
  • The staking tokens will be stored in this contract and the user will be minted the same amount of StakingPool tokens.
  • Any address may unstake their staking tokens at any time.
  • The StakingPool tokens will be burned and the same amount of staking tokens will be transferred to the user.
  • When tokens are minted, burned, or transferred users' shares will be set accordingly in the TokenRewards contract.
  • The restricted address may update its own address at any time.
  • The restricted address remove its address at any time.
UnweightedIndex Contract:
  • This contract defines the UnweightedIndex token. An UnweightedIndex is associated with a "paired token" specified upon deployment.
  • UnweightedIndex tokens represent a selection of underlying assets where all assets are given equal allocation.
  • Each asset is associated with a UniswapV3 liquidity pool and a "base" price.
  • The base price is set at the time the contract is deployed through a UniswapV3 time weighted average price.
  • Users may "bond" at any time by supplying an approved underlying asset.
  • Users will be minted tokens according to the value of the supplied asset and the current "index".
  • The index is determined by the ratio of the current price of all assets relative to their base price.
  • A "bond fee" is taken from the minted tokens. No bond fee is taken if the user is the first to bond tokens.
  • The partner address may bond for free once during the first 7 days after deployment.
  • The contract will then "rebalance".
  • Users may manually trigger a rebalance at any time.
  • Rebalancing will swap assets based on their current index.
  • Assets that have decreased in their index value relative to the actual value of tokens in the contract will be swapped for assets that have increased in index value relative to the actual value of tokens in the contract if the change is more than the "rebalance min swap" value.
  • Users may "debond" their tokens at any time.
  • Users tokens are redeemed for the current "index price".
  • Users specify the percentage of each underlying asset they are redeeming their tokens for.
  • A "debond fee" is taken from tokens being debonded. No debond fee is taken if the user is debonding more than 98% of the current UnweightedIndex token supply.
  • The contract will rebalance after tokens are debonded.
  • Users may burn their UnweightedIndex tokens at any time.
  • Once the contract has reached the "minimum" balance, the accumulated tokens will be swapped for the paired token and deposited as rewards in the TokenRewards contract. This swap is potentially open to frontrunning.
  • If the "burn fee" has been set, a percentage of the tokens collected as fees will be burned.
  • If the "partner fee" has been set, a percentage of the tokens collected as feed will be transferred partner address.
  • Users may take a flashloan of any amount of a single asset in the contract by paying a "flash fee" in the paired token.
  • Users may use this contract to add and remove liquidity from the UnweightedIndex liquidity pool. Using this method will prevent the user from paying fees.
  • The owner may set the rebalance slippage and rebalance minimum swap amount to any values at any time.
  • Any address may withdraw any non-asset tokens and non-UnweightedIndex tokens from the contract to the owner at any time.
  • Any address may withdraw any ETH from the contract to the owner at any time.
  • The Partner address may update its own address at any time.
  • The Partner address may decrease the partner fees at any time.
  • The Rewards address may process tokens collected as fees at any time.
WeightedIndex Contract:
  • This contract defines the WeightedIndex token. A WeightedIndex is associated with a "paired token" specified upon deployment.
  • WeightedIndex tokens represent a selection of underlying assets where the allocation of assets remains at a specified "weight" relative to the total weight across all assets.
  • Each asset is associated with a weight.
  • Users may "bond" at any time by specifying an approved underlying asset.
  • Users must supply the corresponding amount of all other underlying assets based on the weight of each asset relative to the weight and amount provided of the specified asset.
  • A "bond fee" is taken from the minted tokens. No bond fee is taken if the user is the first to bond tokens.
  • The partner address may bond for free once during the first 7 days after deployment.
  • Users may "debond" their tokens at any time to receive the proportional amount of underlying assets.
  • A "debond fee" is taken from tokens being debonded. No debond fee is taken if the user is debonding more than 98% of the current WeightedIndex token supply.
  • Users may burn their WeightedIndex tokens at any time.
  • Once the contract has reached the "minimum" balance, the accumulated tokens will be swapped for the paired token and deposited as rewards in the TokenRewards contract. This swap is potentially open to frontrunning.
  • If the "burn fee" has been set, a percentage of the tokens collected as fees will be burned.
  • If the "partner fee" has been set, a percentage of the tokens collected as feed will be transferred partner address.
  • Users may take a flashloan of any amount of a single asset in the contract by paying a "flash fee" in the paired token.
  • Users may use this contract to add and remove liquidity from the UnweightedIndex liquidity pool. Using this method will prevent the user from paying fees.
  • Any address may withdraw any non-asset tokens and non-WeightedIndex tokens from the contract to the owner at any time.
  • Any address may withdraw any ETH from the contract to the owner at any time.
  • The Partner address may update its own address at any time.
  • The Partner address may decrease the partner fees at any time.
IndexUtils Contract:
  • This contract is used to perform auxillary functions on UnweightedIndex or WeightedIndex Index Fund contracts.
  • Users may use this contract to perform a bond on a specified Index Fund.
  • If the contract is a WeightedIndex Index Fund, the contract will calculate the required assets to perform a bond based on the specified amount of the supplied asset.
  • The contract will then perform the bond for the user and transfer them the received WeightedIndex tokens.
  • If the contract is an UnweightedIndex Index Fund, the contract will perform the bond for the user and transfer them the received WeightedIndex tokens.
  • Users may use this contract to perform a bond with ETH.
  • The supplied ETH will be deposited for WETH. If the user has elected to stake as well, only half of the supplied ETH will be used to bond.
  • If the specified asset is WETH, the received WETH will be used to bond on behalf of the user.
  • If the specified asset is not WETH, the received WETH will be swapped for the specified asset and subsequently used to bond on behalf of the user.
  • If the user elected to stake as well, the remaining ETH supplied by the user will be swapped for the Index Fund's paired token.
  • The received tokens will be paired with the Index Fund tokens and used to add liquidity to the token-Index Fund liquidity pool.
  • The LP tokens will then be staked in the Index Funds corresponding Staking Pool.
  • Users may use this contract to add LP and stake in a single transaction.
  • The supplied Index Fund tokens and Index Fund's paired tokens will be used to add liquidity to the token-Index Fund liquidity pool.
  • The received LP tokens will then be staked in the Index Funds corresponding Staking Pool.
  • Users may use this contract to unstake and remove liquidity for LP tokens for a specified Index Fund in one transaction.
  • The contract will first unstake the specified amount of LP tokens from the Index Fund's Staking Pool.
  • The LP tokens will be used to remove liquidity from the token-Index Fund liquidity pool.
  • The user will be transferred all received tokens and Index Fund tokens.
V3TwapUtilities Contract:
  • This contract is used to determine the current price of an asset.
  • A UniswapV3 Pool is used to determine the asset's price.
  • The price is calculated using a 10 minute time-weighted average price.
V3Locker Contract:
  • This contract is used to collect tokens from the UniswapV3 Position Manager contract after a specified amount of time.
  • Any user may collect fees accumulated by a position. The fees will be sent to the contract owner.
  • The owner may transfer the contract's LP tokens after the locked time has passed.
  • The owner may increase the locked time by any amount at any time.
ProtocolFeeRouter Contract:
  • This contract is used to maintain the protocol fee so that other contracts may fetch the current fee.
  • The owner may set the protocol fee to any value at any time.
ProtocolFees Contract:
  • This contract is used to maintain the yield admin fee and the yield burn fee so that other contracts may fetch the current fees.
  • The owner may set the yield admin fee up to 20% at any time.
  • The owner may set the yield burn fee up to 20% at any time.

Audit Results

Vulnerability Category Notes Result
Arbitrary Jump/Storage Write N/A PASS
Centralization of Control
  • The UnweightedIndex owner may set the rebalance slippage and rebalance minimum swap values.
  • The V3Locker owner may increase the locked time by any amount.
  • The ProtocolFeeRouter fees are uncapped and may be set up to 100%.
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

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.