Fee-recipient reentrancy
Description
After a successful token deployment to a liquidity pool, the functions quoteToken
, swapTokenForETH
, and swapETHForToken
prevent swapping or quoting the token with the contract with the following check:
if (token.dexPair != address(0)) revert TokenAlreadyDeployed();
During the deployment of the token to a liquidity pool, the fee recipient is paid with the _safeTransferETH
function before the variable token.dexPair
is set:
function _deployTokenLiquidity(address _address) internal {
//...
IERC20(token.token).approve(address(router), tokenAmount);
(uint256 tokenAddedAmount, uint256 ethAddedAmount, uint256 liquidity) = router.addLiquidityETH{ value: quotedEthAmount }(
token.token,
false,
quotedTokenAmount,
quotedTokenAmount,
quotedEthAmount,
address(this),
block.timestamp + 10
);
if (liquidity == 0) revert LiquidityDeployFailed();
if (tokenAddedAmount < tokenAmount) {
_safeTransferToken(token.token, $.config.feeRecipient, tokenAmount - tokenAddedAmount);
}
if (ethAddedAmount < ethAmount) {
_safeTransferETH($.config.feeRecipient, ethAmount - ethAddedAmount);
}
token.dexPair = router.pairFor(address(weth), token.token, false);
token.bondingCurve = BondingCurve({ vTokenReserves: 0, vETHReserves: 0, rTokenReserves: 0, rETHReserves: 0 });
emit TokenLiquidityDeploy(token.token, token.dexPair);
}
Note that this ETH transfer happens before the dexPair
is set and bondingCurve
is zeroed, which means that if the fee recipient reenters the contract during the transfer, they will still be able to trade on the invalid bonding curve. If they sell tokens at this time, the tokens will become stuck in the contract, and the ETH sent out will be double-counted, making the contract insolvent.
Impact
Since the fee recipient receives all the fees, it does not cost them anything to attempt this attack.
So, by transactionally deploying a new token and then buying enough of it with ETH such that it immediately is deployed to the DEX, and then doing this reentrancy attack, the fee recipient can freely exfiltrate as much ETH from the native balance of the contract as they like.
Recommendations
The contract should implement the checks-effects-interactions pattern and set the variables token.dexPair
and token.bondingCurve
before transferring ETH to the feeRecipient
.
Remediation
This issue has been acknowledged by PondFun, and a fix was implemented in commit b0721f46↗.