Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · January 2025

Foil Vault

for Sapience

Foil engaged Guardian to review the security of its Vault, providing liquidity across the epoch's price range. From the 19th of November to the 27th of November, a team of 6 auditors reviewed the source code in scope.

Published
Review window
November 19 to 27, 2025
Language
Solidity
Chains
Ethereum
Sector
Derivatives
  • 2 Critical
  • 2 High
  • 14 Medium
  • 17 Low
  • 0 Informational

23 resolved · 12 acknowledged

Scope

Overview

Foil engaged Guardian to review the security of its Vault, providing liquidity across the epoch's price range. From the 19th of November to the 27th of November, a team of 6 auditors reviewed the source code in scope.

Issues Detected Throughout the engagement 4 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 Vault.

Security Recommendation Given the number of High and Critical issues detected, Guardian supports a secondary security review of the Vault at a finalized frozen commit. Furthermore, the Foil team should increase testing with various settlement scenarios which may present opportunities to DoS the Vault’s operations.

Findings 35

  1. C-01 Critical Total DoS Of Epochs DoS Resolved
    Location
    Vault.sol: 604-619

    Description

    Users can create redemption requests for their vault shares using the requestRedeem function, which will increase the totalPendingWithdrawals variable. The only requirement regarding the request amount is that the users' balance must be sufficient.

    Users’ shares are neither transferred nor burned at the creation of the request. Since these shares are transferable, a user can create a request using requestRedeem, transfer shares to another address, create another request, and repeat this process as many times as desired.

    As a result, totalPendingWithdrawals will be inflated. This allows users to manipulate pendingSharesToBurn and totalSupply, or even cause a complete DoS in the system due to an underflow here in the _reconcilePendingTransactions function.

    Recommendation

    The redeem workflow should transfer tokens during the request creation process, similar to the deposit flow.

    The requestRedeem function should transfer shares from the user to the vault. And then, the _redeemShares function should burn these shares from the vault instead of burning from the owner.

    Resolution

    Foil Team: The issue was resolved in PR#193.

  2. C-02 Critical Bond Cannot Be Returned Logical Error Resolved
    Location
    Vault.sol: 133

    Description

    When submitMarketSettlementPrice is called in Vault.sol, the vault is set as the asserter in the UMA oracle. Upon successful settlement of the assertion price, the bond is returned to the vault.

    However, there is no mechanism to refund this bond to the user who submitted the price and paid for it. Additionally, there is no recovery function, causing the bond to remain permanently stuck in the vault.

    Recommendation

    In UMASettlementModule.submitSettlementPrice, allow the caller to specify an address to be set as the asserter. Then, in Vault.submitMarketSettlementPrice, ensure the caller’s address is passed as the asserter to enable proper bond refunds.

    Resolution

    Foil Team: The issue was resolved in PR#181.

  3. H-01 High tradeRatio Rounded In Wrong Direction Rounding Resolved
    Location
    TradeModule.sol

    Description

    The recommendation of H-03 is to round up the trade ratio when going towards the long direction as a short. However, the fix implemented is the opposite - the trade ratio is being rounded down if isLongDirection and rounded up otherwise. Since the problem is not solved, the insolvency issue still exists.

    Currently, tradeRatioD18 is used to compute both closePnL and vEthAmount/borrowedVEth. Foil's goal should always be to maximize borrowedVEth and minimize closePnL and vEthAmount. Because of this, different rounding directions should be used depending on what's being calculated.

    Recommendation

    The end goal should be to maximize the borrowedVEth and minimize the vEthAmount and closePnL. To accomplish this, you can have two different tradeRatios - one rounded down and one rounded up. You will also have three different vEthToZero.

    The first one will be to calculate the closePnL and you will use the tradeRatio that’s rounded down if the position is a long, otherwise use the rounded up one. The second vEthToZero will always use the tradeRatio that’s rounded down and the third vEthToZero will always use the tradeRatio that’s rounded up.

    Next, you will also have two different vEthFromZero for each vEthToZero. Finally, in the if/else statement where you set borrowedVEth and vEthAmount you will choose the appropriate vEthFromZero.

    For the if case you should use the vEthFromZero which absolute value is bigger to maximize borrow and for the else case you should use the vEthFromZero which absolute value is smaller to minimize the credited vETH.

    Resolution

    Foil Team: The issue was resolved in PR#198.

  4. H-02 High Faulty Quoting With Small Amounts Logical Error Resolved
    Location
    LiquidityModule.sol

    Description

    Proof of concept: PoC

    When a new epoch is created, the Vault uses the assets in its reserves to deposit them as collateral in order to create a liquidity position. The vault will call quoteLiquidityPositionTokens to get the amount0 and amount1 that can be added as liquidity for the available collateral.

    However, Epoch.requiredCollateralForLiquidity() now adds 1 to loanAmount0 and loanAmount1. This means the actual required collateral for the position may exceed the available collateral in the vault.

    In result, the transaction will revert because of InsufficientCollateral() and the epoch creation will not be successful. This issue can occur with non-trivial amounts, for example 1e17.

    Recommendation

    Consider implementing higher minimum collateral amounts and documenting this behavior for clarity. Another option to consider is subtracting 1 wei from amount0 and amount1 when creating the LiquidityMintParams, which should account for the additional 1 wei.

    Resolution

    Foil Team: The issue was resolved in PR#197.

  5. M-01 Medium Position With Zero Collateral Logical Error Resolved
    Location
    TradeModule.sol

    Description

    Proof of concept: PoC

    When a position is operating with small amounts, the required collateral for the position can be calculated to be zero due to rounding when calculating value of debt. Consequently, a user can modify their position to a size within a couple thousand wei and have to provide zero collateral.

    All their prior deposited collateral would be returned, and their position would have no backing. In the original review, this issue was not possible since the minimum requiredCollateral was always at least 2 wei.

    Recommendation

    Have a minimum required collateral.

    Resolution

    Foil Team: The issue was resolved in PR#198.

  6. M-02 Medium Inaccessible onlyOwner Functions Access Control Acknowledged
    Location
    Vault.sol

    Description

    Since the Vault contract will be executing the onlyOwner createEpoch() function, it will be set as the owner of the foil system. The ConfigurationModule.updateMarket() function can be called by the Foil owner to update the market parameters.

    However, this function is never called in the Vault contract. This means the market can never be updated once the ownership is transferred to the contract. There is also no call to transferOwnership in Vault, so you can't just use a new vault as the owner.

    Recommendation

    Add calls to updateMarket and transferOwnership in the vault.

    Resolution

    Foil Team: We are keeping everything immutable.

  7. M-03 Medium DoS Via Deposit Before First Epoch DoS Resolved
    Location
    Vault.sol: 341

    Description

    Deposits before the first epoch are possible, with a minimum deposit amount of 1e3. Any pending deposits before the first epoch are utilized to establish the initial liquidity position within the _createNewLiquidityPosition function.

    This function deducts a dust amount of 1e4 from the deposited collateral amounts. If a user intentionally deposits an amount between 1e3 and 1e4 before the first epoch, and there are no other deposits, the initialization will fail due to underflow at this line.

    Recommendation

    Consider setting the minimum deposit amount higher than the dust. Alternatively, keep the codebase unchanged but externally deposit the difference if this situation occurs.

    Resolution

    Foil Team: The issue was resolved in PR#197.

  8. M-04 Medium DoS Via Frontrunning Pool Creation DoS Resolved
    Location
    Epoch.sol: 145

    Description

    After the implementation of the Vault, epoch settlements and the creation of the new epoch happens at the same transaction via callbacks. Because of this atomic behavior, failure of the pool creation for the next epoch will DoS the settlement of the previous epoch.

    An attacker can precompute the virtual token addresses and create the Uniswap pool with these addresses as creating pools is permissionless in the Uniswap. This will cause Epoch.createValid function to revert while calling IUniswapV3Factory.createPool due to require(getPool[token0][token1][fee] == address(0)) check in the factory.

    The attack can cause complete blocking of the epoch settlements and creations. However, attackers must keep frontrunning and create new pools every time someone tries to settleAssertion in the optimistic oracle.

    Recommendation

    Check whether the pool already exists or not by calling the getPool in the factory instead of directly calling the createPool. If the pool already exists, check whether it was already initialized or not and set the starting price. Alternatively, always make sure to use a private RPC to prevent frontrunning.

    Resolution

    Foil Team: The issue was resolved in PR#209.

  9. M-05 Medium Gas Griefing Of Epoch Creation Griefing Resolved
    Location
    Epoch.sol: 206

    Description

    When a new epoch is created, block.timestamp is used as the salt for generating two virtual tokens. In _createVirtualToken, a loop probes for an available salt if a collision occurs.

    However, the salt increments by 1 on each iteration, making it highly predictable and susceptible to front-running. An attacker can exploit this predictability to deliberately create collisions.

    During testing, each iteration of the loop was found to cost approximately 600k gas, making it feasible for an attacker to force the epoch creation process to fail due to an Out-of-Gas error.

    Recommendation

    Consider using a less predictable and more robust mechanism for generating the salt, such as hashing with block variables. Alternatively, consider using CREATE3 which ensure that the address is only dependent on deployer and salt.

    Resolution

    Foil Team: The issue was resolved in PR#197.

  10. M-06 Medium Positions With 0 Collateral Logical Error Resolved
    Location
    TradeModule.sol

    Description

    When a position is operating with small amounts, the required collateral for the position can be calculated to be zero due to rounding when calculating value of debt. Consequently, a user can modify their position to a size within a couple thousand wei and have to provide zero collateral.

    All their prior deposited collateral would be returned, and their position would have no backing. In the original review, this issue was not possible since the minimum requiredCollateral was always at least 2 wei.

    Recommendation

    Have a minimum required collateral.

    Resolution

    Foil Team: The issue was resolved in PR#198.

  11. M-07 Medium Fee Collector Can Hoard Fees Logical Error Acknowledged
    Location
    LiquidityModule.sol

    Description

    M-01 of the previous audit was not addressed. Fee collectors can create under-collateralized positions and collateralize them using depositCollateral.

    Currently, there are no restrictions preventing a fee collector from creating an oversized liquidity position, which can monopolize all available liquidity and hoard fees, preventing other fee collectors from benefiting.

    Recommendation

    Impose limits on the size of liquidity positions that fee collectors can create to ensure fair distribution of fees.

    Resolution

    Foil Team: Acknowledged.

  12. M-08 Medium Decreasing LP May Require Collateral Logical Error Acknowledged
    Location
    Epoch.sol

    Description

    Proof of concept: PoC

    The M-05's recommendation to let the user specify an amount of collateral to be added when decreasing a liquidity position has not been implemented which leaves the problem unsolved.

    Recommendation

    Allow the user to supply additional collateral when decreasing their position.

    Resolution

    Foil Team: Letting users know to decrease by larger than a few wei is acceptable due to this rounding issue.

  13. M-09 Medium vEth Credited When Closing A Position Logical Error Resolved
    Location
    TradeModule

    Description

    Proof of concept: PoC

    When closing a position, the vEthToZero is calculated as initialSize * tradeRatio and should be equal to the signedTradedVEth. However, Solidity division truncates the result. Because of this, tradeRatio will be slightly off - both when rounded down or up - therefore vEthToZero as well.

    Even though vEthFromZero should be roughly equal to targetSize * tradeRatio, the value assigned to it (for targetSize = 0) will be non-zero - positive or negative depending on the rounding. After that the absolute value of vEthFromZero will be assigned to vEthAmount.

    In result, closed positions end up having positive vEthAmount, which is especially bad for long positions. This ultimately leads to an undercollateralized market, preventing the last user from settling.

    Recommendation

    Consider setting the vEthAmount of the new position to 0, if its size is 0 as well.

    Resolution

    Foil Team: The issue was resolved in PR#198.

  14. M-10 Medium resolutionCallback Fails On Small Amounts Logical Error Resolved
    Location
    Vault.sol

    Description

    Function _createEpochAndPosition passing is critical to the Vault's flow, since if the resolutionCallback fails the Vault's functionality is stopped. If the Vault has more collateral than the current minimum collateral, the Vault attempts to _createNewLiquidityPosition.

    The issue is that even with enough collateral to meet the minimum threshold, is it not guaranteed that the liquidity to-be minted from the calculated amount0 and amount1 is greater than 0 due to Uniswap rounding down on small amounts, which would trigger a revert in UniswapV3Pool.mint: require(amount > 0);

    Ultimately, the Vault will attempt to mint which will revert, causing the callback to fail and the mints/epoch creation will not occur.

    Recommendation

    Consider enforcing a higher minimum collateral.

    Resolution

    Foil Team: The issue was resolved in PR#197.

  15. M-11 Medium Cleared borrowedVEth Logical Error Resolved
    Location
    TradeModule

    Description

    Proof of concept: PoC

    When a long position is being modified, its borrowedVEth is set to the absolute value of vEthFromZero. In some cases, it's possible to have a small amount of vGasAmount with 0 vEthFromZero due to the traded vETH matching the vEthToZero, primarily when operating with small position sizes and trade prices.

    This leads to a long position that does not have a loaned amount. This leaves the Foil contract with less available collateral than it should have and in result, the last user will not be able to exit.

    Recommendation

    Validate that any opened long position has positive borrowedVEth: require(borrowedVEth > 0)

    Resolution

    Foil Team: The issue was resolved in PR#198.

  16. M-12 Medium Resetting Does Not Refund Tokens Logical Error Resolved
    Location
    Vault: 649 + 530

    Description

    Proof of concept: PoC

    When a user calls withdrawRequestRedeem(), if their balance after is less than minimumCollateral then resetTransaction() will set their pending amount to zero. However, totalPendingWithdrawals will only be decremented by the amount of shares the user passes in.

    This will lead to totalPendingWithdrawals being larger than the actual amount that is intended to be withdrawn. A malicious user could continuously call requestRedeem() in conjunction with withdrawRequestRedeem in order to inflate totalPendingWithdrawals to be larger than collateralFromPreviousEpoch plus totalPendingDeposits.

    This will cause a DoS via underflow when _reconcilePendingTransactions() is called. Additionally, when a user calls withdrawRequestDeposit(), totalPendingDeposits is only decremented by assets. This will lead to the user’s remaining tokens to be donated to other users of the protocol.

    Recommendation

    If the user’s remaining amount is less than the minimumCollateral, then decrement totalPendingWithdrawals by the full amount or refund the remainder of their balance before calling resetTransaction(), depending on if it is a redeem or deposit.

    Resolution

    Foil Team: The issue was resolved in PR#197.

  17. M-13 Medium Collateral Of Epoch Ahead Can Be Stolen Logical Error Resolved
    Location
    Vault.sol: 288

    Description

    When updating share price after an epoch, if no collateral was received, the share price is set to 1e18. This creates a significant issue as depositors can redeem their entire collateral even though no collateral was received after closing the liquidity position.

    Effectively, this allows depositors to withdraw funds that belong to the next epoch's depositors, who have already transferred their collateral into the contract.

    Recommendation

    Initially, setting sharePrice to 0 instead of 1e18 was considered. However, this would affect the minting of new shares for the next epoch.

    As a solution, if no collateral is received, set the sharePrice for the current epoch to 0 while ensuring the sharePrice for the next epoch is reset to 1e18.

    Resolution

    Foil Team: The issue was resolved in PR#197.

    Guardian Team: The issue was not fixed. The price of the epoch is hardcoded to 1e18 if no collateral is received.

    Foil Team: Let’s halt the vault and allow a request deposit of something higher than 1e8 which will fix this.

  18. M-14 Medium Tick Modulus Hardcoded For Fee Tier Logical Error Resolved
    Location
    Vault: 273, 276

    Description

    _calculateTickBounds() uses modulus 200 in order to set the target tick value to the closest acceptable tick range. However, Foil is compatible with multiple fee tiers, but the value 200 is not.

    For instance, the 0.3% fee tier uses a tick spacing of 60, which is not a divisor of 200. This will cause a revert when attempting to create the epoch.

    Recommendation

    Instead of hardcoding 200, use the appropriate value for the fee tier of the pool.

    Resolution

    Foil Team: The issue was resolved in PR#197.

  19. L-01 Low Single Vault Circuit Should Not Skip Iteration Logical Error Acknowledged
    Location
    Vault.sol: 233

    Description

    In _calculateNextStartTime, if there is a significant delay in resolving an epoch, the vault skips an entire vaultCycleDuration to maintain synchronization with other vaults in the circuit.

    However, if only a single vault exists in the circuit, this synchronization is unnecessary. Skipping vaultCycleDuration in this scenario causes unnecessary downtime where no vaults are available.

    Recommendation

    Introduce a condition to check if only one vault exists in the circuit. In such cases, avoid skipping the vaultCycleDuration and instead start the next epoch immediately after resolution.

    Resolution

    Foil Team: Acknowledged.

  20. L-02 Low Overflow In DecimalPrice Library Overflow Resolved
    Location
    DecimalPrice.sol

    Description

    There is a comment left on L-02 that Foil now uses OpenZeppelin's code for its calculations, but the code in DecimalPrice is not changed - it's still possible for the result of the multiplication to exceed 2^256-1

    Recommendation

    Fix the issue.

    Resolution

    Foil Team: The issue was resolved in PR#202.

  21. L-03 Low Deposit/Withdraw On Behalf Of Others Access Control Acknowledged
    Location
    Vault.sol

    Description

    In the Vault contract, only the owners can request deposits and redemptions. However, claiming of these requests are external and anyone can claim on behalf of the owner.

    Even though the owner created these requests, timing of the claim might matter for the owner and these actions should be access controlled.

    Recommendation

    Not allow other users to claim on behalf of owners.

    Resolution

    Foil Team: I don’t think there’s any advantage to claiming after the epcoh is settled, if anything, these functions not being gated gives us flexibility to force redemptions to clear any pending txns.

  22. L-04 Low New Vaults Cannot Be Added Warning Acknowledged
    Location
    Vault.sol

    Description

    totalVaults is stored as an immutable variable when Vault.sol is created. This implies that no new vaults can be added after the first batch of vaults. This may run counter to protocol design that new collateral types may be added.

    Recommendation

    Consider allowing for new vaults to be added.

    Resolution

    Foil Team: At least the plan right now is not to add any more vaults once a vault is initialized.

  23. L-05 Low minCollateral Redeem Denomination Validation Acknowledged
    Location
    Vault.sol

    Description

    Vault.requestRedeem requires the amount of shares being redeemed to be greater than minimumCollateral. However, minimumCollateral is denominated in assets, not shares.

    Recommendation

    Consider having different validation with the proper denomination.

    Resolution

    Foil Team: Maybe a rename of the variable would be better. Will do that.

  24. L-06 Low Cheaper Settlement Delay Logical Error Acknowledged
    Location
    Global

    Description

    As pointed out in this issue, anyone can dispute rightful assertions to delay the start of a given epoch by paying the bond of $5000.

    Since now anyone can submit a price, the same entity can assert a rightful price and dispute it at the same time towards the end of the assertionLiveness period.

    By doing so, they will receive half of their disputer bond. In result, the cost of the attack will be reduced from $5000 to $2500.

    Recommendation

    Be aware of the reduction in cost.

    Resolution

    Foil Team: Acknowledged.

  25. L-07 Low Insufficient Balance For Last Withdrawer Logical Error Resolved
    Location
    SettlementModule.sol

    Description

    The last user attempting to settle their position may be unable to do so if: market.collateralAsset.balanceOf(address(this)) < withdrawableCollateral.

    This discrepancy can occur due to minor rounding errors during trade or liquidity activities, leaving the contract balance short by a few wei. As a result, the user cannot fully recover their collateral.

    Recommendation

    If market.collateralAsset.balanceOf(address(this)) is less than withdrawableCollateral, consider transferring the remaining contract balance to the user instead.

    This ensures the user can recover as much of their collateral as possible without leaving residual funds in the contract.

    Resolution

    Foil Team: The issue was resolved in PR#202.

  26. L-08 Low Consider Adding Exception Handling Mechanisms Logical Error Resolved
    Location
    Global

    Description

    With the implementation of the Vault contract, the settlement of the previous epoch and the creation of the next epoch happen in a single transaction.

    Because of this, an unexpected failure at any step of the process (e.g., settlement, new epoch creation, quoting, or adding new liquidity) may cause the system to halt.

    Recommendation

    Consider implementing mechanisms like try/catch blocks along the transaction flow, allowing unexpected issues to be resolved externally and ensuring the system remains operational.

    Resolution

    Foil Team: The issue was resolved in PR#209.

  27. L-09 Low minTradeSize For Liquidty Turned Trade Logical Error Acknowledged
    Location
    TradeModule.sol

    Description

    When closing a liquidity position, it can turn into a Trade position if it cannot be repaid. If the amount left for the new Trade position is less than the minTradeSize, the owner of the position will not be able to directly close it.

    They will have to make a bigger trade and close if after that. By doing so, they suffer losses because of price impacts.

    Recommendation

    Be sure to warn the users of Foil about this case.

    Resolution

    Foil Team: Acknowledged.

  28. L-10 Low Traders Unable To Close Profitable Position Logical Error Acknowledged
    Location
    Global

    Description

    L-20 of the previous audit was not addressed. Fee Collectors opened LP positions at the beginning of an epoch and deposit collateral after they've earned fees. This collateral could be streamed in periodically or provided in bulk at settlement.

    Due to the under-collateralized LP positions, traders may find themselves unable to exit profitable positions until Fee Collectors deposit collateral. As Fee Collectors are expected to hold large LP positions, this may affect a large group of traders.

    This leads to temporarily locked funds and potential loss of yield for traders who are unable to close a profitable position promptly.

    Recommendation

    Consider implementing a minimum deposit amount for fee collectors. Or else, document this risk for users.

    Resolution

    Foil Team: Acknowledged.

  29. L-11 Low Epoch startTime Not Utilized Logical Error Resolved
    Location
    Epoch.sol

    Description

    Epochs in Foil have startTime. However, liquidity and trades for a given epoch can be executed as soon as the epoch is created, no matter its startTime.

    Recommendation

    Be aware of this behavior.

    Resolution

    Foil Team: The issue was resolved in PR#202.

  30. L-12 Low Incorrect Error String Logical Error Resolved
    Location
    Vault.sol: 642

    Description

    "Previous deposit request is not in the same epoch" message in the withdrawRequestRedeem function (L642) should be "Previous withdraw request is not in the same epoch".

    Recommendation

    Change the error string in the require statement.

    Resolution

    Foil Team: The issue was resolved in PR#202.

  31. L-13 Low Pending Functions Might Be Misleading Informational Resolved
    Location
    Vault.sol: 543, 662

    Description

    Vault contract has pendingDepositRequest and pendingRedeemRequest functions. However, these functions do not check the transaction type of the pending request and directly return userPendingTransactions[owner]. pendingDepositRequest function can return a redeem request and vice versa.

    Recommendation

    Consider checking the transaction type in these functions, or implement a single function (e.g. pendingRequest) for all transaction types.

    Resolution

    Foil Team: The issue was resolved in PR#202.

  32. L-14 Low Insufficient Trade Size Validation Validation Resolved
    Location
    TradeModule.sol

    Description

    A minTradeSize configuration has been added to the Market in response to L-15. This works fine for createTraderPosition, but it's wrongly implemented in modifyTraderPosition. It calls _checkTradeSize(size) to ensure the trade size is bounded.

    However, the argument passed is size (the final size), not deltaSize. Because of this, small trades (below the minTradeSize) will still be successfully executed.

    Recommendation

    Pass deltaSize instead of size to _checkTradeSize to ensure trades are beyond a minimum delta.

    Furthermore, consider also validating the resulting size of the position, such that situations do not arise where a user creates a large position, decreases by position size-1, such that the delta trade size is large enough but the final position size is 1 wei.

    Resolution

    Foil Team: The issue was resolved in PR#198.

  33. L-15 Low Fee Collectors Can Make Unbacked Trade Positions Logical Error Acknowledged
    Location
    LiquidityModule.sol

    Description

    Proof of concept: PoC

    Because the fee collector is not required to deposit collateral for their position, situations can arise where fee collectors close their liquidity position yet the the resulting position will become a Trade position with non-zero borrowed amounts but zero credit amounts.

    This is because fee collectors will typically enter the following case if (position.depositedCollateralAmount < collateralDelta) due to no collateral requirements which will set a non-zero borrowedvETH.

    Consequently, an unbacked Trade position may be created that cannot be directly decreased and closed, since the deltaSize would be zero and Errors.DeltaTradeIsZero() would be triggered.

    Recommendation

    Clearly document this behavior and even consider if FeeCollectors should close their LP positions before epoch settlement as this allows them to profit without ever depositing collateral.

    Resolution

    Foil Team: Acknowledged.

  34. L-16 Low Negative Ticks Are Rounded Up Logical Error Acknowledged
    Location
    Vault.sol: 271

    Description

    During epoch creation, in _calculateTickBounds positive ticks are rounded down but negative ticks are rounded up, which could lead to unexpected behavior.

    Recommendation

    Consider rounding down negative ticks for consistency or clearly documenting this behavior.

    Resolution

    Foil Team: Acknowledged.

  35. L-17 Low Vault Is Not EIP Compliant EIP Resolved
    Location
    Vault.sol

    Description

    Multiple functions in the Vault are not EIP compliant.

    • totalAssets: Must not revert. However, it can revert if the positionId is not valid.
    • convertToShares: Must not revert. However, it can revert if totalAssets == 0.
    • preview functions: Must be as close as possible to on-chain conditions and must not revert based

    on vault specific user/global limits. May only revert that would also cause mint/withdraw etc. to revert too. However, these function are not supported at all.

    • deposit: Mints shares by depositing exactly assets amount. However, the amount is ignored in the

    codebase.

    • mint: Mints exactly the shares amount. However, the amount is ignored in the codebase.
    • withdraw: Burns shares and sends exactly the assets amount. However, the amount is ignored in

    the codebase.

    • redeem: Burns exactly the shares amount. However, the amount is ignored in the codebase.

    The contract incorrectly signals supporting the ERC4626 interface with the supportsInterface function.

    Recommendation

    One option is trying to make the contract EIP compliant. However, based on what the contract wants to achieve, it might be best to not support ERC4626.

    Consider removing interfaceId == type(IERC4626).interfaceId line from the supportsInterface function to prevent incorrect signaling for the external integrators.

    Resolution

    Foil Team: The issue was resolved in PR#202.

Invariants 37

The review's fuzzing suite asserted 37 invariants. 25 held and 12 did not.

Every invariant tested
IDInvariantResult
GLOBAL-01The price of vGAS should always be in range of the configured min/max ticks.Held
GLOBAL-02There should never be any liquidity outside of the [min, max] range of an epoch.Held
GLOBAL-03The amount of vETH in the system, position manager & swap router should equal theHeld
GLOBAL-04max supply The amount of vGAS in the system, position manager & swap router should equal theHeld
TRADE-01max supply. The debt of a position should never be > the collateral of the position.Held
TRADE-02Long positions have their debt in vETH and own vGASBroken
TRADE-03Short positions have their debt in vGAS and own vETH.Broken
TRADE-04Trader should never have both borrowedVGas and borrowedVEth beHeld
TRADE-05non-zero. Trader's pending loss in ETH-worth should never exceed collateral put down ( should never be in negative equity)Broken
TRADE-06after creating/modifying trade position, the depositedCollateralAmount > debtValue -Held
TRADE-07tokensValue After creating a trade position deposited collateral should be non-zeroHeld
TRADE-08After user closes a trade position, no vGAS, vETH, borrowed vGAS, borrowed vETHHeld
TRADE-09After creating a trader position, positionSize is non-zero.Held
TRADE-10createTradePosition should create a unique positionIdHeld
LIQUID-01The debt of a position should not be > the collateral of the position.Held
LIQUID-02A open LP position should not own any vETH or vGAS.Held
LIQUID-03After all LP positions have been closed, for the remaining trader positions: net shorts == netHeld
LIQUID-04longs. Position.depositedCollateralAmount should be at least the required collateral for their positionBroken
LIQUID-05if their position turned into a Trade type. QuoteLiquidityPositionTokens should match how many tokens are borrowed and how much liquidity is added after creating an LP positionHeld
LIQUID-06with createLiquidityPosition After creating an LP position, liquidity in the Uni pool increasesHeld
LIQUID-07After increasing an LP position, liquidity in the Uni pool increasesHeld
LIQUID-08After decrease an LP position, liquidity in the Uni pool decreasesBroken
LIQUID-09After partial decrease an LP Position, should not get InsufficientColateral revertBroken
LIQUID-10(unexpected in this case) createLiquidityPosition should create a unique positionIdHeld
SETTLE-01It should always be possible to settle all positions after the epoch is settled.Broken
SETTLE-02After settlement with settlePosition, position should not have any borrowedvETH nor borrowedVGAS, and noHeld
SETTLE-03vGAS nor vETH (cleared out position) Settlement should not revert with ERC20InsufficientBalance.Broken
SETTLE-04Settlement should not panic underflowBroken
EPOCH-01Position with non zero loan amount for lp should always have non-zero collateralHeld
VLT-01required. Vault functions should never revert with ERC20InsufficientBalance errorHeld
VLT-02totalPendingDeposits should be sum of deposit requests - withdrawRequestDeposit(s)Broken
VLT-03totalPendingWithdrawals should be sum of requestRedeem(s) -Broken
VLT-04withdrawRequestRedeem(s) pendingSharesToBurn should always be less than or equal to total supply of sharesHeld
VLT-05Pending transaction requested epoch should never be greater than current epochHeld
VLT-06Vault should not PanicBroken
VLT-07mint/deposit should decrease balance of shares in the Vault contract, total supplyHeld
VLT-08should stay the same redeem/withdraw should decrease total supplyHeld

More from Sapience

All 6 reports
  1. LayerZero Composer

    7 findings 7 findings: 5 low, 2 informational
  2. Foil Vault and Prediction Market

    61 findings2 critical · 3 high 61 findings: 2 critical, 3 high, 7 medium, 26 low, 23 informational
  3. Sapience

    87 findings8 high 87 findings: 8 high, 18 medium, 30 low, 31 informational
  4. 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.

Get a quote