tx.origin 은 트랜잭션을 보낸 사람의 주소 값을 돌려주는 솔리디티의 전역 함수이다.
그림으로 설명하면,
A 유저가 B 컨트랙트를 거쳐 C 컨트랙트의 함수를 호출했을때,
C 컨트랙트의 msg.sender
는 B 이다.
A 유저가 B 컨트랙트를 거쳐 C 컨트랙트의 함수를 호출했을때,
C 컨트랙트의 tx.origin
은 A 이다.
아래의 예시 코드를 보면..
contract MyWallet {
address owner;
constructor() {
owner = msg.sender;
}
receive() external payable {}
function deposit() public payable {
payable(this).transfer(msg.value);
}
function withdraw(address _to) public payable {
require(owner == tx.origin,"Owner only.");
payable(_to).transfer(address(this).balance);
}
}
MyWallet 컨트랙트를 배포할 때 owner 를 설정해두었고,
withdraw 함수로 원하는 주소로 이더를 보낼 수 있도록 설정해두었다.
require(owner == tx.origin,"Owner only.");
이 부분도 단순히 보았을 땐 문제가 없다.
다른 주소가 출금을 시도한다면, owner 주소와 일치하지 않기에 당연히 revert 될 것이고, 보안에는 문제가 없어보인다.
하지만, 겉보기와는 다르게 유저의 행동에 따라 해킹당할 여지가 있다.
다음은 피싱공격을 시도하는 Attack 컨트랙트의 예시이다.
contract Attack {
MyWallet mywallet;
address owner;
constructor(MyWallet _MyWallet) {
mywallet = MyWallet(_MyWallet);
owner = msg.sender;
}
receive() external payable {}
function attack() public {
mywallet.withdraw(owner);
}
}
해커는 MyWallet 컨트랙트가 tx.origin 이라는 require 조건문을 사용한다는 것을 알았고, 위의 Attack 컨트랙트를 배포한다.
또한, MyWallet 을 사용하는 웹사이트를 클론해 자신의 Attack 컨트랙트를 사용하도록 사용자들을 속인다.
속은 사용자들은 Attack 컨트랙트를 사용할 것이고, 아래와 같은 그림대로 흘러갈 것이다.
방어 방법은 간단하지만, tx.origin
을 조건문으로 사용하지 않는 것이다. require(msg.sender == owner)
와 같이 사용하면 된다.
tx.origin
은 특별한 경우에 사용한다. 예를 들어, 컨트랙트가 함수를 호출하지 못하도록 할때 tx.origin == msg.sender
와 같은 방법으로 사용하면 EOA 만이 함수를 호출할 수 있게 해준다.
tx.origin 으로 조건문을 설정한다면 내가 만든 서비스의 동작에는 문제가 없을 수 있지만, 해커의 입장에서는 좋은 먹잇감이 될 것이고, 사용자 또한 언제 피싱을 당할지 모르기에 항상 조심해야 하는 입장이 될 것이다.
따라서, 해킹당할 여지 자체를 남기지 않기 위해 항상 적절하게 컨트랙트를 작성하는 것이 좋겠다.