Foil engaged Guardian to review the security of its virtual gas marketplace protocol. From the 26th of August to the 9th of September, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- August 26 to September 9, 2024
- Language
- Solidity
- Chains
- Ethereum
- Sector
- Derivatives
- 9 Critical
- 4 High
- 14 Medium
- 27 Low
- 0 Informational
Scope
Overview
Foil engaged Guardian to review the security of its virtual gas marketplace protocol. From the 26th of August to the 9th of September, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 13 High/Critical issues were uncovered and promptly remediated by the Foil team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the functionality described for the Foil protocol.
Security Recommendation Given the number of High and Critical issues detected, Guardian supports a secondary security review of the protocol at a finalized frozen commit.
Findings 54
-
C-01 Critical increaseLiquidityPosition Uses The Wrong Id Logical Error Resolved
Description
The
increaseLiquidityPositionfunction calls theNonfungiblePositionManagerwith the Foil position ID instead of the Uniswap position ID. As there are liquidity and trade positions in Foil the IDs can differ.Recommendation
Call Uniswap with the correct position ID.
Resolution
Foil Team: The issue was resolved in PR#47.
-
C-02 Critical Liquidity Is Stuck After Epoch Is Settled Logical Error Resolved
Description
The only way to decrease liquidity of a position is through the
decreaseLiquidityPositionfunction. This has a checkepoch.validateEpochNotSettledwhich will revert if the epoch has settled.The
settlePositionfunction is supposed to allow you to exit liquidity positions through a branch which calls_settleLiquidityPosition().However, this calls the
collectfunction in theNonFungiblePositionManagerwhich will collect the tokens owed from fees and previous liquidity burns, but does not burn/decrease the liquidity still in the pool.The user may still get their collateral back, but does not get the value of their liquidity if it exceeds the loan value.
It should be expected that the liquidity is worth than the loan value, since the LP positions are overcollateralized. When settling a liquidity position, consider first burning all the liquidity and the calling collect.
Recommendation
When settling a liquidity position, consider first burning all the liquidity and then calling collect.
Resolution
Foil Team: The issue was resolved in PR#47.
-
C-03 Critical borrowedVGas Is Set To 0 Before Subtraction Logical Error Resolved
Description
In
closePositionPosition, thevGasAmountshould be set tocollectedAmount0 -position.borrowVGas. However,position.borrowedVGasis set to 0 before the subtraction.This means that the user's
vGasamount is overestimated and allows them to drain the protocol.Recommendation
Reset
position.borrowedVGasafter rather than before the subtraction.Resolution
Foil Team: The issue was resolved in PR#47.
-
C-04 Critical Uniswap Pool Creation DoS DoS Resolved
Description
Proof of concept: PoC
Uniswap pool creation is done with three variables (g
asTokenaddress,ethTokenaddress, andfeeRate). These variables are all predictable even before creation of specified tokens.Hence it is possible to frontrun pool creations happening in
Epoch.sol/createValid(), which will lead to revert in epoch creation.Recommendation
Use
CREATE2to deploy the virtual tokens with a configurable salt so that pool creation cannot be permanently DoS, additionally be sure to use a private rpc to avoid being frontran to DoS individual epoch creations.Resolution
Foil Team: The issue was resolved in PR#85.
-
C-05 Critical Settled Trading Positions Can Be Closed Logical Error Resolved
Description
Proof of concept: PoC
The
modifyTraderPositionfunction does not check if the given position is already settled. This enables an attack vector to steal funds by closing an already settled position.As the collateral & the owned tokens of the position are not set to 0 during the settlement process.
Recommendation
Always use the
validateNotSettledvalidation in themodifyTraderPositionfunction as no trader activity should occur after an epoch end besides settlement.If a trader wishes to close their position, they should use the
EpochSettlementModuleto do so.Thus the
epoch.settledcase handling can be removed from theswapTokensExactOutandswapTokensExactInswap functions as they will no longer be callable after the epoch has been settled.Resolution
Foil Team: The issue was resolved in PR#87.
-
C-06 Critical Settlement Can Be Impossible Due To Underflow Underflow Resolved
Description
This Equation in the
settlefunction ofPosition.sol:self.depositedCollateralAmount += self.vEthAmount - self.borrowedVEth;is slightly different from:self.depositedCollateralAmount = self.depositedCollateralAmount + self.vEthAmount -self.borrowedVEth;Because in the first equation, if
self.borrowedEth < self.vEthAmount, then the equation will underflow, even though this position is fully collateralized.This underflow makes certain positions impossible to settle.
Recommendation
Replace
self.depositedCollateralAmount += self.vEthAmount - self.borrowedVEth;Withself.depositedCollateralAmount = self.depositedCollateralAmount + self.vEthAmount -self.borrowedVEth;Resolution
Foil Team: The issue was resolved in PR#47.
-
C-07 Critical Undercollateralized Positions Can Be Created Logical Error Resolved
Description
In this line, if
loanAmount0 > maxAmount0, then the excessloanAmount0is not considered in the collateralization check:uint256 availableAmount0 = maxAmount0 > loanAmount0 ? maxAmount0 - loanAmount0 : 0;
The
loanAmount0entered intocollateralRequirementAtMinTickisloanAmount0 - tokensOwed0.Here is a sequence which leads to a state where
loanAmount0 > maxAmount0: 1. Create a liquidity position when the current tick is belowtickLower, so the loaned amount is 100%token02. Swap so the position is liquidity entirelytoken13. Remove most of the liquidity viadecreaseLiquidityPosition.Since the LP is now 100%
token1, all the claimed tokens fromdecreaseLiquidityPositionwould be token1 (except for a tiny amount of LP fees).DecreaseLiquidityPositionreduced themaxAmount0, whileloanAmount0is still theamount0required to create the initial position, somaxAmount0 >loanAmount0.Now it is true that decreasing a liquidity position should make the collateral requirements lower, but this is already accounted for in
tokensOwed0 tokensOwed1being deducted from loan amounts. 1. Create a position that requires loaningtoken02. Swap so the position is entirelytoken13.decreaseLiquidityPosition.All the claimed tokens from
decreaseLiquidityPositionwould be token1 (except for a tiny amount of LP fees).Recommendation
Consider converting
loanAmount0to the correspondingETHvalue and incorporating it into the collateral calculation rather than subtracting it frommaxAmount0.Resolution
Foil Team: The issue was resolved in PR#91.
-
C-08 Critical vETH Profit In Long Pos Deleted On Close Logical Error Resolved
Description
When a trader owns a long position that made a good amount of profit and the trader decreases the position so that the full loan is repaid the system will:
- calculate the excess
vETHafter fully repaying theborrowedVEthamount - set the
borrowedVEthto 0 - save the excess
vETHasvEthAmountin the position by callingupdateBalance
This is unusual as normally a long position has a
vGasAmountamount > 0 and aborrowedVEthamount > 0, but thevEthAmountis usually 0.This state is problematic when closing the long position as the flow of closing the position looks like the following:
- Swap the positions
vGasAmounttovETH - Increase the
depositedCollateralAmountby the amount ofvETHreceived from the swap - Set the
borrowedVEthto 0 - Call the
resetBalancefunction to set thecurrentTokenAmount,vEthAmount&vGasAmountto 0
Therefore the trader's profit saved in the
vEthAmountvariable is deleted.Recommendation
Increase the positions
depositedCollateralAmountinstead of thevEthAmountwhen decreasing a long position with profit > the borrowedvETHamount.Resolution
Foil Team: The issue was resolved in PR#75.
- calculate the excess
-
C-09 Critical Traders Profit Can Be Stolen When Closing Logical Error Resolved
Description
When a trader closes a position before settlement, there is currently no protection against slippage.
As long as the trader possesses sufficient
vEth/vGasto settle their debts, the closure of the position will be successful.This vulnerability could be exploited by an attacker to siphon off the trader's profits by manipulating the price of the pool, causing the trader to swap at a premium that would be covered by their profits.
By ensuring enough is returned to cover the trader's debt, the attacker can retain the profit minus fees.
Recommendation
Implement a parameter for closing a position that includes slippage protection to prevent potential exploitation by malicious parties.
Resolution
Foil Team: The issue was resolved in PR#92.
-
H-01 High collateralRequirementAtMaxTick Underflow Underflow Resolved
Description
In
collateralRequirementAtMaxTick, this line can revert due to underflow:return totalLoanAmountInEth - maxAmount1;It is possible
totalLoanAmountInEthto be less thanmaxAmount1, as the loan amount of each token isloanAmount - tokensOwed. This means that the position is already overcollateralized by thetokensOwed+ liquidity position.Consider this scenario: 1. User opens liquidity position 2. They wash trade such that the fees paid for
token0andtoken1exceed the loan amountsloanAmount0andloanAmount1The fees are stored in
tokensOwed0andtokensOwed1. Therefore theloanAmount - tokensOwedof both tokens are 0. This is logical because there’s actually no collateral required to back a position who's loans is entirely backed by collected fees. However, sincemaxAmount1is greater than0, then the equationreturn totalLoanAmountInEth - maxAmount1;will underflow.An example where
loanAmountInEthbecomes 0 was chosen to make the underflow obvious, but just a slight reduction inloanAmountInEthcould make the underflow revert happen. This makes it impossible to increase or partially decrease liquidity for some liquidity positions.Recommendation
In the
collateralRequirementAtMaxTickfunction, consider returning0ifmaxAmount1 >totalLoanAmountInEth.Resolution
Foil Team: The issue was resolved in PR#91.
-
H-02 High Required Collateral Invalid For Partial Closes Logical Error Resolved
Description
In the
updateValidLpfunction theloanAmount0andloanAmountare computed by deducting the respectivetokensOwedfrom the loaned amount. However the amount credited to pay down the loan cannot exceed the loaned amount.Consider the following scenario:
- Trader A opens a position which is initially all
vEthliquidity - Price moves downwards, trader A’s position is now entirely
vGasliquidity - Trader A decreases their position and receives all vGas from reducing their liquidity
- Trader A’s
tokensOwed0are not reflected in a reduction of their loaned amount because they had
no loaned
amount0initially.- Thus trader A does not receive any collateral back and instead must supply more collateral
because the collateralization validation measures their position as being worth less.
As a result partial decreases are prevented for positions in this scenario as the
additionalCollateralis hardcoded to 0.Recommendation
Consider passing the
tokensOwed0andtokensOwed1through to thecollateralRequirementAtMinTickfunction and adding them to theavailableAmount0andavailableAmount1values respectively so that this value is not truncated to zero.Resolution
Foil Team: The issue was resolved in PR#91.
- Trader A opens a position which is initially all
-
H-03 High LP Stuck Because Of Underflow Logical Error Resolved
Description
How much
amount0andamount1an LP provides initially and how much it receives while closing the position are subject to change based upon the pool price.Considering LP's can provide liquidity amounts larger than their collateral to the pool, the following scenario is applicable in many situations:
LP's token 1 swapped to token 0 such that LP's
borrowedVEth - collectedVEthamount will be bigger than LP's collateral which will result with underflow while closing the position.This occurs when attempting to deduct the collateral in
_closeLiquidityPosition:position.depositedCollateralAmount = position.borrowedVEth - collectedAmount1;In this case the LP is prevented from closing their position.Recommendation
Allow excess debt that can’t be covered to live on in the
position.borrowedVEth.Note that the case where
position.borrowedVEthis left can only occur when the price of the pool has moved downwards through the LP relative to where the LP was first created.Thus the LP will take on a long position after closing and it is expected and correctly handled when a nonzero
position.borrowedVEthexists.Resolution
Foil Team: The issue was resolved in PR#91.
-
H-04 High Exact Input Amount May Not Be Used Validation Resolved
Description
In the
SwapRouter.exactInputSinglefunction in Uniswap V3, the exactamountInis not guaranteed to always be used. If thesqrtPriceLimitX96is hit during the swap then the swap will complete and theexactInputSinglefunction call will pass.https://github.com/Uniswap/v3-periphery/blob/0682387198a24c7cd63566a2c58398533860a5d1/c
A
sqrtPriceLimitX96of 0 is used in theEpochTradeModule.swapTokensExactInfunction, therefore thesqrtPriceLimitX96is assigned to roughly the min or max tick upon performing the actual swap.This means if the swap are to go outside of the range of valid prices for the epoch the exact input amount will not be entirely used up.In the context of a short, this can mean an overestimation of the amount borrowed which was not entirely used for the swap and causes immediate loss for the user. This is not an issue for the
exactOutputSinglefunction as theamountOutReceivedis validated to be exactly theamountOut:https://github.com/Uniswap/v3-periphery/blob/0682387198a24c7cd63566a2c58398533860a5d1/c
Recommendation
There are a number of ways this edge case can be validated against:
- Consider reverting if the price of the Uniswap pool is outside of the valid range after a swap, or as
an invariant check after all functions which interact with Uniswap.
- Consider validating whether the swap would put price outside of the valid range, and either
reverting or using a partial fill if this is the case.
- Consider reverting if the balance used up by the swap is not exactly the amount specified,
measured by the balance of
address(this).Resolution
Foil Team: The issue was resolved in PR#90.
-
M-01 Medium Mismatching Max Tick Boundary Logical Error Acknowledged
Description
The
baseAssetMaxPriceTickis the input parameter tocreateValidto set the maximum trading tick of the epoch.However, this tick is not the same tick that is used in
epoch.sqrtPriceMaxX96orepoch.maxPriceD18as the UniswaptickSpacingis added to the tick.It is important to distinguish between ticks andtickSpacing.Each tick is 0.01% price difference apart. A tickSpacing contains multiple ticks depending on the fee tier, and for a 1% fee pool this is 200 ticks which corresponds to a 2.02% price difference.
Therefore,
epoch.maxPriceD18andbaseAssetMaxPriceTickcorrespond to different ticks which are 2.02% price difference apart. ThemaxPriceD18is used to bound the settlement price, and is also thehighestPriceduring trades.baseAssetMaxPriceTickis used in thevalidateLpfunction, which limits the range which liquidity is added.Since the
baseAssetMaxPriceTickis lower thanmaxPriceD18, it is impossible for liquidity to be added up to the maximum price, and consequently for traders to swap to that price.Recommendation
Consider consistently using the
baseAssetMaxPriceTickto derive the max tick and max price without adding a tick spacing.If tick adjustment is necessary, consider adding a single
tickrather than an entiretickSpacing.Resolution
Foil Team: Acknowledged. 26
-
M-02 Medium initializeMarket Can Be Front Run Logical Error Resolved
Description
The owner of a market is set with the
initializeMarketfunction, which is called after deploying the system and can be called by anyone.This allows an attacker to take over the market by calling the function before the protocol calls it. A malicious owner would be able to configure malicious Uniswap contracts to steal user funds.
This also acts as a griefing attack as the protocol needs to pay gas for re-deploying the system.
Recommendation
Set the owner of the system at deployment.
Resolution
Foil Team: The issue was resolved in PR#74.
-
M-03 Medium Unnecessary Fees When Closing Short Logical Error Resolved
Description
When closing a short position in the
_closePositionfunction,position.vEthAmountis swapped tovGas. Next, ifposition.borrowedVGas > tokenAmountVGas, thenvEthis swapped back tovGas.Since tokens were swapped from
vEthtovGasback tovEththe position closer had to pay extra fees for unnecessary swaps when they could just swapped a lower amount ofvETHinitially and skipped the second swap.Recommendation
Consider first calculating the amount of
vETHthat needs to be swapped tovGasto close the position and then executing only a single swap.Resolution
Foil Team: The issue was resolved in PR#75.
-
M-04 Medium Trades May Revert At Maximum Tick Logical Error Acknowledged
Description
The
highestPricefor a swap is the exchange rate at the maximum tick. However, this exchange rate does not account for fees.Therefore, the
highestPricecould prevent swaps which occur near the edge of the tick range, as the price after fees are included exceeds thehighestPrice.Recommendation
Account for fees when calculating the highest price.
Resolution
Foil Team: Acknowledged.
-
M-05 Medium Settlement Price frontrunning Frontrunning Resolved
Description
Any user can grief the protocol by frontrunning the
submitSettlementPricefunction call and asserting a price directly to UMA.UMA determines the assertion ID by taking in the following parameters: assertionId = _getId(claim, bond, time, liveness, currency, callbackRecipient, escalationManager,
identifier);All of which an attacker can copy what Foil was going to use. When the attacker's transaction gets executed first, Foil's will revert shortly after with the following check:
require(assertions[assertionId].asserter == address(0), "Assertion already exists");Recommendation
Since time is one of the parameters to create an assertion, submitting the transaction through a private mem-pool will be sufficient to prevent this attack.
Resolution
Foil Team: Resolved.
-
M-06 Medium Owner Can Bypass UMA Assertion Checks Logical Error Resolved
Description
The
UmaSettlementModuleis a contract which ensures owner submits a valid settlement price.However, the owner can change the address of the oracle at any time, and the address does not have to be a legitimate oracle.
The owner can call
updateMarket, change theoptimisticOracleV3address to themselves, and then callassertionResolvedCallback()to accept a malicious price.Recommendation
Consider only allowing oracle updates when the epoch has not ended.
Resolution
Foil Team: The issue was resolved in PR#76.
-
M-07 Medium Underflow loanAmount Calculation Underflow Acknowledged
Description
In the
getCollateralRequirementForAdditionalTokenstheloanAmountis not capped at a minimum of 0 as it is done in theupdateValidLpfunction.This can lead to underflows if the borrowed amount plus the given increase amount is smaller than the
tokensOwed.Recommendation
Calculate the
loanAmountas it is done in theupdateValidLpfunction to prevent underflows.Resolution
Foil Team: Acknowledged.
-
M-08 Medium Collateral Can Be Stuck After Closing Position Logical Error Resolved
Description
When calling
modifyTraderPositionto close a position the system will not automatically withdraw all collateral of the position and instead withdraw based on the givencollateralAmount.This means when a trader makes the mistake of providing a positive
collateralAmountto themodifyTraderPositionwhen closing the position, the closed position will still own collateral.The user can't call the function again with a 0
tokenAmountand 0collateralAmountto withdraw the remaining collateral, as this will call_closePositionagain and perform a 0 token swap which will revert.Therefore there are only two ways to withdraw the remaining collateral:
- Wait till the epoch is settled (which could take up to a month)
- Call
modifyTraderPositionto open a new position and close it again (which costs trading fees)
Recommendation
Automatically withdraw all remaining collateral when closing a position.
Resolution
Foil Team: The issue was resolved in PR#75.
-
M-09 Medium Rounding In Favor Of User Rounding Resolved
Description
There are instances where the protocol rounds in favor of the user. Even though these rounding errors are small, a user withdrawing even a slight amount more than they are entitled to can lead to insufficient funds to pay out the last withdrawer.
In the
settlefunction ofPosition.sol:self.borrowedVEthrounds down:self.borrowedVEth =(self.borrowedVGas * settlementPriceD18) / 1e18;Then in the next line
self.borrowedVEthis subtracted as part of the user's collateral calculation:self.depositedCollateralAmount = self.vEthAmount - self.borrowedVEth;Since
borrowedVEthis lower than the exact value, then this makes the user's collateral slightly higher than it should be.In
_afterSettlementSwapExactOut, this equation calculates theamountIna user needs to get a certain amount out.Since it rounds down, the user can put in less than their required amount:
requiredAmountInVGas =amountOutVEth.divDecimal(epoch.settlementPriceD18);Recommendation
Consider substituting a division function which rounds in the instances where rounding down would be in favor of the user.
Resolution
Foil Team: The issue was resolved in PR#90.
-
M-10 Medium Market Updates Invalidate Previous Positions Logical Error Resolved
Description
updateValid()allows the owner to change the Uniswap v3NonFungiblePositionManager. However, changing this will invalidate thetokenID's of all previous positions, among other problems.Additionally, an update to
uniswapSwapRouterwill freeze previous positions as the tokens are approved to the old and not the new router.Recommendation
Consider storing the variables such as the
uniswapPositionManager,uniswapSwapRouterandoptimisticOracleas part of the epoch parameters so that changes to market parameters only apply to future epochs.Resolution
Foil Team: The issue was resolved in PR#76.
-
M-11 Medium Uniswap Rounding Can Create Insolvent Positions Rounding Resolved
Description
In Uniswap V3 the
getAmount0Deltafunction rounds in the favor of the Uniswap protocol and against the user. Specifically, when supplying liquidity the amount in is rounded up and when burning liquidity theamountOutis rounded down. This behavior results in potentially insolvent positions as the collateralization requirement may not have the same rounding against the user.For example, only one amount can be rounded by 1 wei since the position is assumed to be entirely in one asset. Additionally, the position may not be subject to precision loss at the max tick, but could be at the current tick.
Recommendation
Consider requiring an additional minimal amount of collateral to address any potential rounding from Uniswap that may occur. This amount could be as small as 2 wei.
Resolution
Foil Team: The issue was resolved in PR#91.
-
M-12 Medium Fees Missing In Required Collateral Calc Logical Error Resolved
Description
When calculating the required collateral for a trade position the uniswap fee to close the position is not included.
Therefore the trader might not be able to close the position with the deposited collateral as the fee was not accounted for.
The same could happen for liquidity positions which are converted to trade positions when they are closed.
Recommendation
Include the uniswap fees in the required collateral calculations.
Resolution
Foil Team: The issue was resolved in PR#47.
Guardian Team: As recommended in M-12 the fee is now added to the collateral requirement calculation, but this is only done if the epoch is settled. The fee is needed in this calculation before it is settled as before settlement trades happen in Uniswap. After settlement, the required collateral calculation is no longer needed in general. We recommend to always add the fee to the collateral requirement calculation.
-
M-13 Medium Missing onRecieved Check Best Practices Resolved
Description
Functions
createLiquidityPositionandcreateTraderPositionare minting position NFT's tomsg.sender.If the
msg.senderis a contract and is not capable of handling NFT related actions and/or is not capable of calling other functions in the system, this position NFT's will stuck at the contract.Considering all actions related to both LP's and Trader's have NFT ownership check, this can lead to locked funds for users.
Recommendation
Check if the caller of these functions can safely receive ERC721's via
checkOnErc721Received.Resolution
Foil Team: The issue was resolved in PR#62.
-
M-14 Medium Traders Can’t Close Position Pre Settlement Unexpected Behavior Acknowledged
Description
Traders may encounter difficulties in closing positions and, to a lesser extent, modifying positions.
But of utmost significance, traders may find themselves unable to close their positions before settlement if Liquidity Providers close their positions first.
As a result, traders may not be able to realize profit based on the current pool price and may have to wait until settlement, leading to temporarily locked funds and potential loss of yield for traders who are unable to close a profitable position promptly.
Recommendation
It is advised to document to users that the option to close trades before settlement is not guaranteed and is dependent on the availability of liquidity.
Resolution
Foil Team: Users in any market understand this is a possibility and are always able to trade out of any position with no slippage at expiration, so there is no additional risk compared to any other instrument/market.
-
L-01 Low Unexpected Collateral Amount Used For LPs Unexpected Behavior Acknowledged
Description
In the
updateValidLpfunction theadditionalCollateralamount is provided by the user but not used as the amount of collateral tokens transferred in.Instead the minimum required amount of collateral tokens are transferred in. Thus users will unexpectedly transfer in a lower amount of tokens than their provided
additionalCollateralvalue.Recommendation
Consider removing the use of
additionalCollateraland the associated validation and always transfer in the required collateral, while making it clear to the user how much collateral is to be transferred.Otherwise consider transferring in the entire
additionalCollateralamount and using this full amount for the position’s collateral.Additionally, consider standardizing on a consistent behavior across LP positions and trader positions.
Such that either both LP positions and trader positions transfer in the minimum required collateral or both transfer in a specified amount of collateral from the user.
Resolution
Foil Team: Because we are unable to use desired collateral changes as inputs, both trade and LP functions specify a desired position size and slippage protection is implemented by setting limits on additional collateral requirements.
-
L-02 Low Wrong Description For tokenByIndex Code Quality Resolved
Description
The description above the
tokenByIndexfunction describes the behavior of thetotalSupplyfunction.Recommendation
Update the description to match the behavior of the
tokenByIndexfunction and move the description to thetotalSupplyfunction.Resolution
Foil Team: The issue was resolved in PR#69.
-
L-03 Low Unsafe Collateral Transfers Validation Resolved
Description
Some ERC-20 tokens return a boolean instead of reverting therefore using
transferFromwill not revert when the transfer fails.This enables attack vectors in the system when such a token would be used as collateral.
Recommendation
Use
safeTransferFrominstead oftransferFromor be aware not to add such tokens to the system.Resolution
Foil Team: The issue was resolved in PR#73.
-
L-04 Low Misleading Error In submitSettlementPrice Code Quality Resolved
Description
The
submitSettlementPricefunction checks if the epoch is already settled and returns a "Market already settled" error in that case.This error is misleading because the epoch is settled, not the market.
Recommendation
Change the error message to "Epoch already settled".
Resolution
Foil Team: The issue was resolved in PR#61.
-
L-05 Low Protocol Vulnerable To Reentrancy Logical Error Resolved
Description
The
_closeLiquidityPositionfunction updates thedepositedCollateralAmountto 0 after withdrawing it. This pattern is vulnerable to a reentrancy attack if an ERC-777 token is used as collateral.Also, there is a
ReentrancyGuardinherited in theEpochLiquidityModulebut never used.Recommendation
Use a reentrancy guard in all state changing functions, or be aware not to add such tokens to the system.
Resolution
Foil Team: The issue was resolved in PR#66.
-
L-06 Low Missing Checks In assertionDisputedCallback Validation Resolved
Description
The
assertionResolvedCallbackfunction checks if the given assertion exists & that the epoch is not settled yet, but theassertionDisputedCallbackfunction does not.Recommendation
Add these checks to the
assertionDisputedCallbackfunction to prevent unexpected state changes.Resolution
Foil Team: The issue was resolved in PR#64.
-
L-07 Low Tokens With 18 Decimals Are Not Supported Validation Resolved
Description
The system assumes that the collateral token has the same precision as the
vETHtoken (18 decimals). If a collateral token with a different precision is added, calculations will be incorrect.Recommendation
Consider adding a check in the
initializeMarketflow to ensure that the collateral token has 18 decimals.Resolution
Foil Team: The issue was resolved in PR#77.
-
L-08 Low Revert On 0 Transfer Tokens Not Supported Validation Resolved
Description
The
updateCollateralcould transfer 0 tokens if the givenamtequals the currentdepositedCollateralAmount.Some tokens revert on 0 transfers, which can lead to DoS if such a token is added as collateral in the future.
Recommendation
Consider adding a if statement to check if the amount to transfer is greater than 0 before calling
IERC20.transfer.Resolution
Foil Team: The issue was resolved in PR#72.
-
L-09 Low swapTokensExactOut DoS In Edge Cases Logical Error Resolved
Description
The
swapTokensExactOutfunction checks at the end of the available amount invETHorvGASis bigger than theamountInand reverts otherwise.This means that it will revert if the available amount equals the
amountIn.Recommendation
Consider changing the condition to
availableAmount >= amountInto prevent unnecessary reverts.Resolution
Foil Team: The issue was resolved in PR#75.
-
L-10 Low Rebasing Tokens Are Not Supported Logical Error Acknowledged
Description
As the system calculates within absolute amounts, rebasing tokens are not supported.
When a rebasing token would be used as collateral excess tokens would be stuck in the system if the supply increased, or it would not be possible to withdraw collateral in some cases if the supply decreased.
Recommendation
Handle the collateral amounts with a share price calculation instead, or be aware to not use rebasing tokens as collateral.
Resolution
Foil Team: The issue was resolved in PR#65.
-
L-11 Low Missing Deadline Check MEV Resolved
Description
There are no deadline checks when performing swaps through Uniswap.
This can result in swaps occurring long after the transaction was initially submitted which means the user could have an unexpected price and their slippage parameters would be outdated.
Recommendation
Consider adding a deadline check to swaps and liquidity modification actions.
Resolution
Foil Team: The issue was resolved in PR#86.
-
L-12 Low Misleading Function Name Code Quality Resolved
Description
The function
validateEpochNotSettled()reverts if the the epoch has ended, regardless of whether it is settled, which contradicts the nameRecommendation
Consider changing the name of the function to reflect it's behaviour
Resolution
Foil Team: The issue was resolved in PR#60.
-
L-13 Low tokenByIndex Off By One Logical Error Resolved
Description
Whenever a token is minted in
createLiquidityPosition(), it's index is set tototalSupply() + 1.That means that the first token minted has an index of
1, not0and the highest indexed token has an index equal to the total supply.The
tokenByIndex()function reverts whenindex >= totalSupply. However it is incorrect to revert whenindex == totalSupplyas that is the index of a token that has already been minted.Recommendation
Consider changing
>=to>intokenByIndex.Resolution
Foil Team: The issue was resolved in PR#63.
-
L-14 Low Incorrect Comments In modifyTraderPosition Documentation Resolved
Description
In
modifyTraderPosition, it says that "closing can happen at any time". However, positions positions cannot be closed between when the epoch has ended and when it is settled.Recommendation
Consider removing the misleading comment, and correctly specifying when the position can be closed.
Resolution
Foil Team: The issue was resolved in commit 2d83d89.
-
L-15 Low Outstanding TODO Comments Code Quality Resolved
Description
Throughout the codebase there are outstanding TODO comments, some of which would address findings raised in this report.
Recommendation
Be sure to resolve all TODO comments.
Resolution
Foil Team: The issue was resolved in commit 4f864ab.
-
L-16 Low submitSettlementPrice Overwrites assertionId Logical Error Acknowledged
Description
In the
submitSettlementPricefunction theassertionIdis overwritten when the owner calls the function a second time before the first assertion has reached a terminal state.As a result the previous submitted assertion will not be able to settle as the callback functions in the module will revert.
Additionally, a previously submitted assertion could resolve as disputed after a new valid assertion is submitted by the owner.
This would mark the
settlement.disputedvalue astrueand disallow the settlement of the valid true assertion.Recommendation
Do not allow new assertions to be submitted by the owner until the existing assertion reaches a terminal state.
Resolution
Foil Team: For this iteration of the protocol, we intend to have only the owner address capable of asserting a settlement price. In the event that a false assertion is mistakenly submitted, the intent is that the owner address can immediately resubmit and overwrite the current assertion (resolving the bond sent to UMA separately).
-
L-17 Low Lacking Min/Max Tick Validation Validation Resolved
Description
When creating a new epoch an arbitrary
baseAssetMinPriceTickandbaseAssetMaxPriceTickare accepted and used for the respective epoch minimum and maximum prices.Additionally, an arbitrary
epochParams.feeRateis provided to determine the fee tier used in the Uniswap pool.However there is no validation that the
baseAssetMinPriceTickandbaseAssetMaxPriceTickare even multiples that adhere to the associated tick spacing of the chosen fee tier.This may lead to unexpected behavior and accounting issues.
Recommendation
Consider implementing validation in the
Market.createValidandMarket.updateValidfunctions to assert that thebaseAssetMinPriceTickandbaseAssetMaxPriceTickare indeed even multiples of the relevant tick spacing for the fee tier provided on theepochParams.Resolution
Foil Team: The issue was resolved in PR#70.
-
L-18 Low Disputes Prevent Settlement Logical Error Acknowledged
Description
In the
assertionResolvedCallbackthe settlement is prevented if theepoch.settledvalue has been assigned to true.However this value will be assigned to true as soon as any dispute, valid or not, is submitted.This is because the
assertionDisputedCallbackis called inside of thedisputeAssertionfunction in theOptimisticOracleV3contract.If the dispute is resolved to false, and the original claim is decided to be true, then the
settleAssertionfunction will then call theassertionResolvedCallbackwith a value of true forassertedTruthfully.However this call will not settle the epoch or set the settlement price in the Foil system as the
epoch.settlement.disputedis true.Recommendation
Use the
assertedTruthfullyvalue to decide whether the settlement should occur in theassertionResolvedCallbackfunction rather thanepoch.settlement.disputed.Resolution
Foil Team: Acknowledged.
-
L-19 Low Redundant refundAmountVGas Assignment Optimization Resolved
Description
In the
swapTokensExactOutfunction therefundAmountVGasis redundantly assigned twice when doing a gas for eth swap.Recommendation
Remove the second assignment of the
refundAmountVGasvalue.Resolution
Foil Team: The issue was resolved in PR#75.
-
L-20 Low Missing Two Step Ownership Change Warning Resolved
Description
Only owner of the market can create epochs, update market variables, and submit settlement price.
One step ownership change that is happening with
updateMarketcall is error-prone and can lead to catastrophic results such as not being able to submit settlement price and locked funds.Recommendation
Implement a two step ownership transfer mechanism,
Ownable2Steplibraries can also be used instead of custom ownership mechanism.Resolution
Foil Team: The issue was resolved in PR#71.
-
L-21 Low Config Lacks Adequate Opportunity For Disputes Warning Resolved
Description
Dispute time for settlement prices configured as one hour. It is possible that users can not react to settlements within configured time frame.
Recommendation
Consider giving more time to disputers such that wrong prices can be prevented by anyone around the world in any settlement hour. We recommend configuring it to at least six hours.
Resolution
Foil Team: The issue was resolved in PR#68.
-
L-22 Low Constant Time Implementations Warning Acknowledged
Description
It is possible to create epoch with variable lengths. Which is prone to mistake and can be guaranteed by code.
Recommendation
Make sure epoch times are 30 days by checking difference between
endTimeandstartTime.It is recommended to create epochs with just
startTimeas a variable andcreatingendTimeviaadding 30 days tothestartTime.Resolution
Foil Team: We’d like to retain the ability to have different epoch durations (to fit to the calendar year, for example).
-
L-23 Low Possible To Open Epochs For Past Validation Resolved
Description
It is possible to create epochs for the past. Which is prone to mistake.
Recommendation
Be sure to check
startTimeis at leastblock.timeStampResolution
Foil Team: The issue was resolved in PR#67.
-
L-24 Low Zero Checks In updateValid And createValid Validation Resolved
Description
Market can be created and updated with variables that are not putted as parameter which can lead to catastrophic effects.
Recommendation
Be sure to check parameters are not "0" in
createValidandupdateValidfunctions ofMarket.solResolution
Foil Team: The issue was resolved in PR#77.
-
L-25 Low Bond Currency Should Be Constant Warning Acknowledged
Description
A compromised admin has the ability to change the bond currency to an arbitrary token that only they have control over.
This action would enable them to submit any price without any opportunity for the price to be disputed.
Recommendation
It is advised to make the bond currency constant to ensure that the price can always be disputed.
Resolution
Foil Team: The bond currency is set at epoch creation (in EpochParams). A compromised admin would only be able to change it for an upcoming epoch, at which point market participants would have the option to leave the market.
-
L-26 Low Foil Overestimates Needed Collateral Logical Error Pending
Description
In order to ensure accurate validation of a user's collateral for their LP position, it is essential to calculate the appropriate amount of gas for the position's liquidity.
This is achieved by utilizing the
getAmount0ForLiquidityfunction, which has been customized based onUniswapV3's original function.However, the adjustment made to this function in Foil causes an underestimation of the
maxAmount0for a position.This results in the perceived value of the position being lower, leading to a greater collateral requirement to support the loaned amount.
As a consequence, users will be required to deposit slightly more collateral than necessary in some scenarios.
This rounding is fine from the protocol’s perspective because it ensures users are more collateralized instead of less collateralized.
Additionally, the rounding amount is trivial from the user’s perspective.
Recommendation
Be aware of this rounding and document it for users if deemed necessary.
Resolution
Foil Team: Pending.
-
L-27 Low LP Turned To Trader Will Encounter Price Impact Documentation Pending
Description
If an LP provide a liquidity to a ranger in order to short/long and their liquidity is used, hence they became a counter-party to the trader, their position closing will likely require two step:
- Closing the liquidity position which will turn the position to a trader.
- Closing the trader position.
Closing the trader position means swapping the same amount back (if epoch is not settled) which will encounter a price impact in the opposite direction this time.
Uninformed users about this might get surprised with the end result of their position closing before epoch is settled.
Recommendation
Be sure to inform users about all intricacies of being an LP and what their actions will result with exactly.
Resolution
Foil Team: Pending.
No findings match.
Invariants 16
The review's fuzzing suite asserted 16 invariants. 14 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
GLOBAL-01 | The price of vGAS should always be in range of the configured min/max ticks. | Held |
GLOBAL-02 | The system should never revert with a InsufficientBalance error from the | Held |
GLOBAL-03 | collateral token. There should never be any liquidity outside of the [min, max] range of an | Held |
GLOBAL-04 | epoch. The amount of vETH in the system, position manager & swap router should | Held |
GLOBAL-05 | equal the max supply The amount of vGAS in the system, position manager & swap router should | Held |
TRADE-01 | equal the max supply. The debt of a position should never be > the collateral of the position. | Held |
TRADE-02 | Long positions have their debt in vETH and own vGAS | Held |
TRADE-03 | Short positions have their debt in vGAS and own vETH. | Held |
LIQUID-01 | The debt of a position should not be > the collateral of the position. | Held |
LIQUID-02 | A open LP position should not own any vETH or vGAS. | Held |
LIQUID-03 | After all LP positions have been closed, for the remaining trader | Held |
SETTLE-01 | positions: net shorts == net longs. It should always be possible to settle all positions after the epoch is settled. | Broken |
STLESS-01 | UniV3 and Foils getAmount0ForLiquidity should output the same value when given the same | Broken |
POSITION-01 | inputs. Discovery range size should not change | Held |
POSITION-02 | Anchor liquidity stays the same post-drop | Held |
EPOCH-01 | Floor reserves should not decrease post-drop within delta | Held |
More from Sapience
All 6 reports-
LayerZero Composer
7 findings 7 findings: 5 low, 2 informational -
Foil Vault and Prediction Market
61 findings2 critical · 3 high 61 findings: 2 critical, 3 high, 7 medium, 26 low, 23 informational -
Sapience
87 findings8 high 87 findings: 8 high, 18 medium, 30 low, 31 informational -
Foil Updates
36 findings2 critical · 4 high 36 findings: 2 critical, 4 high, 7 medium, 23 low
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
