Advanced
MEV Mitigation — Project — MEV-aware swap intent forwarder
Avoid getting exploited at the mempool layer.
Developer Advanced
7/8 — MEV Mitigation — Project — MEV-aware swap intent forwarder
What to writeA signed intent forwarder that enforces slippage/deadlines, prevents replays, and only lets a trusted relayer submit swaps.
Cryptography is about how to maintain control over information.
- State & domain
Router, relayer, nonces, and EIP-712 domain for intents. - Intent hash
Hash user intent (tokens, amounts, minOut, deadline, nonce, relayer). - Execute
Only relayer calls; verify signature, nonce, deadlines; perform swap and enforce minOut. - Admin
Rotate relayer; expose events for monitoring.
Start with state, domain separator, and nonce tracking.
contract MEVProtectedSwap {
using ECDSA for bytes32;
IRouter public immutable router;
address public relayer;
mapping(address => uint256) public nonces;
bytes32 public constant INTENT_TYPEHASH = keccak256(
"SwapIntent(address user,address tokenIn,address tokenOut,uint256 amountIn,uint256 minOut,uint256 deadline,uint256 nonce,address relayer)"
);
bytes32 public immutable DOMAIN_SEPARATOR;
event IntentExecuted(address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, uint256 nonce, bytes32 digest);
event RelayerUpdated(address indexed oldRelayer, address indexed newRelayer);
constructor(address _router, address _relayer) {
router = IRouter(_router);
relayer = _relayer;
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("MEVProtectedSwap")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
Hash and verify intents (user signs; relayer submits).
struct SwapIntent {
address user;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minOut;
uint256 deadline;
uint256 nonce;
address relayer;
}
function _hashIntent(SwapIntent memory i) internal view returns (bytes32) {
return keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
INTENT_TYPEHASH,
i.user,
i.tokenIn,
i.tokenOut,
i.amountIn,
i.minOut,
i.deadline,
i.nonce,
i.relayer
)
)
)
);
}
Execute swap: only relayer submits; signature, nonce, deadlines, and minOut enforced.
function executeIntent(SwapIntent calldata i, bytes calldata sig, address[] calldata path) external returns (uint256 out) {
require(msg.sender == relayer && i.relayer == relayer, "relayer");
require(block.timestamp <= i.deadline, "expired");
require(i.nonce == nonces[i.user], "bad nonce");
bytes32 digest = _hashIntent(i);
address signer = digest.recover(sig);
require(signer == i.user, "bad sig");
nonces[i.user] = i.nonce + 1;
IERC20(i.tokenIn).transferFrom(i.user, address(this), i.amountIn);
IERC20(i.tokenIn).approve(address(router), i.amountIn);
uint256[] memory amounts = router.swapExactTokensForTokens(i.amountIn, i.minOut, path, i.user, i.deadline);
out = amounts[amounts.length - 1];
require(out >= i.minOut, "slippage");
emit IntentExecuted(i.user, i.tokenIn, i.tokenOut, i.amountIn, out, i.nonce, digest);
}
Keep rotation simple so you can swap relayers if they fail or get censored.
function setRelayer(address r) external {
require(msg.sender == relayer, "only relayer");
emit RelayerUpdated(relayer, r);
relayer = r;
}
Full reference (interfaces + contract) for copy/paste:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
interface IRouter {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
}
contract MEVProtectedSwap {
using ECDSA for bytes32;
IRouter public immutable router;
address public relayer;
mapping(address => uint256) public nonces;
bytes32 public constant INTENT_TYPEHASH = keccak256(
"SwapIntent(address user,address tokenIn,address tokenOut,uint256 amountIn,uint256 minOut,uint256 deadline,uint256 nonce,address relayer)"
);
bytes32 public immutable DOMAIN_SEPARATOR;
event IntentExecuted(address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, uint256 nonce, bytes32 digest);
event RelayerUpdated(address indexed oldRelayer, address indexed newRelayer);
constructor(address _router, address _relayer) {
router = IRouter(_router);
relayer = _relayer;
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("MEVProtectedSwap")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
struct SwapIntent {
address user;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minOut;
uint256 deadline;
uint256 nonce;
address relayer;
}
function _hashIntent(SwapIntent memory i) internal view returns (bytes32) {
return keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
INTENT_TYPEHASH,
i.user,
i.tokenIn,
i.tokenOut,
i.amountIn,
i.minOut,
i.deadline,
i.nonce,
i.relayer
)
)
)
);
}
function executeIntent(SwapIntent calldata i, bytes calldata sig, address[] calldata path) external returns (uint256 out) {
require(msg.sender == relayer && i.relayer == relayer, "relayer");
require(block.timestamp <= i.deadline, "expired");
require(i.nonce == nonces[i.user], "bad nonce");
bytes32 digest = _hashIntent(i);
address signer = digest.recover(sig);
require(signer == i.user, "bad sig");
nonces[i.user] = i.nonce + 1;
IERC20(i.tokenIn).transferFrom(i.user, address(this), i.amountIn);
IERC20(i.tokenIn).approve(address(router), i.amountIn);
uint256[] memory amounts = router.swapExactTokensForTokens(i.amountIn, i.minOut, path, i.user, i.deadline);
out = amounts[amounts.length - 1];
require(out >= i.minOut, "slippage");
emit IntentExecuted(i.user, i.tokenIn, i.tokenOut, i.amountIn, out, i.nonce, digest);
}
function setRelayer(address r) external {
require(msg.sender == relayer, "only relayer");
emit RelayerUpdated(relayer, r);
relayer = r;
}
}
Try itTests: bad sig/nonce/relayer reverts; expired deadline reverts; minOut enforced; relayer rotation works; private submission hides tx from mempool.