# Solidity Mev Slippage Frontrunning

> Audit decentralized exchange integrations and automated trading logic for missing slippage parameters (amountOutMin = 0), deadline = block.timestamp antipatterns, public mempool sandwich susceptibility, and router validation bugs.

- Skill: `netvar1337/solidity-mev-slippage-frontrunning` (Agent Skill)
- Install (CLI): `npx skillmds@latest add netvar1337/solidity-mev-slippage-frontrunning`
- Raw SKILL.md: https://api.skillmd.com/api/skills/netvar1337/solidity-mev-slippage-frontrunning/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: netvar1337 (https://skillmd.com/u/netvar1337)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/netvar1337/solidity-mev-slippage-frontrunning

---


# Solidity MEV, AMM Slippage Control & Sandwich Attack Vulnerabilities

## Overview & Empirical Grounding
This security auditing skill is synthesized from **364 empirical audit contest findings** (45 Critical, 319 High severity) extracted from the `Zaevlad/audit-findings-dataset` corpus.

Auditors and security reviewers must use this methodology when reviewing smart contracts that exhibit **MEV & AMM Integration** patterns.

### Key Trigger Keywords
`slippage protection`, `sandwich attack`, `mev frontrunning`, `amountoutmin`, `deadline block.timestamp`, `uniswap swap`, `curve exchange`

---

## Threat Model & Core Attack Vectors

Detailed failure modes identified across empirical audit findings:
1. **Unchecked Assumptions & Protocol State Discrepancy**: Deviations between internal accounting state and actual balances/external conditions.
2. **Missing Boundary Checks & Validation**: Failing to constrain user inputs, return values, or stale external data feeds.
3. **Execution Ordering & Invariant Breakage**: Invariants momentarily violated during external calls or intermediary calculations.
4. **Economic & MEV Incentives**: Opportunities for adversaries to extract value through arbitrage, flash loans, or sandwiching.

---

## Step-by-Step Audit Checklist

When reviewing contracts in this domain, follow this rigorous step-by-step verification pipeline:

### 1. Architectural & Entry Point Reconnaissance
- Map all external and public entry points that interact with the vulnerable component.
- Verify access control modifiers (`onlyOwner`, `onlyRole`, custom guards) and ensure roles are initialized in constructor/initializer.
- Trace all token balance state changes and external contract interactions.

### 2. Deep Invariant & Boundary Verification
- **Input Validation**: Check zero-address (`address(0)`), zero-amount, array length parity, and upper/lower bounds.
- **State Transition Sequencing**: Verify Checks-Effects-Interactions (CEI) order. All storage updates must precede external invocations.
- **External Return Handling**: Check return values for external ERC-20 transfers, oracle queries, and delegatecalls.
- **Failure Paths & DoS Hazards**: Ensure reverts in third-party calls do not lock protocol funds or halt liquidation/withdrawal queues.

### 3. Proof of Concept (PoC) Development Guide
When drafting a reproducible exploit in Foundry (`forge test`):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";

contract AuditPoCTest is Test {
    // Setup target protocol and mock dependencies
    function setUp() public {
        // Fork mainnet or deploy local environment
    }

    function test_exploit_vector() public {
        // 1. Establish initial attacker balance
        // 2. Trigger vulnerability
        // 3. Assert invariant violation or balance drain
    }
}
```

---

## Empirical Case Studies from Zaevlad/audit-findings-dataset

The following real-world contest findings illustrate the root causes and remediation patterns:

### Case Study: Pools configured with a malicious hook can bypass the `BunniHub` re-entrancy guard to drain all raw balances and vault reserves of legitimate pools
- **Severity**: High (Dataset Weight: 1.00, ID: #25)
- **Vulnerability Mechanism**:
** The `BunniHub` is the main contract that both holds and accounts the raw balances and  vault reserves that each pool has deposited. It inherits `ReentrancyGuard` which implements the `nonReentrant` modifier:

```solidity
modifier nonReentrant() {
    _nonReentrantBefore();
    _;
    _nonReentrantAfter();
}

function _nonReentrantBefore() internal {
    uint256 statusSlot = STATUS_SLOT;
    uint256 status;
    /// @solidity memory-safe-assembly
    assembly {
        status := tload(statusSlot)
    }
    if (status == ENTERED) revert ReentrancyGuard__ReentrantCall();

    uint256 entered = ENTERED;
    /// @solidity memory-safe-assembly
    assembly {
        tstore(statusSlot, entered)
    }
}

function _nonReentrantAfter() internal {
    uint256 statusSlot = STATUS_SLOT;
    uint256 notEntered = NOT_ENTERED;
    /// @solidity memory-safe-assembly
    assembly {
        tstore(statusSlot, notEntered)
    }
}
```

During the rebalance of a Bunni pool, it is intended to separate this re-entrancy logic into before/after hooks. These functions re-use the same global re-entrancy guard transient storage slot to lock the `BunniHub` against any potential re-entrant execution by a malic

#### Proof of Concept / Vulnerable Pattern
```solidity
** To run the following PoCs:
* Add the following malicious vault implementation inside `test/mocks/ERC4626Mock.sol`:

```solidity
interface MaliciousHook {
    function continueAttackFromMaliciousVault() external;
}

contract MaliciousERC4626 is ERC4626 {
    address internal immutable _asset;
    MaliciousHook internal immutable maliciousHook;
    bool internal attackStarted;

    constructor(IERC20 asset_, address _maliciousHook) {
        _asset = address(asset_);
        maliciousHook = MaliciousHook(_maliciousHook);
    }

    function asset() public view override returns (address) {
        return _asset;
    }

    function name() public pure override returns (string memory) {
        return "MockERC4626";
    }

    function symbol() public pure override returns (string memory) {
        return "MOCK-ERC4626";
    }

    function setupAttack() external {
        attackStarted = true;
    }

    function previewRedeem(uint256 shares) public view override returns (uint256 assets) {
        return type(uint128).max;
    }

    function withdraw(uint256 assets, address to, address owner) public override returns(uint256 shares){
        if(attackStarted) {
            malicious
```

- **Remediation Recommendation**:
** The root cause of the attack was the ability to disable the intended global re-entrancy protection by invoking `unlockForRebalance()` which was subsequently disabled. One solution would be to implement the rebalance re-entrancy protection on a per-pool basis such that one pool cannot manipulate the re-entrancy guard transient storage state of another. Additionally, allowing Bunni pools to be deployed with any arbitrary hook implementation greatly increases the attack surface. It is instead recommended to minimise this by constraining the hook to be the canonical `BunniHook` implementation such that it is not possible to call such `BunniHub` functions directly.

---
### Case Study: Successful transactions are not stored, causing a replay attack on ``redeemDepositsAndInternalBalances``
- **Severity**: High (Dataset Weight: 1.00, ID: #3539)
- **Vulnerability Mechanism**:
in redeemDepositsAndInternalBalances there is no validation about the parameters that have been used which should be stored and should not be reused.

As a result, parameters that have already been used can be reused.

Look at this:

```solidity
function redeemDepositsAndInternalBalances(
        address owner,
        address reciever,
        AccountDepositData[] calldata deposits,
        AccountInternalBalance[] calldata internalBalances,
        uint256 ownerRoots,
        bytes32[] calldata proof,
        uint256 deadline,
        bytes calldata signature
    ) external payable fundsSafu noSupplyChange nonReentrant {
        // verify deposits are valid.
        // note: if the number of contracts that own deposits is small,
        // deposits can be stored in bytecode rather than relying on a merkle tree.
        verifyDepositsAndInternalBalances(owner, deposits, internalBalances, ownerRoots, proof);

        // signature verification.
        verifySignature(owner, reciever, deadline, signature)
```

```solidity
function verifyDepositsAndInternalBalances(
        address account,
        AccountDepositData[] calldata deposits,
        AccountInternalBalance[] calldata inte

#### Proof of Concept / Vulnerable Pattern
```solidity
Due to difficulty finding the same MERKLE_ROOT value as the L2ContractMigrationFacet contract.

So, this PoC only proved a simple signature replay of L2ContractMigrationFacet.

Then the bug in verifyDepositsAndInternalBalances can be proven by yourself with a replay attack scheme, since only you know the "leaves" of MERKLE_ROOT in the L2ContractMigrationFacet contract.
paste this code on dir/test of new foundry project
don't forget to install dependencies such as forge-std and openzeppelin
run with forge test -vvvv

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import "../src/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract PocTest is Test {
    using ECDSA for bytes32;

    L2ContractMigrationFacetsimple L2CMFOp;

    uint256 internal signerPrivateKey;

    uint256 optimismFork;

    bytes32 private constant REDEEMDEPOSITTYPE_HASH =
        keccak256(
            "redeemDepositsAndInternalBalances(address owner,address reciever,uint256 deadline)"
        );

    function setUp() public {
        optimismFork = vm.createSelectFork("https://rpc.ankr.com/opti
```

- **Remediation Recommendation**:
```solidity
    mapping(bytes32 => bool) public isRedeemed;

    function verifyDepositsAndInternalBalances(
        address account,
        AccountDepositData[] calldata deposits,
        AccountInternalBalance[] calldata internalBalances,
        uint256 ownerRoots,
        bytes32[] calldata proof
    ) internal {
        bytes32 leaf = keccak256(abi.encode(account, deposits, internalBalances, ownerRoots));
        if (isRedeemed[leaf]) revert REDEEMED_ALREADY();
        require(MerkleProof.verify(proof, MERKLE_ROOT, leaf), "Migration: invalid proof");
        isRedeemed[leaf] = true;
    }
```

And also do that in verifySignature ensure that can't be front running attack, like add access control or nonces

---
### Case Study: A malicious user can prevent vote creation at almost no cost Submitted by b0g0
- **Severity**: High (Dataset Weight: 1.00, ID: #4726)
- **Vulnerability Mechanism**:
A staker in the GOAT protocol can raise a reputation challenge against any other staker. This happens by creating a vote, where the challenger (attacker) and the challenged (defender) are the 2 sides. The attacker locks wstETH in the Voting.sol contract proportional to the amount of the defender earnings he wants to freeze.

Vote creation happens through the Controller.createVote() function:
```solidity
function createVote(
    address defender_,
    uint dEthValue_,
    uint voterPercent_,
    uint freezeDuration_,
    uint minWstethA_,
    uint wstethA_
)
external
payable
{
    require(_isPausedAttack == 0, "paused");
    address attacker = msg.sender;
    // _weth.deposit{value: address(this).balance}();
    _prepareWsteth(minWstethA_, wstethA_);
    uint aEthValue = _geth.balanceOf(address(this));
    require(defender_ != address(_devTeam));
    require(dEthValue_ >= _minDefenderFund, "dEthValue_ too small");
    require(voterPercent_ <= _maxVoterPercent, "voterPercent_ too high");
    require(freezeDuration_ >= _minFreezeDuration && freezeDuration_ <= _maxFreezeDuration, "freezeDuration_ invalid");
    require(aEthValue <= dEthValue_ && aEthValue * LPercentage.DEMI / dEthValue

#### Proof of Concept / Vulnerable Pattern
```solidity
Since the protocol has no tests, I created a Foundry Project and wrote fork tests using the contracts deployed on Arbitrum Sepolia. The used Sepolia RPC provider is a demo one (I actually used Alchemy).
```solidity
import "forge-std/Test.sol";
import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;
interface IController {
    function ethStake(
        address payable poolOwner_,
        uint duration_,
        uint minSPercent_,
        uint poolConfigCode_,
        uint minWstethA_,
        uint wstethA_
    ) external payable;
    function dctStake(
        uint amount_,
        address payable poolOwner_,
        uint duration_
    ) external payable;
    function earningPulls(
        address account_,
        address[] memory poolOwners_,
        address bountyPullerTo_
    ) external;
    function lockWithdraw(
        bool isEth_,
        address payable poolOwner_,
        uint amount_,
        address payable dest_,
        bool isForced_,
        uint minEthA_
    ) external;
    function earningReinvest(
        bool isEth_,
        address payable poolOwner_,
        uint duration_,
 
```

- **Remediation Recommendation**:
* Make aEthValue a parameter that is passed by the user and use it in the exploited check:
```solidity
function createVote(
    address defender_,
    uint dEthValue_,
    uint aEthValue, // <----- provided by user
    uint voterPercent_,
    uint freezeDuration_,
    uint minWstethA_,
    uint wstethA_
) external payable {
    // ...
    // this cannot be influenced by direct transfers
    require(aEthValue <= dEthValue_ && aEthValue * LPercentage.DEMI / dEthValue_ >= _minAttackerFundRate, "aEthValue invalid");
}
```
The transferFrom will fail in case the user did not provided the appropriate amount.

---
### Case Study: AerodromeCLGaugeContractGuardallows the man-
- **Severity**: High (Dataset Weight: 1.00, ID: #22936)
- **Vulnerability Mechanism**:
The lack of checks in the functions deposit and withdraw within the AerodromeCLGaugeContractGuard allows the manager to drain the Vault by withdrawing liquidity from Velodrome and receiving unsupported tokens, which the manager can later steal. When a Vault wants to interact with a Velodrome gauge, a contract guard ensures that only certain functions can be called with the correct arguments. However, the lack of checks in the guard for the functions deposit and withdraw allows the manager to drain the Vault by following this attack sequence:
1. The manager uses the Vault to mint a liquidity NFT from a CLPool (e.g. WETH-USDC) through NonFungiblePositionManager::mint.
2. The manager uses the Vault to decrease liquidity from that NFT through NonFungiblePositionManager::decreaseLiquidity.
3. The manager removes the assets USDC and WETH from being supported in the Vault.
4. The manager uses the Vault to deposit that NFT into the gauge through CLGauge::deposit.
After executing step 4, the Vault will receive the liquidity that was removed in step 2, but those tokens won't be accounted for in the Vault's total value because the USDC and WETH tokens have been removed from the supported asse

#### Proof of Concept / Vulnerable Pattern
```solidity
The following PoC is a test that forks the Optimism blockchain and executes the attack on a Vault. The test can be pasted in any Foundry environment and can be run with the command forge test --match-test test_gauge. Additionally, you must have the following lines in the .env file in order for the test to fork the blockchain:
OPTIMISM_RPC_URL=https://opt-mainnet.g.alchemy.com/v2/{key}
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
```solidity
pragma solidity 0.8.13;
import {Test} from "forge-std/Test.sol";
interface IVelodromeNonfungiblePositionManager {
    struct DecreaseLiquidityParams {
        uint256 tokenId;
        uint128 liquidity;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }
    function ownerOf(uint256 tokenId) external view returns (address);
    function positions(uint256 tokenId) external view returns (uint96, address, address, address, int24, int24, int24, uint128, uint256, uint256, uint128, uint128);
    function decreaseLiquidity(DecreaseLiquidityParams calldata params) external;
    function approve(address to, uint256 tokenId) external;
}
interface ICLGauge {
    function deposit(uint256 tokenId) external;
}
interfa
```

- **Remediation Recommendation**:
To mitigate this issue is recommended to ensure that both tokens from the liquidity pool are supported by the Vault before calling deposit and withdraw.
```solidity
if (method == IVelodromeCLGauge.deposit.selector) {
    uint256 tokenId = abi.decode(params, (uint256));
    _validateTokenId(nonfungiblePositionManagerGuard, tokenId, poolLogic);
    (, , address token0, address token1, , , , , , , , ) = IVelodromeNonfungiblePositionManager(nonfungiblePositionManager).positions(tokenId);
    require(IHasSupportedAsset(poolManagerLogic).isSupportedAsset(token0));
    require(IHasSupportedAsset(poolManagerLogic).isSupportedAsset(token1));
    txType = uint16(TransactionType.VelodromeCLStake);
} else if (method == IVelodromeCLGauge.withdraw.selector) {
    uint256 tokenId = abi.decode(params, (ui

---
### Case Study: An attacker can drain the entire protocol balance of sUSDe during the yield phase due to incorrect redemption accounting logic in pUSDeVault::_withdraw
- **Severity**: High (Dataset Weight: 1.00, ID: #23521)
- **Vulnerability Mechanism**:
After transitioning to the yield phase, the entire protocol balance of USDe is deposited into sUSDe and pUSDe can be deposited into the yUSDe vault to earn additional yield from the sUSDe. When initiating a redemption, yUSDeVault::_withdraw is called which in turn invokes pUSDeVault::redeem:

```solidity
function _withdraw(address caller, address receiver, address owner, uint256 pUSDeAssets, uint256
shares) internal override {,!
    if (!withdrawalsEnabled) {
        revert WithdrawalsDisabled();
    }
    if (caller != owner) {
        _spendAllowance(owner, caller, shares);
    }
    _burn(owner, shares);
    @> pUSDeVault.redeem(pUSDeAssets, receiver, address(this));
    emit Withdraw(caller, receiver, owner, pUSDeAssets, shares);
}
```

This is intended to have the overall effect of atomically redeeming yUSDe -> pUSDe -> sUSDe by previewing and applying any necessary yield from sUSDe:

```solidity
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
    internal override {,!
    if (PreDepositPhase.YieldPhase == currentPhase) {
        // sUSDeAssets = sUSDeAssets + user_yield_sUSDe
        @> assets += previewYield(caller, shares)

#### Proof of Concept / Vulnerable Pattern
```solidity
```solidity
pragma solidity 0.8.28;
import {Test} from "forge-std/Test.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {MockUSDe} from "../contracts/test/MockUSDe.sol";
import {MockStakedUSDe} from "../contracts/test/MockStakedUSDe.sol";
import {MockERC4626} from "../contracts/test/MockERC4626.sol";
import {pUSDeVault} from "../contracts/predeposit/pUSDeVault.sol";
import {yUSDeVault} from "../contracts/predeposit/yUSDeVault.sol";
import {console2} from "forge-std/console2.sol";

contract CritTest is Test {
    uint256 constant MIN_SHARES = 0.1 ether;
    MockUSDe public USDe;
    MockStakedUSDe public sUSDe;
    pUSDeVault public pUSDe;
    yUSDeVault public yUSDe;
    address account;
    address alice = makeAddr("alice");
    address bob = makeAddr("bob");

    function setUp() public {
        address owner = msg.sender;
        // Prepare Ethena and Ethreal contracts
        USDe = new MockUSDe();
        sUSDe = new MockStakedUSDe(USDe, owner, owner);
        // Prepare pUSDe and Depos
```

- **Remediation Recommendation**:
Recommended Mitigation: While the assets corresponding to the accrued yield should be included when previewing the sUSDe withdrawal, only the base assets should be passed to the subsequent call to `_withdraw()`:

```solidity
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
    internal override {,!
    if (PreDepositPhase.YieldPhase == currentPhase) {
        // sUSDeAssets = sUSDeAssets + user_yield_sUSDe
        -- assets += previewYield(caller, shares);
        ++ uint256 assetsPlusYield = assets + previewYield(caller, shares);
        -- uint sUSDeAssets = sUSDe.previewWithdraw(assets);
        ++ uint sUSDeAssets = sUSDe.previewWithdraw(assetsPlusYield);
        _withdraw(
            address(sUSDe),
            caller,
            re

---
### Case Study: Incorrect Execution Fee Refund address on Failed Deposits or withdrawals in Strategy Vaults
- **Severity**: High (Dataset Weight: 0.98, ID: #15547)
- **Vulnerability Mechanism**:
The Strategy Vaults within the protocol use a two-step process for handling deposits/withdrawals via GMXv2. A createDeposit() transaction is followed by a callback function (afterDepositExecution() or afterDepositCancellation()) based on the transaction's success. In the event of a failed deposit due to vault health checks, the execution fee refund is mistakenly sent to the depositor instead of the keeper who triggers the deposit failure process.

The protocol handles the deposit through the deposit function, which uses several parameters including an execution fee that refunds any excess fees. 

```solidity
function deposit(GMXTypes.DepositParams memory dp) external payable nonReentrant {
        GMXDeposit.deposit(_store, dp, false);
    }

struct DepositParams {
    // Address of token depositing; can be tokenA, tokenB or lpToken
    address token;
    // Amount of token to deposit in token decimals
    uint256 amt;
    // Minimum amount of shares to receive in 1e18
    uint256 minSharesAmt;
    // Slippage tolerance for adding liquidity; e.g. 3 = 0.03%
    uint256 slippage;
    // Execution fee sent to GMX for adding liquidity
    uint256 executionFee;
  }
```

The refund is in

- **Remediation Recommendation**:
The processDepositFailure  and processWithdrawFailure functions must be modified to update self.refundee to the current executor of the function, which, in the case of deposit or withdraw failure, is the keeper.

```solidity
function processDepositFailure(GMXTypes.Store storage self, uint256 slippage, uint256 executionFee) external {
        GMXChecks.beforeProcessAfterDepositFailureChecks(self);

        GMXTypes.RemoveLiquidityParams memory _rlp;

		self.refundee = payable(msg.sender);

		...
        }
```

```solidity
function processWithdrawFailure(
    GMXTypes.Store storage self,
    uint256 slippage,
    uint256 executionFee
  ) external {
    GMXChecks.beforeProcessAfterWithdrawFailureChecks(self);

	self.refundee = payable(msg.sender);

	...
  }
```


---

## Remediation & Hardening Principles

- **Fail-Safe Defaults**: Always favor reverting rather than proceeding with invalid or unexpected state.
- **Defensive Type Safety**: Use OpenZeppelin's vetted libraries (`SafeERC20`, `ReentrancyGuardUpgradeable`, `SafeCast`, `Math`).
- **Two-Step Critical Actions**: Enforce commit-reveal or timelock mechanisms for governance and high-impact parameter changes.
- **Comprehensive Testing Matrix**: Pair unit tests with property-based invariant testing (Foundry `invariant` or Echidna) and mutation testing.

