Swap tokens on Uniswap V2/V3/V4 using Nethereum (.NET/C#). Use this skill whenever the user asks about token swaps, DEX trading, Uniswap integration, quoting prices, slippage protection, price impact, Universal Router, Permit2, or any DeFi swap operation with C# or .NET.
Nethereum provides a complete Uniswap integration covering V2, V3, and V4. The web3.UniswapV4() extension gives you access to quoting, routing, pool discovery, and swap execution — all through typed C# services. This is the standard way to execute on-chain token swaps from .NET.
NuGet: Nethereum.Uniswap
dotnet add package Nethereum.Uniswap
The UniswapV4() extension on Web3 exposes the full service hierarchy. It defaults to Mainnet addresses but accepts any network's addresses:
var web3 = new Web3(new Account(privateKey), rpcUrl);
var uniswap = web3.UniswapV4();
// Or for a specific network:
var uniswap = web3.UniswapV4(UniswapV4Addresses.BaseSepolia);
The uniswap object provides: Pools (state, cache, discovery), Pricing (quoter, price calculator, slippage, price impact), Positions (liquidity management), UniversalRouter (swap execution), and Math (tick math).
Before swapping, simulate the trade with the quoter — this is a read-only call, no gas cost:
var pathKeys = V4PathEncoder.EncodeMultihopExactInPath(new List<PoolKey> { pool }, eth);
var amountIn = Web3.Convert.ToWei(0.001);
var quoteParams = new QuoteExactParams
{
Path = pathKeys,
ExactAmount = amountIn,
ExactCurrency = eth
};
var quote = await uniswap.Pricing.Quoter.QuoteExactInputQueryAsync(quoteParams);
var quoteAmount = Web3.Convert.FromWei(quote.AmountOut, 6); // USDC = 6 decimals
The V4PathEncoder encodes the swap route. For single-hop, pass one pool key. For multi-hop (e.g., ETH→DAI→USDC), pass multiple pool keys.
Swaps go through the Universal Router. The pattern is: build actions (swap + settle input + take output), then execute:
var v4ActionBuilder = uniswap.GetUniversalRouterV4ActionsBuilder();
v4ActionBuilder.AddCommand(new SwapExactIn
{
CurrencyIn = eth,
AmountIn = amountIn,
AmountOutMinimum = quote.AmountOut * 95 / 100, // 5% slippage
Path = pathKeys.MapToActionV4()
});
v4ActionBuilder.AddCommand(new SettleAll { Currency = eth, Amount = amountIn });
v4ActionBuilder.AddCommand(new TakeAll { Currency = usdc, MinAmount = 0 });
var routerBuilder = new UniversalRouterBuilder();
routerBuilder.AddCommand(v4ActionBuilder.GetV4SwapCommand());
var executeFunction = routerBuilder.GetExecuteFunction(amountIn);
var receipt = await uniswap.UniversalRouter.ExecuteRequestAndWaitForReceiptAsync(executeFunction);
The three actions form the standard swap pattern: swap exchanges tokens in the pool, settle pays the input, take collects the output. AmountOutMinimum is your slippage protection.
Nethereum provides calculators for both:
var slippage = uniswap.Pricing.SlippageCalculator;
// For exact-input swaps: minimum acceptable output
var tolerance = new BigDecimal(0.5m); // 0.5%
var minAmountOut = slippage.CalculateMinimumAmountOut(expectedAmountOut, tolerance);
// For exact-output swaps: maximum you're willing to pay
var maxAmountIn = slippage.CalculateMaximumAmountIn(expectedAmountIn, tolerance);
// Price impact classification
var impactCalc = uniswap.Pricing.PriceImpactCalculator;
var impact = impactCalc.CalculatePriceImpact(input, output, midPrice);
var level = impactCalc.ClassifyPriceImpact(impact);
// Returns: Low (<1%), Medium (1-3%), High (3-5%), Critical (>5%)
var warning = impactCalc.GetPriceImpactWarning(level);
Calculate human-readable prices from the pool's raw sqrtPriceX96 value:
// Get price of token0 in terms of token1 (adjusted for decimals)
var priceToken0InToken1 = uniswap.Pricing.PriceCalculator.CalculatePriceFromSqrtPriceX96(
sqrtPriceX96, decimals0: 18, decimals1: 6);
// Inverse gives token1 price in terms of token0
var priceToken1InToken0 = priceToken0InToken1 == 0 ? 0 : 1 / priceToken0InToken1;
Tick math conversions for working with price ranges:
var sqrtPriceX96 = uniswap.Math.Tick.GetSqrtRatioAtTick(tick);
var tick = uniswap.Math.Tick.GetTickAtSqrtRatio(sqrtPriceX96);
Find available pools for a token pair using on-chain Initialize events:
var poolCache = uniswap.Pools.Cache;
var pools = await poolCache.FindPoolsForTokenAsync(token: usdc, fromBlockNumber: start, toBlockNumber: end);
Or fetch a specific pool directly:
var pool = await poolCache.GetOrFetchPoolAsync(currency0: eth, currency1: usdc, fee: 500, tickSpacing: 10);
For large trades or exotic pairs, route through intermediate tokens for better prices:
var pathFinder = uniswap.Pricing.QuotePricePathFinder;
var cachedPools = await poolCache.GetAllCachedPoolsAsync();
// Direct path only
var bestDirect = await pathFinder.FindBestDirectPathAsync(
tokenIn: eth, tokenOut: usdc,
amountIn: Web3.Convert.ToWei(1),
candidatePools: cachedPools);
// Multi-hop through intermediate tokens
var bestPath = await pathFinder.FindBestPathAsync(
tokenIn: usdc, tokenOut: weth,
amountIn: Web3.Convert.ToWei(1000, UnitConversion.EthUnit.Mwei),
intermediateTokens: new[] { dai, eth },
candidatePools: cachedPools);
Before swapping ERC-20 tokens (not native ETH), ensure the router has approval:
await uniswap.Accounts.Approvals.CheckAndApproveIfNeededAsync(
tokenAddress, owner, spender, requiredAmount);
// Or check and approve separately
var status = await uniswap.Accounts.Approvals.CheckApprovalAsync(
tokenAddress, owner, spender, requiredAmount);
if (!status.IsApproved)
{
await uniswap.Accounts.Approvals.ApproveAsync(
tokenAddress, spender, AccountApprovalService.GetMaxApprovalAmount());
}
Before executing a swap, verify the sender has enough tokens:
var balanceResult = await uniswap.Accounts.Balances.ValidateBalanceAsync(
tokenAddress, owner, requiredAmount);
if (!balanceResult.HasSufficientBalance)
Console.WriteLine($"Need {balanceResult.Deficit} more tokens");
For Uniswap V3, swaps use the same Universal Router but with Permit2 for gasless approvals. The flow: approve Permit2 once, then sign off-chain permits per swap:
// One-time: approve Permit2 to spend your WETH
var weth9Service = web3.Eth.ERC20.GetContractService(weth9);
await weth9Service.ApproveRequestAndWaitForReceiptAsync(permit2Address, IntType.MAX_INT256_VALUE);
// Per-swap: sign a Permit2 authorization
var permit = new PermitSingle
{
Spender = universalRouterAddress,
SigDeadline = 2000000000,
Details = new PermitDetails
{
Amount = amountIn * 100000,
Expiration = 0,
Nonce = 0,
Token = weth9
}
};
var permitService = new Permit2Service(web3, permit2Address);
var signedPermit = await permitService.GetSinglePermitWithSignatureAsync(
permit, new EthECKey(privateKey));
// Build the swap via Universal Router
var planner = new UniversalRouterBuilder();
planner.AddCommand(new Permit2PermitCommand
{
Permit = signedPermit.PermitRequest,
Signature = signedPermit.GetSignatureBytes()
});
planner.AddCommand(new V3SwapExactInCommand
{
AmountIn = amountIn,
AmountOutMinimum = quote.AmountOut - 10000,
Path = path,
Recipient = account.Address,
FundsFromPermit2OrUniversalRouter = true
});
var receipt = await universalRouterService.ExecuteRequestAndWaitForReceiptAsync(
planner.GetExecuteFunction(amountIn));
Uniswap reverts with custom errors that the router service can decode:
catch (SmartContractCustomErrorRevertException e)
{
var error = universalRouterService.FindCustomErrorException(e);
if (error != null) Console.WriteLine($"Uniswap error: {error.Message}");
}
For full documentation, see: https://docs.nethereum.com/docs/defi/guide-uniswap-swap