Predy Finance Attack - How a Liquidity Pool Can Be Drained
The PredyPool contract on Arbitrum, part of the Predy Finance project, was attacked on May 14, 2024. In this attack, the attacker exploited a vulnerability in the PredyPool contract to transfer the liquidity from existing pairs into a newly created pair, successfully draining the pool. Let's dive in to discover how this attack was carried out.
Overview
Attacker address:
https://arbiscan.io/address/0x76b02ab483482740248e2ab38b5a879a31c6d008
Attack transaction: https://arbiscan.io/tx/0xbe163f651d23f0c9e4d4a443c0cc163134a31a1c2761b60188adcfd33178f50f
Vulnerable contract (PredyPool): https://arbiscan.io/address/0x9215748657319b17fecb2b5d086a3147bfbc8613
Exploit analysis
The PredyPool contract allows users to create their own pairs, which are registered on Uniswap V3. This verification process is conducted by calling the UniswapV3Factory contract. By owning a pair, the creator can whitelist addresses permitted to trade with it. This feature facilitates the opening or closing of perpetual future positions on the Predy platform. With the established pair, users can utilize the supply function to add tokens as liquidity or withdraw them by calling the withdraw function.
Before the attack, the PredyPool had two pairs (Bridged-USDC, WETH) and (USDC, WETH), with only whitelisted addresses permitted to trade with these pairs. However, the PredyPool contract does not prevent the creation of new pairs with the same tokens as the existing pairs. Therefore, to use the trade feature, which could trigger the exploit, the attacker created their own new pair of (USDC, WETH).
Examining the source code of the trade function, we can see that it will make a call to callTradeAfterCallback and subsequently trigger the predyTradeAfterCallback hook to the attacker's contract. The code responsible for protecting the contract’s assets from being drained after the trade call is PositionCalculator.checkSafe(), located at the bottom of the trade function.
In the callTradeAfterCallback function, we can see a call to initializeLock(), which sets the locker to the current caller.
Inside the callback hook, the caller can freely call the take function, which is protected by the onlyByLocker modifier, to borrow tokens from the pool.
Inside the callback hook, the attacker tried to move the liquidity from the existing (USDC, WETH) pair into his own new pair by making the following calls for each token:
take(): Takes all the tokens from the pool.supply(): Supplies the taken tokens as liquidity into his new pair.
We can notice that the supply function has the nonReentrant modifier to protect the pool from reentrancy attacks. However, the trade function does not have this modifier, so this reentrancy attack can be executed successfully. Finally, the attacker completed the exploit, moving all the liquidity of the (USDC, WETH) pair into his new pair. After the reentrancy attack and completion of the trade call, the attacker can drain all the assets of the pool by withdrawing the liquidity from his pair.
Current status
The drained funds of the PredyPool contract amount to approximately 219,585 USDC and 83.9 WETH. After the attack occurred, the Predy team upgraded the contract to pause the pool temporarily.
Conclusion
From this attack, we can summarize some major vulnerabilities within the PredyPool contract:
Improper use of
nonReentrantmodifiers, which failed to protect the contract from this incident.Sharing of the pair’s liquidity in a single contract, which can make all pairs vulnerable if a single pair is manipulated or attacked.
By sharing our thoughts on these past incidents, we hope that our web3 community can avoid these same attack vectors in future projects and make the DeFi ecosystem more secure.





