tx.origin против msg.sender
Две почти одинаковые переменные, одна из которых открывает дверь фишингу.
msg.sender — непосредственный вызывающий текущей функции (может быть контрактом). tx.origin — адрес, инициировавший всю транзакцию (всегда внешний аккаунт, EOA).
В чём разница
Когда пользователь Алиса вызывает контракт A, а тот вызывает контракт B, то внутри B: msg.sender == адрес A, но tx.origin == адрес Алисы. tx.origin «видит» начало цепочки, msg.sender — только последнее звено.
Алиса (EOA) --> Контракт A --> Контракт B
В контракте B:
msg.sender = A (кто позвал прямо сейчас)
tx.origin = Алиса (кто запустил всю транзакцию)Почему авторизация по tx.origin опасна
Соблазн: «пускать, если транзакцию запустил владелец» — require(tx.origin == owner). Проблема концептуальна: владельца можно обманом заставить вызвать вредоносный контракт-приманку (например, кликнуть на «безобидную» кнопку). Тогда вредоносный контракт от своего имени дёрнет защищённую функцию — и проверка tx.origin == owner пройдёт, ведь транзакцию всё ещё инициировал владелец. Атакующий действует «руками» жертвы.
// УЯЗВИМО: авторизация по инициатору транзакции
function withdrawAll() external {
require(tx.origin == owner); // владельца можно заманить в контракт-приманку
payable(tx.origin).transfer(address(this).balance);
}Правильно проверять непосредственного вызывающего — он должен быть именно владельцем, а не «где-то в начале цепочки был владелец»:
// БЕЗОПАСНО: проверяем прямого вызывающего
function withdrawAll() external {
require(msg.sender == owner);
payable(owner).transfer(address(this).balance);
}Как работает под капотом
EVM выставляет tx.origin один раз на всю транзакцию, а msg.sender — заново на каждом уровне вызова. Авторизация должна опираться на «кто прямо сейчас просит доступ», то есть на msg.sender. Использование tx.origin для контроля доступа считается явным антипаттерном; он допустим разве что для редких эвристик «вызвал ли это человек, а не контракт», да и те ненадёжны.
Частые ошибки
- require(tx.origin == ...) для авторизации — открывает фишинг через контракт-приманку.
- Путать «инициатора» и «прямого вызывающего». Для прав важен именно прямой вызывающий.
Итоги
msg.sender— прямой вызывающий;tx.origin— инициатор всей транзакции.- Авторизация по
tx.originуязвима: жертву заманивают в контракт-приманку. - Для контроля доступа всегда используйте
msg.sender.