Uniswap V3 Integration
Comprehensive guide to securing Uniswap V3 interactions with Matador.
Automating liquidity provision or token swaps via smart accounts introduces significant risk. Matador allows you to define granular permission policies for Uniswap V3, ensuring that automated agents or delegated keys cannot drain funds or execute unfavorable trades.
Security Architecture
Permission Patterns
1. Basic Swap Protection
The most critical check is ensuring the recipient of the swap is the smart account itself. This prevents an attacker from using your funds to swap tokens into their own wallet.
import "abis/UniswapRouter.json" as Uniswap;
permission SafeSwap -> 1.0.0 {
parameters: {
authorizer: address,
router: address,
account: address
}
pub fn authorize() -> bool {
return authorizedBy(parameters.authorizer);
}
fn main() -> bool {
if (context.target != parameters.router) {
return false;
}
if (context.selector == Uniswap.exactInputSingle) {
return Uniswap.exactInputSingle.params.recipient == parameters.account;
}
return false;
}
}2. Token Whitelisting
Restrict the input and output tokens to a known list. This prevents "dusting" attacks or unauthorized trading of volatile assets.
import "abis/UniswapRouter.json" as Uniswap;
permission TokenWhitelist -> 1.0.0 {
parameters: {
authorizer: address,
router: address,
tokenIn: address,
tokenOut: address
}
pub fn authorize() -> bool {
return authorizedBy(parameters.authorizer);
}
fn main() -> bool {
if (context.target != parameters.router) {
return false;
}
if (context.selector == Uniswap.exactInputSingle) {
if (Uniswap.exactInputSingle.params.tokenIn != parameters.tokenIn) {
return false;
}
return Uniswap.exactInputSingle.params.tokenOut == parameters.tokenOut;
}
return false;
}
}3. Slippage Protection
Prevent malicious execution (e.g., sandwich attacks) by requiring a configured minimum output amount in calldata.
Calldata minimum output
This example does not read an oracle. It verifies that the swap calldata sets amountOutMinimum at or above the policy threshold.
import "abis/UniswapRouter.json" as Uniswap;
permission SlippageGuard -> 1.0.0 {
parameters: {
authorizer: address,
router: address,
minAmountOut: uint256
}
pub fn authorize() -> bool {
return authorizedBy(parameters.authorizer);
}
fn main() -> bool {
if (context.target != parameters.router) {
return false;
}
if (context.selector == Uniswap.exactInputSingle) {
return Uniswap.exactInputSingle.params.amountOutMinimum >= parameters.minAmountOut;
}
return false;
}
}Integration Tutorial
Setup Project
Create a new directory for your policies and download the Uniswap ABI.
mkdir my-defi-bot
cd my-defi-bot
npm install -D @steerprotocol/matador-cli
mkdir abis
# Download SwapRouter02.json to ./abis/Write the Policy
Create policies/swap.matador with the content below. This combines target checking, selector checking, and recipient validation.
import "abis/SwapRouter02.json" as Router;
permission SwapPolicy -> 1.0.0 {
parameters: {
router: address,
usdc: address,
weth: address,
account: address
}
fn main() -> bool {
if (context.target != parameters.router) {
return false;
}
if (context.selector == Router.exactInputSingle) {
if (Router.exactInputSingle.params.recipient != parameters.account) {
return false;
}
if (Router.exactInputSingle.params.tokenIn != parameters.usdc) {
return false;
}
return Router.exactInputSingle.params.tokenOut == parameters.weth;
}
return false;
}
}Compile
Compile the policy to generate the bytecode.
npx --package @steerprotocol/matador-cli matador-policy-cli compile policies/swap.matadorDeploy & Provision
Use a script to deploy the policy to your smart account.
const policy = require('./policies/swap.json');
// Encode parameters (Router, USDC, WETH addresses)
// In production, these are part of the 'context' if hardcoded,
// or passed as 'args' if the policy is generic.
// Matador compiler handles this mapping.
await account.grantPermission(policyId, policy.hexData);Gas Optimization
Uniswap structs are large. To minimize gas costs:
- Order Matters: Place cheap checks (like
context.targetorselector) before expensive calldata decoding or external calls. - Use read callables for queries: Keep externally callable
pub fnhelpers read-only and reserve state changes formain(). - Avoid Deep Nesting: Accessing
params.recipientrequires decoding the struct. If you only need to check the function selector, don't access the struct fields.
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
Execution Reverted | The policy condition evaluated to false. | Check transaction arguments against the policy rules. Ensure recipient is correct. |
Type Mismatch | Comparing tokenIn (address) to a number. | Ensure DSL types match the ABI definitions. |
Decoding Error | The transaction data does not match exactInputSingle. | Ensure the bot is calling the exact function defined in the policy. exactInput (multi-hop) has a different struct. |