F-2024-0013·permit-frontrunning

Permit doesn't follow OpenZeppelin's recommendation to handle front runs

Acknowledgeddexammraffle
TL;DR

removeLiquidityWithPermit and removeLiquidityNativeWithPermit call permit without try/catch, so a front-run permit causes the entire transaction to revert. OpenZeppelin recommends a try/catch wrapper with a fallback allowance check.

Severity
LOW
Impact
LOW
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
LOW
02Section · Description

Description

The removeLiquidityWithPermit and removeLiquidityNativeWithPermit functions in the contract use the permit function of ERC20 tokens without proper error handling. According to OpenZeppelin's latest recommendations, permit calls should be wrapped in a try/catch block to handle potential failures gracefully.

OpenZeppelin's security considerations explain that valid permit signatures are an intent to spend an allowance and have built-in replay protection but can be submitted by anyone, making them vulnerable to frontrunning. The recommended pattern is to wrap the call in a try/catch and tolerate prior execution.

Current implementation:

solidity
IERC20Permit(pool).permit(msg.sender, address(this), value, _params.deadline, _params.v, _params.r, _params.s);

This implementation doesn't account for potential failures of the permit function, which could lead to transaction reverts in cases where the permit has already been used or in front-running scenarios.

03Section · Impact

Impact

The current implementation may cause unnecessary transaction failures in the following scenarios:

  1. Front-running: If a permit is front-run, the subsequent transaction will fail entirely.
  2. Replay: If a user accidentally tries to use the same permit twice, the second transaction will fail.
  3. Smart Contract Wallets: Some smart contract wallets cannot produce permit signatures, potentially limiting functionality for these users.

These issues do not pose a direct security threat but may lead to a suboptimal user experience and potential gas waste for failed transactions.

04Section · Recommendation

Recommendation

Implement a try/catch block around the permit call and add a fallback allowance check. This approach allows the function to proceed if the permit has already been executed or fails for other reasons, as long as the necessary allowance is in place.

Update the removeLiquidityWithPermit and removeLiquidityNativeWithPermit functions to use the following pattern:

solidity
function removeLiquidityWithPermit(
MonadexV1Types.RemoveLiquidityWithPermit calldata _params
)
external
beforeDeadline(_params.deadline)
returns (uint256, uint256)
{
address pool = MonadexV1Library.getPool(i_factory, _params.tokenA, _params.tokenB);
uint256 value = _params.approveMax ? type(uint256).max : _params.lpTokensToBurn;
try IERC20Permit(pool).permit(
msg.sender, address(this), value, _params.deadline, _params.v, _params.r, _params.s
) {
// Permit executed successfully, proceed
} catch {
// Check allowance to see if permit was already executed
uint256 allowance = IERC20(pool).allowance(msg.sender, address(this));
if (allowance < value) {
revert PermitFailed();
}
}
return removeLiquidity(
_params.tokenA,
_params.tokenB,
_params.lpTokensToBurn,
_params.amountAMin,
_params.amountBMin,
_params.receiver,
_params.deadline
);
}

And apply the same pattern to removeLiquidityNativeWithPermit.

F-2024-0013

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx