[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-8-polnyj-svop-tokenov-chistyj-yul":3},{"article":4,"author":58},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":7,"meta_description":16,"focus_keyword":17,"og_image":18,"canonical_url":18,"robots_meta":19,"created_at":15,"updated_at":15,"tags":20,"category_name":38,"related_articles":39},"d0000000-0000-0000-0000-000000000208","a0000000-0000-0000-0000-000000000012","Deep EVM #8: Полный своп токенов на чистом Yul","deep-evm-8-polnyj-svop-tokenov-chistyj-yul","Финальный проект серии Deep EVM: пишем полноценный контракт свопа токенов через Uniswap V2 целиком на Yul. Диспетчеризация функций, безопасные трансферы, расчёт выходного количества и сборка контракта.","## Цель проекта\n\nВ этой финальной статье серии мы объединяем все изученные концепции — опкоды, управление памятью, газоэффективные циклы, безопасность — и строим реальный контракт: **своп токенов через Uniswap V2**, написанный целиком на Yul.\n\nЭтот контракт будет:\n1. Принимать вызов `swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin)`\n2. Переводить `tokenIn` от пользователя на контракт\n3. Одобрять трансфер для пула Uniswap V2\n4. Рассчитывать выходное количество\n5. Выполнять своп через пул\n6. Переводить полученные токены пользователю\n\n## Структура контракта на Yul\n\nStandalone Yul-контракт состоит из двух секций: **конструктор** (init code) и **runtime code**:\n\n```yul\nobject \"TokenSwap\" {\n    \u002F\u002F Конструктор: выполняется при деплое\n    code {\n        \u002F\u002F Сохраняем owner в слот 0\n        sstore(0, caller())\n\n        \u002F\u002F Копируем runtime code в память и возвращаем\n        let size := datasize(\"runtime\")\n        let offset := dataoffset(\"runtime\")\n        datacopy(0, offset, size)\n        return(0, size)\n    }\n\n    object \"runtime\" {\n        \u002F\u002F Runtime code: выполняется при каждом вызове\n        code {\n            \u002F\u002F Диспетчеризация функций\n            switch selector()\n            case 0xd004f0f7 \u002F* swap(address,address,uint256,uint256) *\u002F {\n                swap()\n            }\n            case 0x8da5cb5b \u002F* owner() *\u002F {\n                returnUint(sload(0))\n            }\n            default {\n                revert(0, 0)\n            }\n\n            \u002F\u002F === Вспомогательные функции ===\n\n            function selector() -> s {\n                s := shr(224, calldataload(0))\n            }\n\n            function returnUint(v) {\n                mstore(0x00, v)\n                return(0x00, 0x20)\n            }\n\n            function revertWithReason(msg, len) {\n                mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)\n                mstore(0x04, 0x20)\n                mstore(0x24, len)\n                mstore(0x44, msg)\n                revert(0x00, 0x64)\n            }\n\n            \u002F\u002F ... (функции определены ниже)\n        }\n    }\n}\n```\n\n## Шаг 1: Диспетчеризация функций\n\nПервое, что делает контракт при получении вызова — определяет, какую функцию вызвать:\n\n```yul\nfunction selector() -> s {\n    \u002F\u002F Загружаем первые 32 байта calldata\n    \u002F\u002F Сдвигаем вправо на 224 бита, оставляя 4 байта селектора\n    s := shr(224, calldataload(0))\n}\n```\n\nСелектор функции — это первые 4 байта от `keccak256` сигнатуры:\n```\n\u002F\u002F keccak256(\"swap(address,address,uint256,uint256)\") = 0xd004f0f7...\n\u002F\u002F keccak256(\"owner()\") = 0x8da5cb5b...\n```\n\nНа уровне байткода `switch\u002Fcase` компилируется в серию `PUSH4 + EQ + PUSH2 + JUMPI` — точно так же, как Solidity делает диспетчеризацию.\n\n## Шаг 2: Безопасный трансфер токенов\n\nОдна из самых критичных функций — безопасный вызов `transfer` и `transferFrom` на ERC-20 токенах:\n\n```yul\nfunction safeTransferFrom(token, from, to, amount) {\n    \u002F\u002F Селектор transferFrom(address,address,uint256) = 0x23b872dd\n    mstore(0x00, 0x23b872dd00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, from)\n    mstore(0x24, to)\n    mstore(0x44, amount)\n\n    let success := call(gas(), token, 0, 0x00, 0x64, 0x00, 0x20)\n\n    \u002F\u002F Проверка: вызов успешен И (нет returndata ИЛИ returndata == true)\n    if iszero(and(\n        success,\n        or(\n            iszero(returndatasize()),\n            and(gt(returndatasize(), 31), eq(mload(0x00), 1))\n        )\n    )) {\n        \u002F\u002F \"Transfer failed\"\n        revertWithReason(\"Transfer failed\", 15)\n    }\n}\n\nfunction safeTransfer(token, to, amount) {\n    \u002F\u002F Селектор transfer(address,uint256) = 0xa9059cbb\n    mstore(0x00, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, to)\n    mstore(0x24, amount)\n\n    let success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)\n\n    if iszero(and(\n        success,\n        or(\n            iszero(returndatasize()),\n            and(gt(returndatasize(), 31), eq(mload(0x00), 1))\n        )\n    )) {\n        revertWithReason(\"Transfer failed\", 15)\n    }\n}\n\nfunction safeApprove(token, spender, amount) {\n    \u002F\u002F Селектор approve(address,uint256) = 0x095ea7b3\n    mstore(0x00, 0x095ea7b300000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, spender)\n    mstore(0x24, amount)\n\n    let success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)\n\n    if iszero(and(\n        success,\n        or(\n            iszero(returndatasize()),\n            and(gt(returndatasize(), 31), eq(mload(0x00), 1))\n        )\n    )) {\n        revertWithReason(\"Approve failed\", 14)\n    }\n}\n```\n\nПаттерн проверки `or(iszero(returndatasize()), and(gt(returndatasize(), 31), eq(mload(0x00), 1)))` обрабатывает оба случая:\n- Токены, не возвращающие данные (USDT) — `returndatasize() == 0`\n- Токены, возвращающие bool (стандартные ERC-20) — `mload(0x00) == 1`\n\n## Шаг 3: Вычисление адреса пула\n\nUniswap V2 использует CREATE2 для детерминированных адресов пулов:\n\n```yul\nfunction getPair(factory, tokenA, tokenB) -> pair {\n    \u002F\u002F Сортируем токены: token0 \u003C token1\n    let token0 := tokenA\n    let token1 := tokenB\n    if gt(tokenA, tokenB) {\n        token0 := tokenB\n        token1 := tokenA\n    }\n\n    \u002F\u002F Вычисляем salt = keccak256(abi.encodePacked(token0, token1))\n    mstore(0x00, token0)\n    mstore(0x20, token1)\n    let salt := keccak256(0x00, 0x40)\n\n    \u002F\u002F Вычисляем адрес CREATE2:\n    \u002F\u002F address = keccak256(0xff ++ factory ++ salt ++ init_code_hash)[12:]\n    mstore(0x00, 0xff00000000000000000000000000000000000000000000000000000000000000)\n    mstore8(1, shr(88, factory)) \u002F\u002F вставляем factory адрес побайтно\n    \u002F\u002F ... (упрощение — на практике используем полный расчёт)\n\n    \u002F\u002F Для демонстрации — вызываем factory.getPair()\n    mstore(0x00, 0xe6a4390500000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, tokenA)\n    mstore(0x24, tokenB)\n\n    let success := staticcall(gas(), factory, 0x00, 0x44, 0x00, 0x20)\n    if iszero(success) { revert(0, 0) }\n\n    pair := mload(0x00)\n    if iszero(pair) {\n        revertWithReason(\"Pair not found\", 14)\n    }\n}\n```\n\n## Шаг 4: Расчёт выходного количества\n\nФормула Uniswap V2 для расчёта выходного количества:\n\n```\namountOut = (amountIn * 997 * reserveOut) \u002F (reserveIn * 1000 + amountIn * 997)\n```\n\nНа Yul:\n\n```yul\nfunction getAmountOut(amountIn, reserveIn, reserveOut) -> amountOut {\n    \u002F\u002F amountIn должен быть > 0\n    if iszero(amountIn) {\n        revertWithReason(\"Zero input\", 10)\n    }\n    \u002F\u002F Резервы должны быть > 0\n    if or(iszero(reserveIn), iszero(reserveOut)) {\n        revertWithReason(\"No liquidity\", 12)\n    }\n\n    let amountInWithFee := mul(amountIn, 997)\n    let numerator := mul(amountInWithFee, reserveOut)\n    let denominator := add(mul(reserveIn, 1000), amountInWithFee)\n\n    amountOut := div(numerator, denominator)\n}\n\nfunction getReserves(pair, tokenA, tokenB) -> reserveA, reserveB {\n    \u002F\u002F Вызов getReserves() на пуле\n    mstore(0x00, 0x0902f1ac00000000000000000000000000000000000000000000000000000000)\n    let success := staticcall(gas(), pair, 0x00, 0x04, 0x00, 0x60)\n    if iszero(success) { revert(0, 0) }\n\n    let reserve0 := mload(0x00)\n    let reserve1 := mload(0x20)\n\n    \u002F\u002F Определяем порядок токенов\n    switch lt(tokenA, tokenB)\n    case 1 {\n        reserveA := reserve0\n        reserveB := reserve1\n    }\n    default {\n        reserveA := reserve1\n        reserveB := reserve0\n    }\n}\n```\n\n## Шаг 5: Выполнение свопа\n\nГлавная функция, объединяющая все шаги:\n\n```yul\nfunction swap() {\n    \u002F\u002F Декодируем аргументы из calldata\n    let tokenIn := and(calldataload(4), 0xffffffffffffffffffffffffffffffffffffffff)\n    let tokenOut := and(calldataload(36), 0xffffffffffffffffffffffffffffffffffffffff)\n    let amountIn := calldataload(68)\n    let amountOutMin := calldataload(100)\n\n    \u002F\u002F Валидация\n    if iszero(amountIn) {\n        revertWithReason(\"Zero amount\", 11)\n    }\n\n    \u002F\u002F Адрес Uniswap V2 Factory (Ethereum mainnet)\n    let factory := 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f\n\n    \u002F\u002F 1. Получаем адрес пула\n    let pair := getPair(factory, tokenIn, tokenOut)\n\n    \u002F\u002F 2. Переводим tokenIn от пользователя на пул\n    safeTransferFrom(tokenIn, caller(), pair, amountIn)\n\n    \u002F\u002F 3. Получаем резервы\n    let reserveIn, reserveOut := getReserves(pair, tokenIn, tokenOut)\n\n    \u002F\u002F 4. Рассчитываем выходное количество\n    let amountOut := getAmountOut(amountIn, reserveIn, reserveOut)\n\n    \u002F\u002F 5. Проверяем slippage\n    if lt(amountOut, amountOutMin) {\n        revertWithReason(\"Slippage\", 8)\n    }\n\n    \u002F\u002F 6. Определяем порядок amount0Out\u002Famount1Out\n    let amount0Out := 0\n    let amount1Out := 0\n    switch lt(tokenIn, tokenOut)\n    case 1 {\n        \u002F\u002F tokenIn = token0, tokenOut = token1\n        amount1Out := amountOut\n    }\n    default {\n        \u002F\u002F tokenIn = token1, tokenOut = token0\n        amount0Out := amountOut\n    }\n\n    \u002F\u002F 7. Вызываем pair.swap(amount0Out, amount1Out, to, data)\n    mstore(0x00, 0x022c0d9f00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, amount0Out)\n    mstore(0x24, amount1Out)\n    mstore(0x44, caller())      \u002F\u002F получатель — вызывающий\n    mstore(0x64, 0x80)          \u002F\u002F смещение до data\n    mstore(0x84, 0)             \u002F\u002F длина data = 0 (нет flash swap)\n\n    let success := call(gas(), pair, 0, 0x00, 0xa4, 0x00, 0x00)\n    if iszero(success) {\n        \u002F\u002F Копируем revert reason\n        let rSize := returndatasize()\n        returndatacopy(0x00, 0x00, rSize)\n        revert(0x00, rSize)\n    }\n\n    \u002F\u002F 8. Возвращаем amountOut\n    returnUint(amountOut)\n}\n```\n\n## Анализ газа\n\nСравним наш Yul-контракт с эквивалентом на Solidity:\n\n| Операция | Solidity | Yul | Экономия |\n|----------|----------|-----|----------|\n| Диспетчеризация функций | ~200 | ~80 | 60% |\n| Декодирование calldata | ~500 | ~50 | 90% |\n| safeTransferFrom | ~1500 | ~800 | 47% |\n| getReserves | ~1000 | ~500 | 50% |\n| Расчёт amountOut | ~300 | ~100 | 67% |\n| Вызов pair.swap | ~1200 | ~600 | 50% |\n| **Итого (без внешних вызовов)** | **~4700** | **~2130** | **55%** |\n\nОбщая стоимость свопа останется высокой (~120000-150000 газа) из-за SLOAD\u002FSSTORE внутри пула. Но для MEV-ботов, выполняющих тысячи свопов в день, экономия ~2500 газа за своп — это существенная прибыль.\n\n## Безопасность контракта\n\nНаш контракт имеет несколько важных ограничений:\n\n1. **Нет проверки переполнения** — расчёт `amountInWithFee * reserveOut` может переполниться для очень больших значений. В продакшене используйте `mulmod` или проверки.\n2. **Фронтраннинг** — `amountOutMin` защищает от слишком большого проскальзывания, но не от сэндвич-атак.\n3. **Нет deadline** — в продакшене добавьте проверку `timestamp \u003C deadline`.\n4. **Одноразовый approve** — для максимальной безопасности используйте `approve` на точную сумму.\n\n## Деплой и тестирование\n\nДля компиляции и деплоя standalone Yul-контракта:\n\n```bash\n# Компиляция с solc\nsolc --strict-assembly --optimize --optimize-runs 200 swap.yul\n\n# Тестирование с Foundry\nforge test -vvvv --match-contract SwapTest\n```\n\n## Итоги серии\n\nЗа восемь статей серии Deep EVM мы прошли путь от базовых опкодов до полноценного контракта на Yul:\n\n1. **Опкоды, стек, газ** — фундамент EVM\n2. **Модель памяти** — стек, память, хранилище, calldata\n3. **Экономика газа** — стоимость операций и оптимизация\n4. **Безопасность** — msg.sender, реентрабельность, CEI\n5. **Введение в Yul** — синтаксис и встроенные функции\n6. **Управление памятью** — mstore, mload, свободный указатель\n7. **Циклы и условия** — газоэффективные паттерны\n8. **Своп на Yul** — применение всех концепций\n\nЭти знания — фундамент для серьёзной разработки смарт-контрактов, аудита безопасности и создания MEV-ботов. Понимание EVM на уровне опкодов отличает хорошего разработчика от великого.","\u003Ch2 id=\"\">Цель проекта\u003C\u002Fh2>\n\u003Cp>В этой финальной статье серии мы объединяем все изученные концепции — опкоды, управление памятью, газоэффективные циклы, безопасность — и строим реальный контракт: \u003Cstrong>своп токенов через Uniswap V2\u003C\u002Fstrong>, написанный целиком на Yul.\u003C\u002Fp>\n\u003Cp>Этот контракт будет:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Принимать вызов \u003Ccode>swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin)\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Переводить \u003Ccode>tokenIn\u003C\u002Fcode> от пользователя на контракт\u003C\u002Fli>\n\u003Cli>Одобрять трансфер для пула Uniswap V2\u003C\u002Fli>\n\u003Cli>Рассчитывать выходное количество\u003C\u002Fli>\n\u003Cli>Выполнять своп через пул\u003C\u002Fli>\n\u003Cli>Переводить полученные токены пользователю\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"yul\">Структура контракта на Yul\u003C\u002Fh2>\n\u003Cp>Standalone Yul-контракт состоит из двух секций: \u003Cstrong>конструктор\u003C\u002Fstrong> (init code) и \u003Cstrong>runtime code\u003C\u002Fstrong>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">object \"TokenSwap\" {\n    \u002F\u002F Конструктор: выполняется при деплое\n    code {\n        \u002F\u002F Сохраняем owner в слот 0\n        sstore(0, caller())\n\n        \u002F\u002F Копируем runtime code в память и возвращаем\n        let size := datasize(\"runtime\")\n        let offset := dataoffset(\"runtime\")\n        datacopy(0, offset, size)\n        return(0, size)\n    }\n\n    object \"runtime\" {\n        \u002F\u002F Runtime code: выполняется при каждом вызове\n        code {\n            \u002F\u002F Диспетчеризация функций\n            switch selector()\n            case 0xd004f0f7 \u002F* swap(address,address,uint256,uint256) *\u002F {\n                swap()\n            }\n            case 0x8da5cb5b \u002F* owner() *\u002F {\n                returnUint(sload(0))\n            }\n            default {\n                revert(0, 0)\n            }\n\n            \u002F\u002F === Вспомогательные функции ===\n\n            function selector() -&gt; s {\n                s := shr(224, calldataload(0))\n            }\n\n            function returnUint(v) {\n                mstore(0x00, v)\n                return(0x00, 0x20)\n            }\n\n            function revertWithReason(msg, len) {\n                mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)\n                mstore(0x04, 0x20)\n                mstore(0x24, len)\n                mstore(0x44, msg)\n                revert(0x00, 0x64)\n            }\n\n            \u002F\u002F ... (функции определены ниже)\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"1\">Шаг 1: Диспетчеризация функций\u003C\u002Fh2>\n\u003Cp>Первое, что делает контракт при получении вызова — определяет, какую функцию вызвать:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function selector() -&gt; s {\n    \u002F\u002F Загружаем первые 32 байта calldata\n    \u002F\u002F Сдвигаем вправо на 224 бита, оставляя 4 байта селектора\n    s := shr(224, calldataload(0))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Селектор функции — это первые 4 байта от \u003Ccode>keccak256\u003C\u002Fcode> сигнатуры:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F keccak256(\"swap(address,address,uint256,uint256)\") = 0xd004f0f7...\n\u002F\u002F keccak256(\"owner()\") = 0x8da5cb5b...\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>На уровне байткода \u003Ccode>switch\u002Fcase\u003C\u002Fcode> компилируется в серию \u003Ccode>PUSH4 + EQ + PUSH2 + JUMPI\u003C\u002Fcode> — точно так же, как Solidity делает диспетчеризацию.\u003C\u002Fp>\n\u003Ch2 id=\"2\">Шаг 2: Безопасный трансфер токенов\u003C\u002Fh2>\n\u003Cp>Одна из самых критичных функций — безопасный вызов \u003Ccode>transfer\u003C\u002Fcode> и \u003Ccode>transferFrom\u003C\u002Fcode> на ERC-20 токенах:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function safeTransferFrom(token, from, to, amount) {\n    \u002F\u002F Селектор transferFrom(address,address,uint256) = 0x23b872dd\n    mstore(0x00, 0x23b872dd00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, from)\n    mstore(0x24, to)\n    mstore(0x44, amount)\n\n    let success := call(gas(), token, 0, 0x00, 0x64, 0x00, 0x20)\n\n    \u002F\u002F Проверка: вызов успешен И (нет returndata ИЛИ returndata == true)\n    if iszero(and(\n        success,\n        or(\n            iszero(returndatasize()),\n            and(gt(returndatasize(), 31), eq(mload(0x00), 1))\n        )\n    )) {\n        \u002F\u002F \"Transfer failed\"\n        revertWithReason(\"Transfer failed\", 15)\n    }\n}\n\nfunction safeTransfer(token, to, amount) {\n    \u002F\u002F Селектор transfer(address,uint256) = 0xa9059cbb\n    mstore(0x00, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, to)\n    mstore(0x24, amount)\n\n    let success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)\n\n    if iszero(and(\n        success,\n        or(\n            iszero(returndatasize()),\n            and(gt(returndatasize(), 31), eq(mload(0x00), 1))\n        )\n    )) {\n        revertWithReason(\"Transfer failed\", 15)\n    }\n}\n\nfunction safeApprove(token, spender, amount) {\n    \u002F\u002F Селектор approve(address,uint256) = 0x095ea7b3\n    mstore(0x00, 0x095ea7b300000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, spender)\n    mstore(0x24, amount)\n\n    let success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)\n\n    if iszero(and(\n        success,\n        or(\n            iszero(returndatasize()),\n            and(gt(returndatasize(), 31), eq(mload(0x00), 1))\n        )\n    )) {\n        revertWithReason(\"Approve failed\", 14)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Паттерн проверки \u003Ccode>or(iszero(returndatasize()), and(gt(returndatasize(), 31), eq(mload(0x00), 1)))\u003C\u002Fcode> обрабатывает оба случая:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Токены, не возвращающие данные (USDT) — \u003Ccode>returndatasize() == 0\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Токены, возвращающие bool (стандартные ERC-20) — \u003Ccode>mload(0x00) == 1\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"3\">Шаг 3: Вычисление адреса пула\u003C\u002Fh2>\n\u003Cp>Uniswap V2 использует CREATE2 для детерминированных адресов пулов:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function getPair(factory, tokenA, tokenB) -&gt; pair {\n    \u002F\u002F Сортируем токены: token0 &lt; token1\n    let token0 := tokenA\n    let token1 := tokenB\n    if gt(tokenA, tokenB) {\n        token0 := tokenB\n        token1 := tokenA\n    }\n\n    \u002F\u002F Вычисляем salt = keccak256(abi.encodePacked(token0, token1))\n    mstore(0x00, token0)\n    mstore(0x20, token1)\n    let salt := keccak256(0x00, 0x40)\n\n    \u002F\u002F Вычисляем адрес CREATE2:\n    \u002F\u002F address = keccak256(0xff ++ factory ++ salt ++ init_code_hash)[12:]\n    mstore(0x00, 0xff00000000000000000000000000000000000000000000000000000000000000)\n    mstore8(1, shr(88, factory)) \u002F\u002F вставляем factory адрес побайтно\n    \u002F\u002F ... (упрощение — на практике используем полный расчёт)\n\n    \u002F\u002F Для демонстрации — вызываем factory.getPair()\n    mstore(0x00, 0xe6a4390500000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, tokenA)\n    mstore(0x24, tokenB)\n\n    let success := staticcall(gas(), factory, 0x00, 0x44, 0x00, 0x20)\n    if iszero(success) { revert(0, 0) }\n\n    pair := mload(0x00)\n    if iszero(pair) {\n        revertWithReason(\"Pair not found\", 14)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"4\">Шаг 4: Расчёт выходного количества\u003C\u002Fh2>\n\u003Cp>Формула Uniswap V2 для расчёта выходного количества:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>amountOut = (amountIn * 997 * reserveOut) \u002F (reserveIn * 1000 + amountIn * 997)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>На Yul:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function getAmountOut(amountIn, reserveIn, reserveOut) -&gt; amountOut {\n    \u002F\u002F amountIn должен быть &gt; 0\n    if iszero(amountIn) {\n        revertWithReason(\"Zero input\", 10)\n    }\n    \u002F\u002F Резервы должны быть &gt; 0\n    if or(iszero(reserveIn), iszero(reserveOut)) {\n        revertWithReason(\"No liquidity\", 12)\n    }\n\n    let amountInWithFee := mul(amountIn, 997)\n    let numerator := mul(amountInWithFee, reserveOut)\n    let denominator := add(mul(reserveIn, 1000), amountInWithFee)\n\n    amountOut := div(numerator, denominator)\n}\n\nfunction getReserves(pair, tokenA, tokenB) -&gt; reserveA, reserveB {\n    \u002F\u002F Вызов getReserves() на пуле\n    mstore(0x00, 0x0902f1ac00000000000000000000000000000000000000000000000000000000)\n    let success := staticcall(gas(), pair, 0x00, 0x04, 0x00, 0x60)\n    if iszero(success) { revert(0, 0) }\n\n    let reserve0 := mload(0x00)\n    let reserve1 := mload(0x20)\n\n    \u002F\u002F Определяем порядок токенов\n    switch lt(tokenA, tokenB)\n    case 1 {\n        reserveA := reserve0\n        reserveB := reserve1\n    }\n    default {\n        reserveA := reserve1\n        reserveB := reserve0\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"5\">Шаг 5: Выполнение свопа\u003C\u002Fh2>\n\u003Cp>Главная функция, объединяющая все шаги:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function swap() {\n    \u002F\u002F Декодируем аргументы из calldata\n    let tokenIn := and(calldataload(4), 0xffffffffffffffffffffffffffffffffffffffff)\n    let tokenOut := and(calldataload(36), 0xffffffffffffffffffffffffffffffffffffffff)\n    let amountIn := calldataload(68)\n    let amountOutMin := calldataload(100)\n\n    \u002F\u002F Валидация\n    if iszero(amountIn) {\n        revertWithReason(\"Zero amount\", 11)\n    }\n\n    \u002F\u002F Адрес Uniswap V2 Factory (Ethereum mainnet)\n    let factory := 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f\n\n    \u002F\u002F 1. Получаем адрес пула\n    let pair := getPair(factory, tokenIn, tokenOut)\n\n    \u002F\u002F 2. Переводим tokenIn от пользователя на пул\n    safeTransferFrom(tokenIn, caller(), pair, amountIn)\n\n    \u002F\u002F 3. Получаем резервы\n    let reserveIn, reserveOut := getReserves(pair, tokenIn, tokenOut)\n\n    \u002F\u002F 4. Рассчитываем выходное количество\n    let amountOut := getAmountOut(amountIn, reserveIn, reserveOut)\n\n    \u002F\u002F 5. Проверяем slippage\n    if lt(amountOut, amountOutMin) {\n        revertWithReason(\"Slippage\", 8)\n    }\n\n    \u002F\u002F 6. Определяем порядок amount0Out\u002Famount1Out\n    let amount0Out := 0\n    let amount1Out := 0\n    switch lt(tokenIn, tokenOut)\n    case 1 {\n        \u002F\u002F tokenIn = token0, tokenOut = token1\n        amount1Out := amountOut\n    }\n    default {\n        \u002F\u002F tokenIn = token1, tokenOut = token0\n        amount0Out := amountOut\n    }\n\n    \u002F\u002F 7. Вызываем pair.swap(amount0Out, amount1Out, to, data)\n    mstore(0x00, 0x022c0d9f00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, amount0Out)\n    mstore(0x24, amount1Out)\n    mstore(0x44, caller())      \u002F\u002F получатель — вызывающий\n    mstore(0x64, 0x80)          \u002F\u002F смещение до data\n    mstore(0x84, 0)             \u002F\u002F длина data = 0 (нет flash swap)\n\n    let success := call(gas(), pair, 0, 0x00, 0xa4, 0x00, 0x00)\n    if iszero(success) {\n        \u002F\u002F Копируем revert reason\n        let rSize := returndatasize()\n        returndatacopy(0x00, 0x00, rSize)\n        revert(0x00, rSize)\n    }\n\n    \u002F\u002F 8. Возвращаем amountOut\n    returnUint(amountOut)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Анализ газа\u003C\u002Fh2>\n\u003Cp>Сравним наш Yul-контракт с эквивалентом на Solidity:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Операция\u003C\u002Fth>\u003Cth>Solidity\u003C\u002Fth>\u003Cth>Yul\u003C\u002Fth>\u003Cth>Экономия\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Диспетчеризация функций\u003C\u002Ftd>\u003Ctd>~200\u003C\u002Ftd>\u003Ctd>~80\u003C\u002Ftd>\u003Ctd>60%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Декодирование calldata\u003C\u002Ftd>\u003Ctd>~500\u003C\u002Ftd>\u003Ctd>~50\u003C\u002Ftd>\u003Ctd>90%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>safeTransferFrom\u003C\u002Ftd>\u003Ctd>~1500\u003C\u002Ftd>\u003Ctd>~800\u003C\u002Ftd>\u003Ctd>47%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>getReserves\u003C\u002Ftd>\u003Ctd>~1000\u003C\u002Ftd>\u003Ctd>~500\u003C\u002Ftd>\u003Ctd>50%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Расчёт amountOut\u003C\u002Ftd>\u003Ctd>~300\u003C\u002Ftd>\u003Ctd>~100\u003C\u002Ftd>\u003Ctd>67%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Вызов pair.swap\u003C\u002Ftd>\u003Ctd>~1200\u003C\u002Ftd>\u003Ctd>~600\u003C\u002Ftd>\u003Ctd>50%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>\u003Cstrong>Итого (без внешних вызовов)\u003C\u002Fstrong>\u003C\u002Ftd>\u003Ctd>\u003Cstrong>~4700\u003C\u002Fstrong>\u003C\u002Ftd>\u003Ctd>\u003Cstrong>~2130\u003C\u002Fstrong>\u003C\u002Ftd>\u003Ctd>\u003Cstrong>55%\u003C\u002Fstrong>\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Общая стоимость свопа останется высокой (~120000-150000 газа) из-за SLOAD\u002FSSTORE внутри пула. Но для MEV-ботов, выполняющих тысячи свопов в день, экономия ~2500 газа за своп — это существенная прибыль.\u003C\u002Fp>\n\u003Ch2 id=\"\">Безопасность контракта\u003C\u002Fh2>\n\u003Cp>Наш контракт имеет несколько важных ограничений:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Нет проверки переполнения\u003C\u002Fstrong> — расчёт \u003Ccode>amountInWithFee * reserveOut\u003C\u002Fcode> может переполниться для очень больших значений. В продакшене используйте \u003Ccode>mulmod\u003C\u002Fcode> или проверки.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Фронтраннинг\u003C\u002Fstrong> — \u003Ccode>amountOutMin\u003C\u002Fcode> защищает от слишком большого проскальзывания, но не от сэндвич-атак.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Нет deadline\u003C\u002Fstrong> — в продакшене добавьте проверку \u003Ccode>timestamp &lt; deadline\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Одноразовый approve\u003C\u002Fstrong> — для максимальной безопасности используйте \u003Ccode>approve\u003C\u002Fcode> на точную сумму.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"\">Деплой и тестирование\u003C\u002Fh2>\n\u003Cp>Для компиляции и деплоя standalone Yul-контракта:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\"># Компиляция с solc\nsolc --strict-assembly --optimize --optimize-runs 200 swap.yul\n\n# Тестирование с Foundry\nforge test -vvvv --match-contract SwapTest\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Итоги серии\u003C\u002Fh2>\n\u003Cp>За восемь статей серии Deep EVM мы прошли путь от базовых опкодов до полноценного контракта на Yul:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Опкоды, стек, газ\u003C\u002Fstrong> — фундамент EVM\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Модель памяти\u003C\u002Fstrong> — стек, память, хранилище, calldata\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Экономика газа\u003C\u002Fstrong> — стоимость операций и оптимизация\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Безопасность\u003C\u002Fstrong> — msg.sender, реентрабельность, CEI\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Введение в Yul\u003C\u002Fstrong> — синтаксис и встроенные функции\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Управление памятью\u003C\u002Fstrong> — mstore, mload, свободный указатель\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Циклы и условия\u003C\u002Fstrong> — газоэффективные паттерны\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Своп на Yul\u003C\u002Fstrong> — применение всех концепций\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Эти знания — фундамент для серьёзной разработки смарт-контрактов, аудита безопасности и создания MEV-ботов. Понимание EVM на уровне опкодов отличает хорошего разработчика от великого.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.434596Z","Финальный проект серии Deep EVM: контракт свопа токенов через Uniswap V2 целиком на Yul с детальным разбором каждого шага.","Yul своп токенов Uniswap",null,"index, follow",[21,26,30,34],{"id":22,"name":23,"slug":24,"created_at":25},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":27,"name":28,"slug":29,"created_at":25},"c0000000-0000-0000-0000-000000000020","Gas Optimization","gas-optimization",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000017","Huff","huff",{"id":35,"name":36,"slug":37,"created_at":25},"c0000000-0000-0000-0000-000000000018","Yul","yul","Блокчейн",[40,46,52],{"id":41,"title":42,"slug":43,"excerpt":44,"locale":12,"category_name":38,"published_at":45},"de000000-0000-0000-0000-000000000013","Уровень интероперабельности Ethereum: как 55+ L2 становятся одной сетью","uroven-interoperabelnosti-ethereum-kak-55-l2-stanovyatsya-odnoj-setyu","У Ethereum 55+ роллапов Layer 2, фрагментирующих ликвидность и пользовательский опыт. Уровень интероперабельности Ethereum — объединяющий кросс-роллап-мессаджинг, общие секвенсоры и based-роллапы — призван объединить их в единую компонуемую сеть.","2026-03-28T10:44:36.068675Z",{"id":47,"title":48,"slug":49,"excerpt":50,"locale":12,"category_name":38,"published_at":51},"de000000-0000-0000-0000-000000000012","ZK-доказательства за пределами роллапов: верифицируемый AI-инференс на Ethereum","zk-dokazatelstva-za-predelami-rollapov-verificiruemyj-ai-inferens-ethereum","Доказательства с нулевым разглашением — это уже не только инструмент масштабирования. В 2026 году zkML обеспечивает верифицируемый AI-инференс в блокчейне, ZK-копроцессоры переносят тяжёлые вычисления оффчейн с ончейн-верификацией, а новые системы доказательств SP1 и Jolt делают это практичным.","2026-03-28T10:44:36.026310Z",{"id":53,"title":54,"slug":55,"excerpt":56,"locale":12,"category_name":38,"published_at":57},"dd000000-0000-0000-0000-000000000013","EIP-7702 на практике: создание потоков смарт-аккаунтов после Pectra","eip-7702-na-praktike-sozdanie-potokov-smart-akkauntov-posle-pectra","EIP-7702 позволяет любому EOA Ethereum временно действовать как смарт-контракт в рамках одной транзакции. Вот как реализовать пакетные транзакции, спонсирование газа и социальное восстановление с помощью нового примитива абстракции аккаунтов.","2026-03-28T10:44:35.357227Z",{"id":13,"name":59,"slug":60,"bio":61,"photo_url":18,"linkedin":18,"role":62,"created_at":63,"updated_at":63},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]