[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-4-primitivy-bezopasnosti-kontrol-dostupa-reenterabelnost":3},{"article":4,"author":55},{"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":34,"related_articles":35},"d0000000-0000-0000-0000-000000000204","a0000000-0000-0000-0000-000000000012","Deep EVM #4: Примитивы безопасности — msg.sender, контроль доступа и реентрабельность","deep-evm-4-primitivy-bezopasnosti-kontrol-dostupa-reenterabelnost","Как работают msg.sender, tx.origin и контроль доступа на уровне EVM. Детальный разбор атаки реентрабельности, паттерна checks-effects-interactions и современных защитных механизмов.","## Безопасность начинается с понимания контекста\n\nБезопасность смарт-контрактов — это не набор правил, которые вы запоминаете. Это глубокое понимание того, как EVM обрабатывает вызовы, кто является вызывающим, и что происходит, когда один контракт вызывает другой. Большинство критических уязвимостей в истории Ethereum были вызваны непониманием этих фундаментальных механизмов.\n\n## msg.sender vs tx.origin\n\nНа уровне опкодов:\n- **CALLER** (msg.sender) — адрес, непосредственно вызвавший текущий контракт. Если контракт A вызывает контракт B, то внутри B `msg.sender == address(A)`.\n- **ORIGIN** (tx.origin) — адрес EOA (внешнего аккаунта), инициировавшего транзакцию. Остаётся неизменным через все уровни вложенных вызовов.\n\n```solidity\n\u002F\u002F Пользователь -> Контракт A -> Контракт B -> Контракт C\n\u002F\u002F Внутри C:\n\u002F\u002F msg.sender = address(B)   \u002F\u002F непосредственный вызывающий\n\u002F\u002F tx.origin  = Пользователь \u002F\u002F инициатор транзакции\n```\n\n### Почему tx.origin опасен\n\nИспользование `tx.origin` для авторизации — классическая уязвимость:\n\n```solidity\n\u002F\u002F УЯЗВИМЫЙ контракт\ncontract VulnerableWallet {\n    address public owner;\n\n    function transfer(address to, uint256 amount) external {\n        require(tx.origin == owner); \u002F\u002F ОПАСНО!\n        payable(to).transfer(amount);\n    }\n}\n\n\u002F\u002F Атака:\n\u002F\u002F 1. Злоумышленник создаёт контракт-приманку\n\u002F\u002F 2. Владелец взаимодействует с приманкой (например, \"получить бесплатные NFT\")\n\u002F\u002F 3. Приманка вызывает VulnerableWallet.transfer(attacker, balance)\n\u002F\u002F 4. tx.origin == owner проходит, потому что владелец инициировал транзакцию!\n\u002F\u002F 5. Средства украдены\n\ncontract Attacker {\n    VulnerableWallet wallet;\n\n    function attack() external {\n        wallet.transfer(msg.sender, address(wallet).balance);\n    }\n\n    \u002F\u002F Владелец вызывает эту \"безобидную\" функцию\n    function claimFreeNFT() external {\n        \u002F\u002F ... якобы логика NFT ...\n        this.attack(); \u002F\u002F tx.origin всё ещё == владелец\n    }\n}\n```\n\nВсегда используйте `msg.sender` для авторизации. Единственное безопасное применение `tx.origin` — проверка, что вызывающий является EOA (не контрактом): `require(msg.sender == tx.origin)`.\n\n## Паттерны контроля доступа\n\n### Простой Ownable\n\nПростейший паттерн — единственный владелец:\n\n```solidity\ncontract Ownable {\n    address public owner;\n    error NotOwner();\n\n    modifier onlyOwner() {\n        if (msg.sender != owner) revert NotOwner();\n        _;\n    }\n\n    constructor() {\n        owner = msg.sender;\n    }\n\n    function transferOwnership(address newOwner) external onlyOwner {\n        owner = newOwner;\n    }\n}\n```\n\nНа уровне EVM `onlyOwner` компилируется примерно так:\n```\nCALLER           \u002F\u002F msg.sender на стек\nSLOAD(0)         \u002F\u002F owner из хранилища\nEQ               \u002F\u002F сравнение\nPUSH2 continue   \u002F\u002F адрес продолжения\nJUMPI            \u002F\u002F если равны — продолжить\nREVERT           \u002F\u002F иначе — откат\ncontinue:\nJUMPDEST\n```\n\n### Role-Based Access Control (RBAC)\n\nДля более сложных систем используется ролевой контроль доступа:\n\n```solidity\ncontract AccessControl {\n    mapping(bytes32 => mapping(address => bool)) private _roles;\n\n    bytes32 public constant ADMIN_ROLE = keccak256(\"ADMIN\");\n    bytes32 public constant MINTER_ROLE = keccak256(\"MINTER\");\n    bytes32 public constant PAUSER_ROLE = keccak256(\"PAUSER\");\n\n    modifier onlyRole(bytes32 role) {\n        require(_roles[role][msg.sender], \"AccessControl: unauthorized\");\n        _;\n    }\n\n    function grantRole(bytes32 role, address account) external onlyRole(ADMIN_ROLE) {\n        _roles[role][account] = true;\n    }\n\n    function revokeRole(bytes32 role, address account) external onlyRole(ADMIN_ROLE) {\n        _roles[role][account] = false;\n    }\n}\n```\n\nВ хранилище проверка `_roles[role][msg.sender]` вычисляется как:\n```\nslot = keccak256(msg.sender . keccak256(role . base_slot))\nSLOAD(slot) \u002F\u002F 2100 газа (холодный) или 100 газа (тёплый)\n```\n\n## Реентрабельность: самая опасная уязвимость\n\nРеентрабельность (reentrancy) — уязвимость, при которой внешний вызов позволяет вызываемому контракту повторно войти в вызывающий контракт до завершения текущего выполнения.\n\n### Классический пример: DAO Hack\n\nЭта уязвимость стала причиной взлома The DAO в 2016 году (украдено ~$60M), что привело к хардфорку Ethereum.\n\n```solidity\n\u002F\u002F УЯЗВИМЫЙ контракт\ncontract VulnerableVault {\n    mapping(address => uint256) public balances;\n\n    function deposit() external payable {\n        balances[msg.sender] += msg.value;\n    }\n\n    function withdraw() external {\n        uint256 amount = balances[msg.sender];\n        require(amount > 0);\n\n        \u002F\u002F ОПАСНО: внешний вызов ДО обновления состояния\n        (bool success, ) = msg.sender.call{value: amount}(\"\");\n        require(success);\n\n        \u002F\u002F Это выполнится только после возврата из call\n        \u002F\u002F Но к этому моменту атакующий уже вызвал withdraw повторно!\n        balances[msg.sender] = 0;\n    }\n}\n\n\u002F\u002F Контракт атакующего\ncontract Attacker {\n    VulnerableVault vault;\n\n    function attack() external payable {\n        vault.deposit{value: 1 ether}();\n        vault.withdraw();\n    }\n\n    \u002F\u002F Вызывается при получении ETH\n    receive() external payable {\n        if (address(vault).balance >= 1 ether) {\n            vault.withdraw(); \u002F\u002F Повторный вход!\n        }\n    }\n}\n```\n\nПоследовательность вызовов:\n```\nAttacker.attack()\n  -> Vault.withdraw()\n    -> Attacker.receive() (получает 1 ETH)\n      -> Vault.withdraw() (balances[attacker] всё ещё > 0!)\n        -> Attacker.receive() (получает ещё 1 ETH)\n          -> ... (повторяется, пока в Vault есть ETH)\n```\n\n### Паттерн Checks-Effects-Interactions (CEI)\n\nГлавный защитный паттерн от реентрабельности:\n\n```solidity\nfunction withdraw() external {\n    \u002F\u002F 1. CHECKS — проверки\n    uint256 amount = balances[msg.sender];\n    require(amount > 0);\n\n    \u002F\u002F 2. EFFECTS — изменение состояния\n    balances[msg.sender] = 0;  \u002F\u002F Обнуляем ДО внешнего вызова\n\n    \u002F\u002F 3. INTERACTIONS — внешние вызовы\n    (bool success, ) = msg.sender.call{value: amount}(\"\");\n    require(success);\n}\n```\n\nТеперь при повторном входе `balances[msg.sender]` уже равен нулю, и `require(amount > 0)` не пройдёт.\n\n### Мьютекс (ReentrancyGuard)\n\nДополнительный слой защиты — мьютекс-блокировка:\n\n```solidity\ncontract ReentrancyGuard {\n    uint256 private _status;\n    uint256 private constant NOT_ENTERED = 1;\n    uint256 private constant ENTERED = 2;\n\n    modifier nonReentrant() {\n        require(_status != ENTERED, \"ReentrancyGuard: reentrant call\");\n        _status = ENTERED;\n        _;\n        _status = NOT_ENTERED;\n    }\n}\n```\n\nПочему `_status` инициализируется как 1, а не 0? Потому что запись 0->1 (SSTORE нулевое->ненулевое) стоит 20000 газа, а запись 1->2 (ненулевое->ненулевое) стоит только 2900 газа. Использование 1\u002F2 вместо 0\u002F1 экономит ~17000 газа при первом вызове.\n\n### Транзиентное хранилище для блокировки\n\nС EIP-1153 блокировка реентрабельности стала намного дешевле:\n\n```solidity\nmodifier nonReentrant() {\n    assembly {\n        if tload(0) { revert(0, 0) }\n        tstore(0, 1)\n    }\n    _;\n    assembly {\n        tstore(0, 0)\n    }\n}\n\u002F\u002F Стоимость: ~200 газа вместо ~6000 газа\n```\n\n## Read-Only Reentrancy\n\nОтносительно новый вектор атаки, затрагивающий протоколы, которые полагаются на view-функции других контрактов для определения цен или состояния:\n\n```solidity\n\u002F\u002F Пул ликвидности\ncontract Pool {\n    function getPrice() external view returns (uint256) {\n        return totalAssets \u002F totalShares; \u002F\u002F Зависит от состояния\n    }\n\n    function withdraw(uint256 shares) external {\n        uint256 assets = shares * totalAssets \u002F totalShares;\n        totalShares -= shares;\n        \u002F\u002F ВНЕШНИЙ ВЫЗОВ — состояние временно неконсистентно\n        \u002F\u002F totalShares уменьшено, но totalAssets ещё нет\n        token.transfer(msg.sender, assets);\n        totalAssets -= assets; \u002F\u002F Обновление после вызова\n    }\n}\n\n\u002F\u002F Атакующий вызывает Pool.getPrice() из receive()\n\u002F\u002F В этот момент totalShares уменьшено, а totalAssets — нет\n\u002F\u002F getPrice() вернёт завышенную цену!\n```\n\nЗащита: используйте паттерн CEI и для view-функций применяйте блокировку реентрабельности.\n\n## Переполнение и недополнение\n\nДо Solidity 0.8 арифметические операции не проверяли переполнение:\n\n```solidity\n\u002F\u002F Solidity \u003C 0.8\nuint8 x = 255;\nx += 1; \u002F\u002F x == 0 (переполнение!)\n\nuint8 y = 0;\ny -= 1; \u002F\u002F y == 255 (недополнение!)\n```\n\nС Solidity 0.8+ все арифметические операции автоматически проверяются. Но если вы используете `unchecked` для оптимизации газа, будьте осторожны:\n\n```solidity\n\u002F\u002F Безопасно: i никогда не переполнится при разумной длине массива\nfor (uint256 i; i \u003C arr.length;) {\n    \u002F\u002F ...\n    unchecked { ++i; }\n}\n\n\u002F\u002F ОПАСНО: пользовательский ввод в unchecked\nunchecked {\n    uint256 result = userInput - fee; \u002F\u002F Может быть отрицательным!\n}\n```\n\n## Заключение\n\nБезопасность смарт-контрактов строится на трёх столпах: понимание контекста вызова (msg.sender vs tx.origin), правильный контроль доступа (RBAC, Ownable) и защита от реентрабельности (CEI, мьютекс, транзиентное хранилище). Каждая из этих концепций имеет прямое представление на уровне опкодов EVM.\n\nВ следующей статье мы начнём изучать Yul — ассемблерный язык Solidity, который даёт прямой доступ к опкодам EVM.","\u003Ch2 id=\"\">Безопасность начинается с понимания контекста\u003C\u002Fh2>\n\u003Cp>Безопасность смарт-контрактов — это не набор правил, которые вы запоминаете. Это глубокое понимание того, как EVM обрабатывает вызовы, кто является вызывающим, и что происходит, когда один контракт вызывает другой. Большинство критических уязвимостей в истории Ethereum были вызваны непониманием этих фундаментальных механизмов.\u003C\u002Fp>\n\u003Ch2 id=\"msg-sender-vs-tx-origin\">msg.sender vs tx.origin\u003C\u002Fh2>\n\u003Cp>На уровне опкодов:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>CALLER\u003C\u002Fstrong> (msg.sender) — адрес, непосредственно вызвавший текущий контракт. Если контракт A вызывает контракт B, то внутри B \u003Ccode>msg.sender == address(A)\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>ORIGIN\u003C\u002Fstrong> (tx.origin) — адрес EOA (внешнего аккаунта), инициировавшего транзакцию. Остаётся неизменным через все уровни вложенных вызовов.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Пользователь -&gt; Контракт A -&gt; Контракт B -&gt; Контракт C\n\u002F\u002F Внутри C:\n\u002F\u002F msg.sender = address(B)   \u002F\u002F непосредственный вызывающий\n\u002F\u002F tx.origin  = Пользователь \u002F\u002F инициатор транзакции\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Почему tx.origin опасен\u003C\u002Fh3>\n\u003Cp>Использование \u003Ccode>tx.origin\u003C\u002Fcode> для авторизации — классическая уязвимость:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F УЯЗВИМЫЙ контракт\ncontract VulnerableWallet {\n    address public owner;\n\n    function transfer(address to, uint256 amount) external {\n        require(tx.origin == owner); \u002F\u002F ОПАСНО!\n        payable(to).transfer(amount);\n    }\n}\n\n\u002F\u002F Атака:\n\u002F\u002F 1. Злоумышленник создаёт контракт-приманку\n\u002F\u002F 2. Владелец взаимодействует с приманкой (например, \"получить бесплатные NFT\")\n\u002F\u002F 3. Приманка вызывает VulnerableWallet.transfer(attacker, balance)\n\u002F\u002F 4. tx.origin == owner проходит, потому что владелец инициировал транзакцию!\n\u002F\u002F 5. Средства украдены\n\ncontract Attacker {\n    VulnerableWallet wallet;\n\n    function attack() external {\n        wallet.transfer(msg.sender, address(wallet).balance);\n    }\n\n    \u002F\u002F Владелец вызывает эту \"безобидную\" функцию\n    function claimFreeNFT() external {\n        \u002F\u002F ... якобы логика NFT ...\n        this.attack(); \u002F\u002F tx.origin всё ещё == владелец\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Всегда используйте \u003Ccode>msg.sender\u003C\u002Fcode> для авторизации. Единственное безопасное применение \u003Ccode>tx.origin\u003C\u002Fcode> — проверка, что вызывающий является EOA (не контрактом): \u003Ccode>require(msg.sender == tx.origin)\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"\">Паттерны контроля доступа\u003C\u002Fh2>\n\u003Ch3>Простой Ownable\u003C\u002Fh3>\n\u003Cp>Простейший паттерн — единственный владелец:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract Ownable {\n    address public owner;\n    error NotOwner();\n\n    modifier onlyOwner() {\n        if (msg.sender != owner) revert NotOwner();\n        _;\n    }\n\n    constructor() {\n        owner = msg.sender;\n    }\n\n    function transferOwnership(address newOwner) external onlyOwner {\n        owner = newOwner;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>На уровне EVM \u003Ccode>onlyOwner\u003C\u002Fcode> компилируется примерно так:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>CALLER           \u002F\u002F msg.sender на стек\nSLOAD(0)         \u002F\u002F owner из хранилища\nEQ               \u002F\u002F сравнение\nPUSH2 continue   \u002F\u002F адрес продолжения\nJUMPI            \u002F\u002F если равны — продолжить\nREVERT           \u002F\u002F иначе — откат\ncontinue:\nJUMPDEST\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Role-Based Access Control (RBAC)\u003C\u002Fh3>\n\u003Cp>Для более сложных систем используется ролевой контроль доступа:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract AccessControl {\n    mapping(bytes32 =&gt; mapping(address =&gt; bool)) private _roles;\n\n    bytes32 public constant ADMIN_ROLE = keccak256(\"ADMIN\");\n    bytes32 public constant MINTER_ROLE = keccak256(\"MINTER\");\n    bytes32 public constant PAUSER_ROLE = keccak256(\"PAUSER\");\n\n    modifier onlyRole(bytes32 role) {\n        require(_roles[role][msg.sender], \"AccessControl: unauthorized\");\n        _;\n    }\n\n    function grantRole(bytes32 role, address account) external onlyRole(ADMIN_ROLE) {\n        _roles[role][account] = true;\n    }\n\n    function revokeRole(bytes32 role, address account) external onlyRole(ADMIN_ROLE) {\n        _roles[role][account] = false;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>В хранилище проверка \u003Ccode>_roles[role][msg.sender]\u003C\u002Fcode> вычисляется как:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>slot = keccak256(msg.sender . keccak256(role . base_slot))\nSLOAD(slot) \u002F\u002F 2100 газа (холодный) или 100 газа (тёплый)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Реентрабельность: самая опасная уязвимость\u003C\u002Fh2>\n\u003Cp>Реентрабельность (reentrancy) — уязвимость, при которой внешний вызов позволяет вызываемому контракту повторно войти в вызывающий контракт до завершения текущего выполнения.\u003C\u002Fp>\n\u003Ch3>Классический пример: DAO Hack\u003C\u002Fh3>\n\u003Cp>Эта уязвимость стала причиной взлома The DAO в 2016 году (украдено ~$60M), что привело к хардфорку Ethereum.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F УЯЗВИМЫЙ контракт\ncontract VulnerableVault {\n    mapping(address =&gt; uint256) public balances;\n\n    function deposit() external payable {\n        balances[msg.sender] += msg.value;\n    }\n\n    function withdraw() external {\n        uint256 amount = balances[msg.sender];\n        require(amount &gt; 0);\n\n        \u002F\u002F ОПАСНО: внешний вызов ДО обновления состояния\n        (bool success, ) = msg.sender.call{value: amount}(\"\");\n        require(success);\n\n        \u002F\u002F Это выполнится только после возврата из call\n        \u002F\u002F Но к этому моменту атакующий уже вызвал withdraw повторно!\n        balances[msg.sender] = 0;\n    }\n}\n\n\u002F\u002F Контракт атакующего\ncontract Attacker {\n    VulnerableVault vault;\n\n    function attack() external payable {\n        vault.deposit{value: 1 ether}();\n        vault.withdraw();\n    }\n\n    \u002F\u002F Вызывается при получении ETH\n    receive() external payable {\n        if (address(vault).balance &gt;= 1 ether) {\n            vault.withdraw(); \u002F\u002F Повторный вход!\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Последовательность вызовов:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Attacker.attack()\n  -&gt; Vault.withdraw()\n    -&gt; Attacker.receive() (получает 1 ETH)\n      -&gt; Vault.withdraw() (balances[attacker] всё ещё &gt; 0!)\n        -&gt; Attacker.receive() (получает ещё 1 ETH)\n          -&gt; ... (повторяется, пока в Vault есть ETH)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Паттерн Checks-Effects-Interactions (CEI)\u003C\u002Fh3>\n\u003Cp>Главный защитный паттерн от реентрабельности:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function withdraw() external {\n    \u002F\u002F 1. CHECKS — проверки\n    uint256 amount = balances[msg.sender];\n    require(amount &gt; 0);\n\n    \u002F\u002F 2. EFFECTS — изменение состояния\n    balances[msg.sender] = 0;  \u002F\u002F Обнуляем ДО внешнего вызова\n\n    \u002F\u002F 3. INTERACTIONS — внешние вызовы\n    (bool success, ) = msg.sender.call{value: amount}(\"\");\n    require(success);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Теперь при повторном входе \u003Ccode>balances[msg.sender]\u003C\u002Fcode> уже равен нулю, и \u003Ccode>require(amount &gt; 0)\u003C\u002Fcode> не пройдёт.\u003C\u002Fp>\n\u003Ch3>Мьютекс (ReentrancyGuard)\u003C\u002Fh3>\n\u003Cp>Дополнительный слой защиты — мьютекс-блокировка:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract ReentrancyGuard {\n    uint256 private _status;\n    uint256 private constant NOT_ENTERED = 1;\n    uint256 private constant ENTERED = 2;\n\n    modifier nonReentrant() {\n        require(_status != ENTERED, \"ReentrancyGuard: reentrant call\");\n        _status = ENTERED;\n        _;\n        _status = NOT_ENTERED;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Почему \u003Ccode>_status\u003C\u002Fcode> инициализируется как 1, а не 0? Потому что запись 0-&gt;1 (SSTORE нулевое-&gt;ненулевое) стоит 20000 газа, а запись 1-&gt;2 (ненулевое-&gt;ненулевое) стоит только 2900 газа. Использование 1\u002F2 вместо 0\u002F1 экономит ~17000 газа при первом вызове.\u003C\u002Fp>\n\u003Ch3>Транзиентное хранилище для блокировки\u003C\u002Fh3>\n\u003Cp>С EIP-1153 блокировка реентрабельности стала намного дешевле:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">modifier nonReentrant() {\n    assembly {\n        if tload(0) { revert(0, 0) }\n        tstore(0, 1)\n    }\n    _;\n    assembly {\n        tstore(0, 0)\n    }\n}\n\u002F\u002F Стоимость: ~200 газа вместо ~6000 газа\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"read-only-reentrancy\">Read-Only Reentrancy\u003C\u002Fh2>\n\u003Cp>Относительно новый вектор атаки, затрагивающий протоколы, которые полагаются на view-функции других контрактов для определения цен или состояния:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Пул ликвидности\ncontract Pool {\n    function getPrice() external view returns (uint256) {\n        return totalAssets \u002F totalShares; \u002F\u002F Зависит от состояния\n    }\n\n    function withdraw(uint256 shares) external {\n        uint256 assets = shares * totalAssets \u002F totalShares;\n        totalShares -= shares;\n        \u002F\u002F ВНЕШНИЙ ВЫЗОВ — состояние временно неконсистентно\n        \u002F\u002F totalShares уменьшено, но totalAssets ещё нет\n        token.transfer(msg.sender, assets);\n        totalAssets -= assets; \u002F\u002F Обновление после вызова\n    }\n}\n\n\u002F\u002F Атакующий вызывает Pool.getPrice() из receive()\n\u002F\u002F В этот момент totalShares уменьшено, а totalAssets — нет\n\u002F\u002F getPrice() вернёт завышенную цену!\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Защита: используйте паттерн CEI и для view-функций применяйте блокировку реентрабельности.\u003C\u002Fp>\n\u003Ch2 id=\"\">Переполнение и недополнение\u003C\u002Fh2>\n\u003Cp>До Solidity 0.8 арифметические операции не проверяли переполнение:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Solidity &lt; 0.8\nuint8 x = 255;\nx += 1; \u002F\u002F x == 0 (переполнение!)\n\nuint8 y = 0;\ny -= 1; \u002F\u002F y == 255 (недополнение!)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>С Solidity 0.8+ все арифметические операции автоматически проверяются. Но если вы используете \u003Ccode>unchecked\u003C\u002Fcode> для оптимизации газа, будьте осторожны:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Безопасно: i никогда не переполнится при разумной длине массива\nfor (uint256 i; i &lt; arr.length;) {\n    \u002F\u002F ...\n    unchecked { ++i; }\n}\n\n\u002F\u002F ОПАСНО: пользовательский ввод в unchecked\nunchecked {\n    uint256 result = userInput - fee; \u002F\u002F Может быть отрицательным!\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Заключение\u003C\u002Fh2>\n\u003Cp>Безопасность смарт-контрактов строится на трёх столпах: понимание контекста вызова (msg.sender vs tx.origin), правильный контроль доступа (RBAC, Ownable) и защита от реентрабельности (CEI, мьютекс, транзиентное хранилище). Каждая из этих концепций имеет прямое представление на уровне опкодов EVM.\u003C\u002Fp>\n\u003Cp>В следующей статье мы начнём изучать Yul — ассемблерный язык Solidity, который даёт прямой доступ к опкодам EVM.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.404390Z","Как работают msg.sender, tx.origin и контроль доступа на уровне EVM. Реентрабельность, паттерн CEI и современные защитные механизмы.","EVM безопасность реентрабельность",null,"index, follow",[21,26,30],{"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-000000000013","Security","security",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000014","Solidity","solidity","Блокчейн",[36,43,49],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":41,"published_at":42},"d0200000-0000-0000-0000-000000000013","Почему Бали становится хабом импакт-технологий Юго-Восточной Азии в 2026 году","pochemu-bali-stanovitsya-khabom-impakt-tekhnologiy-2026","Бали занимает 16-е место среди стартап-экосистем Юго-Восточной Азии. Растущая концентрация Web3-разработчиков, ИИ-стартапов в области устойчивого развития и компаний в сфере эко-тревел-технологий формирует нишу столицы импакт-технологий региона.","Инженерия","2026-03-28T10:44:37.953039Z",{"id":44,"title":45,"slug":46,"excerpt":47,"locale":12,"category_name":41,"published_at":48},"d0200000-0000-0000-0000-000000000012","Защита данных в ASEAN: чек-лист разработчика для мультистранового комплаенса","zashchita-dannykh-asean-chek-list-razrabotchika-komplaens","Семь стран ASEAN имеют собственные законы о защите данных с разными моделями согласия, требованиями к локализации и штрафами. Практический чек-лист для разработчиков мультистрановых приложений.","2026-03-28T10:44:37.944001Z",{"id":50,"title":51,"slug":52,"excerpt":53,"locale":12,"category_name":41,"published_at":54},"d0200000-0000-0000-0000-000000000011","Цифровая трансформация Индонезии на $29 миллиардов: возможности для софтверных компаний","tsifrovaya-transformatsiya-indonezii-29-milliardov-vozmozhnosti-dlya-kompaniy","Рынок IT-услуг Индонезии вырастет с $24,37 млрд в 2025 году до $29,03 млрд в 2026 году. Облачная инфраструктура, искусственный интеллект, электронная коммерция и дата-центры обеспечивают самый быстрый рост в Юго-Восточной Азии.","2026-03-28T10:44:37.917095Z",{"id":13,"name":56,"slug":57,"bio":58,"photo_url":18,"linkedin":18,"role":59,"created_at":60,"updated_at":60},"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"]