Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungThe Homelab Is the New Resume(21.09.2026 um 00:29 Uhr)
Sichere ProgrammierungPerl 🐪 Weekly #791 - The Dark Side is here!(21.09.2026 um 00:44 Uhr)
Sichere Programmierungbro.js v3.0.0 – What’s new(21.09.2026 um 00:44 Uhr)
Sichere ProgrammierungFour bugs my test suite couldn't catch(21.09.2026 um 00:49 Uhr)
Sichere ProgrammierungAutomating Deployment with Github Actions(21.09.2026 um 00:49 Uhr)
Linux Tipps & HardeningKernel prepatch 7.3-rc4(21.09.2026 um 00:52 Uhr)
IT NachrichtenHow to use Xbox mode on your Windows PC(21.09.2026 um 00:30 Uhr)
Sichere ProgrammierungThe Homelab Is the New Resume(21.09.2026 um 00:29 Uhr)
Sichere ProgrammierungPerl 🐪 Weekly #791 - The Dark Side is here!(21.09.2026 um 00:44 Uhr)
Sichere Programmierungbro.js v3.0.0 – What’s new(21.09.2026 um 00:44 Uhr)
Sichere ProgrammierungFour bugs my test suite couldn't catch(21.09.2026 um 00:49 Uhr)
Sichere ProgrammierungAutomating Deployment with Github Actions(21.09.2026 um 00:49 Uhr)
Linux Tipps & HardeningKernel prepatch 7.3-rc4(21.09.2026 um 00:52 Uhr)
IT NachrichtenHow to use Xbox mode on your Windows PC(21.09.2026 um 00:30 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

A .NET Dinosaur in Web3. Day 18 - Automated Market Maker

Reagiere als Erste:r — dein Feedback zählt!

🏦 Day 6 of 7: Building a Mini Uniswap in 80 Lines of Solidity

Imagine a vending machine. It has 1,000 coffee beans and 1,000 coins. No menu, no cashier — just one iron rule: the product of the two numbers inside must never decrease.

That's it!

This is how Uniswap works — and this is what I built on Day 6, coming from .NET. Here's how, why it's elegant, and where you can step on a rake.

Why an Order Book Doesn't Work on a Blockchain

Traditional exchanges — Binance, NYSE, any CEX — run on an order book. Market makers post bids and asks. A matching engine pairs them. Millions of updates per second, all in a centralised database.

In a blockchain, this is impossible. Transactions take 12 seconds. Every state change costs gas. Storing millions of constantly changing orders would eat all the profit before a single trade completes.

Uniswap's solution: replace the order book with a liquidity pool — a smart contract holding two tokens — and replace the matching engine with pure math.

Just a formula — below.

x · y = k — The Formula That Broke Finance

The Constant Product Invariant:

x · y = k

Where x is the reserve of Token0, y is the reserve of Token1, and k is a constant that must never decrease during swaps.

When a trader sells Token0 into the pool, x increases. To keep k constant, y must decrease — the contract sends out Token1. The price is determined automatically by the ratio of reserves.

Live example with numbers:

Pool: 1,000 Token0, 1,000 Token1. k = 1,000,000.

Trader sells 100 Token0:

amountOut = (reserveOut × amountIn) / (reserveIn + amountIn)
amountOut = (1000 × 100) / (1000 + 100)
amountOut = 100,000 / 1,100
amountOut ≈ 90.9 Token1

The trader gets ~90.9, not 100. That gap is slippage — and it's not a bug. It's the formula protecting the pool. The more you buy relative to pool size, the worse your price gets. Naturally. Mathematically.

After the swap: pool has 1,100 Token0 and ~909.1 Token1. k ≈ 1,000,000. Invariant holds.

The Contract: SimpleAMM

Three functions. Each one exists for a specific reason.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleAMM {
    error ZeroAmount();
    error InvalidToken();
    error ZeroLiquidity();
    error TransferFailed();
    error InvalidRatio();

    IERC20 public immutable token0;
    IERC20 public immutable token1;

    // Internal reserves — cheaper than calling balanceOf() every time
    uint256 public reserve0;
    uint256 public reserve1;

    event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1);
    event Swap(address indexed trader, address tokenIn, uint256 amountIn, uint256 amountOut);

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    // Pure math — no state, no side effects
    function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut)
        public pure returns (uint256)
    {
        if (_amountIn == 0) revert ZeroAmount();
        if (_reserveIn == 0 || _reserveOut == 0) revert ZeroLiquidity();

        // Multiply first, divide last — always
        uint256 numerator = _reserveOut * _amountIn;
        uint256 denominator = _reserveIn + _amountIn;
        return numerator / denominator;
    }

    function addLiquidity(uint256 _amount0, uint256 _amount1) external {
        if (_amount0 == 0 || _amount1 == 0) revert ZeroAmount();

        // If pool already has liquidity, enforce the current price ratio
        if (reserve0 > 0 && reserve1 > 0) {
            if (_amount0 * reserve1 != _amount1 * reserve0) revert InvalidRatio();
        }

        if (!token0.transferFrom(msg.sender, address(this), _amount0)) revert TransferFailed();
        if (!token1.transferFrom(msg.sender, address(this), _amount1)) revert TransferFailed();

        reserve0 += _amount0;
        reserve1 += _amount1;

        emit LiquidityAdded(msg.sender, _amount0, _amount1);
    }

    function swap(address _tokenIn, uint256 _amountIn) external returns (uint256 amountOut) {
        if (_amountIn == 0) revert ZeroAmount();
        if (_tokenIn != address(token0) && _tokenIn != address(token1)) revert InvalidToken();

        bool isToken0 = _tokenIn == address(token0);

        (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = isToken0
            ? (token0, token1, reserve0, reserve1)
            : (token1, token0, reserve1, reserve0);

        // CEI: pull tokens in first
        if (!tokenIn.transferFrom(msg.sender, address(this), _amountIn)) revert TransferFailed();

        // Calculate output
        amountOut = getAmountOut(_amountIn, reserveIn, reserveOut);

        // Update reserves
        if (isToken0) {
            reserve0 += _amountIn;
            reserve1 -= amountOut;
        } else {
            reserve0 -= amountOut;
            reserve1 += _amountIn;
        }

        emit Swap(msg.sender, _tokenIn, _amountIn, amountOut);

        // Send output tokens to trader
        if (!tokenOut.transfer(msg.sender, amountOut)) revert TransferFailed();
    }
}

getAmountOut — pure math, no state. Separated deliberately so it can be called by anyone to preview a trade before executing it. In DeFi this is standard: quote first, then transact.

addLiquidity — the ratio check is the interesting part. If the pool already has reserves, you can't deposit in arbitrary proportions. _amount0 * reserve1 != _amount1 * reserve0 detects any imbalance. Deposit skewed amounts and you'd instantly change the price — essentially donating money to arbitrageurs.

swap — the ternary tuple assignment is the cleanest part of the contract. Instead of two separate if/else branches, one line maps all four variables correctly based on direction:

(IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = isToken0
    ? (token0, token1, reserve0, reserve1)
    : (token1, token0, reserve1, reserve0);

Where You Can Step on a Rake

Integer division truncates, silently.

getAmountOut divides at the end — intentionally. But the truncation still happens. 100,000 / 1,100 = 90, not 90.909.... The pool keeps the remainder. At scale across millions of trades, this accumulated dust is non-trivial. Production AMMs handle this with basis points (fee = 30 bps = multiply by 997/1000 before dividing).

Internal reserves vs balanceOf.

The contract tracks reserve0 and reserve1 internally instead of calling token0.balanceOf(address(this)) every time. Two reasons: gas savings (SLOAD is expensive, external calls are more expensive), and security — if someone sends tokens directly to the contract without going through addLiquidity, the reserves won't silently become unbalanced and break the invariant.

Console Verification Flow

npx hardhat ignition deploy ignition/modules/SimpleAMM.ts --network localhost --reset
npx hardhat console --network localhost

const { viem } = await network.create();
const [owner, trader] = await viem.getWalletClients();
const cViem = require("viem");

const t0Address = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const t1Address = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
const ammAddress = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0";

const token0 = await viem.getContractAt("MyToken", t0Address);
const token1 = await viem.getContractAt("MyToken", t1Address);
const amm = await viem.getContractAt("SimpleAMM", ammAddress);

// Trader buys tokens via ICO
const t0AsTrader = await viem.getContractAt("MyToken", t0Address, { client: { wallet: trader } });
const t1AsTrader = await viem.getContractAt("MyToken", t1Address, { client: { wallet: trader } });
await t0AsTrader.write.buyTokens({ value: cViem.parseEther("2") }); // 2000 Token0
await t1AsTrader.write.buyTokens({ value: cViem.parseEther("2") }); // 2000 Token1

// Add liquidity 1000:1000
const ammAsTrader = await viem.getContractAt("SimpleAMM", ammAddress, { client: { wallet: trader } });
await t0AsTrader.write.approve([ammAddress, cViem.parseEther("1000")]);
await t1AsTrader.write.approve([ammAddress, cViem.parseEther("1000")]);
await ammAsTrader.write.addLiquidity([cViem.parseEther("1000"), cViem.parseEther("1000")]);

console.log("Reserve 0:", cViem.formatEther(await amm.read.reserve0())); // 1000
console.log("Reserve 1:", cViem.formatEther(await amm.read.reserve1())); // 1000

// Swap 100 Token0 → Token1
await t0AsTrader.write.approve([ammAddress, cViem.parseEther("100")]);
await ammAsTrader.write.swap([t0Address, cViem.parseEther("100")]);

const traderT1Balance = await token1.read.balanceOf([trader.account.address]);
console.log("Trader Token1 after swap:", cViem.formatEther(traderT1Balance));
// ~1090.909... — math checks out

The formula lands exactly. 1,000 × 100 / 1,100 = 90.909... Token1 received. The invariant holds.

What This Day Actually Meant

Six days ago I was writing owner = msg.sender in a constructor. Today I implemented the core pricing engine of a decentralised exchange.

What transferred directly from .NET:

  • CEI pattern — same as any transactional system
  • Separation of pure logic (getAmountOut) from state mutation (swap) — same as keeping domain logic out of controllers
  • Defensive checks before any state change — same as guard clauses

What was genuinely new:

  • Thinking in invariants instead of conditions
  • Price as an emergent property of reserves, not a stored value
  • The elegance of x · y = k — one line that replaces an entire matching engine

What's Next

Day 7: Reentrancy Protection — the vulnerability that cost $60M in the 2016 DAO hack, and how to write contracts that can't be drained.

Repo: github.com/alena-dev-soft

Follow the journey on Telegram: t.me/dotnetToWeb3

Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 6 of 7.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten A .NET Dinosaur in Web3. Day 18 - Automated Market Maker

Thematisch verwandte Begriffe: Dinosaur, Web3, Automated, Market · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94084 | Suricata before 8.0.7 has an Http2ThreadMultiBuf use-after-free when a t…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick