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.
Проверьте себя
1. Что такое tx.origin?
AПрямой вызывающий функции
BАдрес, инициировавший всю транзакцию (всегда EOA)
CАдрес контракта
DАдрес валидатора
2. Почему авторизация require(tx.origin == owner) опасна?
AОна тратит лишний газ
BВладельца можно заманить вызвать контракт-приманку, и проверка пройдёт
Ctx.origin всегда равен нулю
DЭто не компилируется
3. Что использовать для контроля доступа?
Atx.origin
Bmsg.sender
Cblock.timestamp
Dblockhash