Контроль доступа и tx.origin
Improper access control — взломщик №1 в смарт-контрактах. Чаще всего дыра — это просто забытая или неверная проверка «кто вызвал».
Использовать tx.origin для авторизации — всё равно что пускать в дом любого, кого привёл ваш знакомый. Достаточно одной фишинговой ссылки.
Контроль доступа решает, кому можно вызывать опасные функции: минт, вывод средств, смену параметров. Базовый паттерн — Ownable: один владелец и модификатор onlyOwner. Для более сложных систем — ролевой доступ (AccessControl): роли вроде MINTER_ROLE, PAUSER_ROLE, выдаваемые конкретным адресам.
msg.sender против tx.origin
Ключевое различие. msg.sender — непосредственный вызывающий (кто прямо сейчас обратился к контракту). tx.origin — самый первый отправитель всей цепочки вызовов (всегда EOA). Авторизация по tx.origin уязвима: если владелец зайдёт на вредоносный контракт и тот вызовет ваш контракт, tx.origin останется адресом владельца, и проверка ложно пройдёт.
ФИШИНГ ЧЕРЕЗ tx.origin
======================
Владелец -> вызывает Evil.attack()
|
| Evil вызывает Victim.withdraw()
v
Victim проверяет tx.origin == owner -> ЭТО ВЛАДЕЛЕЦ! (обман)
проверка msg.sender == owner -> это Evil, REVERT (правильно)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Treasury is AccessControl {
bytes32 public constant MANAGER = keccak256("MANAGER");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MANAGER, msg.sender);
}
// ПРАВИЛЬНО: msg.sender + ролевая проверка
function payout(address to, uint256 amount) external onlyRole(MANAGER) {
// ... выплата ...
}
// НИКОГДА: require(tx.origin == owner) — уязвимо к фишингу
}
Как работает под капотом (EVM/газ)
В стеке вызовов msg.sender меняется на каждом «прыжке» (контракт A зовёт B — для B msg.sender это A). А tx.origin один на всю транзакцию — это исходный EOA. Поэтому проверка по tx.origin «не видит» промежуточный вредоносный контракт. Ролевой AccessControl хранит маппинг «роль → адрес → есть ли» и проверяет его в модификаторе onlyRole — это обычные дешёвые чтения storage.
# Та же логика на Python: msg.sender надёжен, tx.origin обманывают
def victim_withdraw(msg_sender, tx_origin, owner, mode):
if mode == "tx.origin":
return tx_origin == owner # УЯЗВИМО
return msg_sender == owner # БЕЗОПАСНО
owner = "0xOWNER"
# фишинг: владелец дернул Evil, Evil дернул Victim
# msg.sender == Evil, но tx.origin == owner
print("tx.origin проверка пропускает Evil?",
victim_withdraw("0xEVIL", owner, owner, "tx.origin")) # True — плохо
print("msg.sender проверка пропускает Evil?",
victim_withdraw("0xEVIL", owner, owner, "msg.sender")) # False — хорошо
«Та же логика на Python ▶». Проверка tx.origin ложно признаёт владельцем вредоносный контракт-посредник; msg.sender видит реального вызывающего и отклоняет его.
Частые ошибки и уязвимости
- Авторизация по
tx.origin— открывает фишинг через промежуточный контракт. - Забытый модификатор доступа на функции минта/вывода/настройки — кто угодно её вызовет.
- Один всемогущий владелец без таймлока — точка отказа и риск, если ключ скомпрометирован.
Best practices
- Авторизуйте только по
msg.sender, никогда поtx.origin. - Применяйте принцип наименьших привилегий и ролевой доступ (
AccessControl) вместо одного владельца, где это уместно. - Для критичных прав используйте мультисиг и таймлок, чтобы один ключ не решал всё.
Двухшаговая передача владения и таймлок
Даже корректная проверка msg.sender не спасает от человеческой ошибки. Классическая беда — передача владения: если просто записать нового владельца, опечатка в адресе навсегда заблокирует все административные функции, ведь исправить уже некому. Поэтому используют двухшаговую передачу (паттерн Ownable2Step у OpenZeppelin): текущий владелец предлагает нового, а тот должен явно принять владение отдельной транзакцией. Опечатка просто не будет принята, и контроль останется у прежнего владельца. Для критичных параметров протокола добавляют таймлок: изменение не применяется сразу, а ставится в очередь с задержкой (например, 48 часов), за которую сообщество успевает заметить подозрительное действие и среагировать. Вместе с мультисигом (когда для действия нужны подписи нескольких ключей) это превращает «всемогущего владельца» из единой точки отказа в управляемый, прозрачный процесс.
Итоги
Контроль доступа — главный фронт безопасности. Проверяйте msg.sender, дробите права по ролям и не доверяйте tx.origin. Дальше — об арифметике, oracle-зависимостях и финальном чеклисте.