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
Attacker's wallet: 0x8cdc37ed79c5ef116b9dc2a53cb86acaca3716bf
Attacker's contract: 0xb7f6354b2cfd3018b3261fbc63248a56a24ae91a
Attack transaction: 0xed11d5b013bf3296b1507da38b7bcb97845dd037d33d3d1b0c5e763889cdbed1
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.
A typical execution flow for a token swap operation is as follows:
The user calls
SwapFacet.execute()
with an array oftokenRef
and a list of operations (ops
).SwapFacet
then invokes its internal_execute()
function to process the user's operations._execute()
iterates throughops
and processes each operation based on its type.If the current operation is a swap request,
_execute()
calls the corresponding pool'svelocore__execute()
to simulate the swap and return the balance delta. It then invokes_verifyAndApplyDelta()
to verify and apply the results.
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:
Missing Caller Validation:
velocore__execute()
should only be called from Vault contracts (such asSwapFacet
in this case), but this validation is missing. This allows anyone to call the function, introducing a potential attack vector.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 to1e9
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 least3.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.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:
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 callingvelocore__execute()
only once with slightly different parameters. It's important to note that this doesn't immediately affect pool balances sincevelocore__execute()
performs no transfers; it only updates the feeMultiplier.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.
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.