Velocore's Incident Analysis

On June 2nd, 2024, Velocore suffered an attack on their CPMM pool, resulting in financial losses of approximately $6.8 million. The root cause of the exploit was faulty logic in calculating transaction fee in ConstantProductPool, combined with an integer underflow.

Overview

Exploit Analysis

In technical documentations, Velocore recommends that users interact directly with the Vault contracts to perform actions. The most important functions, from the users' perspective, is Vault.execute(). This function allow users to swap, stake, convert or vote without needing to know the address of internal pools, routers, etc. Furthermore, it supports batching operations, allowing users to perform multiple actions in a single transaction.

Thanks for reading Verichains! Subscribe for free to receive new posts and support my work.

A typical execution flow for a token swap operation is as follows:

  1. The user calls SwapFacet.execute() with an array of tokenRef and a list of operations (ops).

  2. SwapFacet then invokes its internal _execute() function to process the user's operations.

    1. _execute() iterates through ops and processes each operation based on its type.

    2. If the current operation is a swap request, _execute() calls the corresponding pool's velocore__execute() to simulate the swap and return the balance delta. It then invokes _verifyAndApplyDelta() to verify and apply the results.

  3. After returning to the outer execute() function, it checks the user's balances and handles transfers to or withdrawals from the user's wallet.

There are two types of pool in Velocore: volatile pool (CPMM) and stable pool (Wombat pool). The attacked pool is a volatile pool, so we review its implementation in ConstantProductPool.sol.

The velocore__execute() function in ConstantProductPool is responsible for calculating swap outcomes and is invoked by the SwapFacet contract whenever a user initiates a swap on Velocore. However, this function contains several flaws:

  1. Missing Caller Validation: velocore__execute() should only be called from Vault contracts (such as SwapFacet in this case), but this validation is missing. This allows anyone to call the function, introducing a potential attack vector.

  2. No Upper Bound on feeMultiplier: There is no upper bound check for the feeMultiplier. The logic shows that the feeMultiplier increases each time velocore__execute() is called and only resets to 1e9 when the block timestamp changes (the third code block). Since feeMultiplier is used to calculate the transaction fee (effectiveFee1e9 in the first code block), when feeMultiplier exceeds 3.33e11, effectiveFee1e9 will be at least 3.33e11 * 3e6 / 1e9 = 1e9, which means the fee exceeds 100%. This significantly impacts unaccountedFeeAsGrowth1e18, leading to changes in requestedGrowth1e18 and ultimately affecting the pool's balances.

  3. Unchecked Arithmetic: The calculation of unaccountedFeeAsGrowth1e18 is placed inside an unchecked block (the second code block). Combined with the second flaw, the expression 1e18 - ((1e18 - k) * effectiveFee1e9) / 1e9 can underflow, leading to unexpected behaviors.

With all the given information, we can now understand the hacker's strategy:

  1. Manipulating feeMultiplier: The hacker directly invoked velocore__execute() multiple times to artificially raise the feeMultiplier, causing effectiveFee1e9 to exceed 100%. This is required to execute the integer overflow attack in the following steps. In the actual attack, velocore__execute() was called three times with the same arguments. Through testing, we achieved the same effect by calling velocore__execute() only once with slightly different parameters. It's important to note that this doesn't immediately affect pool balances since velocore__execute() performs no transfers; it only updates the feeMultiplier.

  2. Draining USDC Liquidity: By leveraging the flash loan feature, the hacker attempted to use LP tokens to withdraw nearly all the USDC from the pool, creating a scarcity of USDC and significantly impacting the swap price. In the actual attack, the hacker executed three operations to drain 98% of the pool's USDC each time. During our testing, we found that we could achieve the same result in a single operation, with no differences in outcome.

  3. Exploiting Integer Underflow: Finally, the hacker performed another single-token withdrawal, choosing a precise amount of USDC that triggered an integer underflow, causing rpow() to return an abnormally large value. This led the pool to miscalculate, awarding the hacker LP tokens instead of executing a proper withdrawal. This allows the hacker to have enough LP tokens to repay the amount they borrowed in the previous steps.

Conclusion

Although Velocore has passed multiple audits, it still contains several issues that can be exploited in combination to create a large-scale attack. The root cause of these vulnerabilities seems to stem from an improper system design. The SwapFacet contract relies on ConstantProductPool's logic to update the pool's balances and perform token transfers. However, ConstantProductPool calculates deltas using its own internal variables (independent of SwapFacet) and only returns the results. This design creates a disconnect between the contracts, leading to miscommunication and, ultimately, the exploit. To mitigate future risks, we recommend Velocore refactor its codebase to establish clear and robust logic between internal components.

Thanks for reading Verichains! Subscribe for free to receive new posts and support my work.

Source
Disclaimer: The content above is only the author's opinion which does not represent any position of Followin, and is not intended as, and shall not be understood or construed as, investment advice from Followin.
Like
Add to Favorites
Comments