Post

Foundry > VM. Cheatcode

Foundry > VM. Cheatcode

Foundry Test 라이브러리 vm. Cheatcode 정리

1. 전체 요약

Foundry에서 vm.Cheatcode를 호출할 때 사용하는 객체다.

Cheatcode란 실제 Solidity/EVM에는 존재하지 않지만, Foundry 테스트 환경에서만 사용할 수 있는 특수 기능이다.

주로 다음과 같은 작업을 할 때 사용한다.

  • 특정 주소로 msg.sender 조작
  • 테스트 계정에 ETH 지급
  • revert 발생 여부 검증
  • block timestamp / block number 변경
  • storage slot 직접 읽기/쓰기
  • 메인넷/테스트넷 fork 생성
  • 환경변수 읽기
  • 배포 스크립트에서 트랜잭션 broadcast

즉, vm.은 스마트 컨트랙트 테스트를 위해 EVM 상태를 강제로 조작하거나 검증하는 도구라고 보면 된다.


2. 기본 사용 방식

Foundry 테스트 파일에서는 보통 forge-std/Test.sol을 import하고 Test를 상속한다.

import "forge-std/Test.sol";

contract MyTest is Test {
    function testSomething() public {
        vm.prank(address(1));
        // 다음 호출의 msg.sender가 address(1)이 됨
    }
}

여기서 vm.prank()처럼 vm. 뒤에 붙는 함수들이 Foundry Cheatcode다.


3. 자주 사용하는 vm. 옵션

3.1 vm.prank()

역할

다음 한 번의 외부 호출에서 msg.sender를 특정 주소로 바꾼다.

문법

vm.prank(address);

예시

address alice = makeAddr("alice");

vm.prank(alice);
vault.deposit{value: 1 ether}();

설명

위 코드는 vault.deposit()을 호출할 때 msg.sender가 테스트 컨트랙트가 아니라 alice가 되도록 만든다.

즉, 실제로는 테스트 컨트랙트가 호출하지만, 컨트랙트 입장에서는 alice가 호출한 것처럼 보인다.

자주 쓰는 상황

  • 특정 사용자가 함수를 호출하는 상황 테스트
  • owner가 아닌 사용자의 접근 테스트
  • 공격자 주소로 함수 호출 테스트

3.2 vm.startPrank() / vm.stopPrank()

역할

여러 번의 호출 동안 msg.sender를 특정 주소로 유지한다.

문법

vm.startPrank(address);
...
vm.stopPrank();

예시

address alice = makeAddr("alice");

vm.startPrank(alice);

vault.deposit{value: 1 ether}();
vault.withdraw(0.5 ether);

vm.stopPrank();

설명

vm.prank()는 다음 호출 1번에만 적용된다.

반면 vm.startPrank()vm.stopPrank()가 호출되기 전까지 계속 적용된다.

차이점

vm.prank(alice);
target.callOne(); // alice로 호출됨
target.callTwo(); // 다시 테스트 컨트랙트가 호출한 것으로 처리됨
vm.startPrank(alice);
target.callOne(); // alice로 호출됨
target.callTwo(); // alice로 호출됨
vm.stopPrank();

자주 쓰는 상황

  • 한 사용자가 여러 행동을 연속으로 수행하는 테스트
  • deposit 후 withdraw 테스트
  • approve 후 transferFrom 테스트

3.3 vm.deal()

역할

특정 주소에 ETH 잔액을 넣는다.

문법

vm.deal(address, amount);

예시

address alice = makeAddr("alice");

vm.deal(alice, 10 ether);

설명

테스트에서 만든 주소는 기본적으로 ETH가 없다.

따라서 payable 함수에 ETH를 보내려면 먼저 vm.deal()로 잔액을 넣어줘야 한다.

실전 예시

function testDeposit() public {
    address alice = makeAddr("alice");

    vm.deal(alice, 1 ether);

    vm.prank(alice);
    vault.deposit{value: 1 ether}();

    assertEq(vault.balances(alice), 1 ether);
}

흐름

1
2
3
4
5
1. alice 주소 생성
2. alice에게 1 ETH 지급
3. alice가 호출한 것처럼 msg.sender 조작
4. alice가 1 ETH deposit
5. vault 내부 balance 확인

3.4 vm.expectRevert()

역할

다음 호출이 revert 되는지 검증한다.

문법

vm.expectRevert();

또는 revert 메시지를 지정할 수도 있다.

vm.expectRevert("Not owner");

custom error를 검증할 수도 있다.

vm.expectRevert(MyError.selector);

예시

address attacker = makeAddr("attacker");

vm.prank(attacker);
vm.expectRevert("Not owner");
target.withdraw();

설명

위 코드는 attackerwithdraw()를 호출했을 때 "Not owner" 메시지와 함께 revert 되어야 테스트가 통과한다.

만약 revert가 발생하지 않으면 테스트는 실패한다.

주의점

vm.expectRevert()바로 다음 external call에 적용된다.

vm.expectRevert();
target.someFunction();

이런 식으로 바로 다음 줄에 검증하려는 호출을 두는 것이 좋다.


3.5 vm.warp()

역할

block.timestamp 값을 변경한다.

문법

vm.warp(newTimestamp);

예시

vm.warp(block.timestamp + 7 days);
staking.withdraw();

설명

staking, vesting, timelock, cooldown처럼 시간이 중요한 컨트랙트를 테스트할 때 사용한다.

자주 쓰는 상황

  • 7일 후 출금 가능 여부 테스트
  • lock 기간 이후 claim 테스트
  • deadline 만료 테스트

3.6 vm.roll()

역할

block.number 값을 변경한다.

문법

vm.roll(newBlockNumber);

예시

vm.roll(block.number + 100);

설명

블록 번호 기반 로직을 테스트할 때 사용한다.

자주 쓰는 상황

  • 특정 블록 이후 실행 가능
  • 블록 번호 기반 보상 계산
  • governance voting delay 테스트

3.7 vm.load()

역할

특정 컨트랙트의 storage slot 값을 직접 읽는다.

문법

bytes32 value = vm.load(address target, bytes32 slot);

예시

bytes32 slot = bytes32(uint256(0));
bytes32 value = vm.load(address(target), slot);

설명

Solidity의 storage는 slot 단위로 저장된다.

vm.load()를 사용하면 getter 함수가 없어도 특정 slot 값을 직접 읽을 수 있다.

자주 쓰는 상황

  • proxy storage 확인
  • EIP-1967 implementation slot 확인
  • storage collision 분석
  • Ethernaut PuzzleWallet류 문제 분석

3.8 vm.store()

역할

특정 컨트랙트의 storage slot 값을 강제로 변경한다.

문법

vm.store(address target, bytes32 slot, bytes32 value);

예시

bytes32 slot = bytes32(uint256(0));
bytes32 newValue = bytes32(uint256(123));

vm.store(address(target), slot, newValue);

설명

vm.store()는 컨트랙트 내부 로직을 거치지 않고 storage 값을 직접 바꾼다.

그래서 일반적인 기능 테스트보다는 보안 테스트, storage 분석, 특수 상황 재현에 많이 사용된다.

주의점

실제 블록체인에서는 이런 식으로 storage를 마음대로 바꿀 수 없다.

테스트 환경에서만 가능한 조작이다.


3.9 vm.createFork()

역할

메인넷이나 테스트넷 상태를 복사해서 로컬 테스트 환경에 fork를 만든다.

문법

uint256 forkId = vm.createFork(RPC_URL);

특정 블록 기준으로 fork할 수도 있다.

uint256 forkId = vm.createFork(RPC_URL, blockNumber);

예시

string memory rpc = vm.envString("RPC_URL");

uint256 fork = vm.createFork(rpc);
vm.selectFork(fork);

설명

실제 배포된 컨트랙트를 대상으로 테스트하고 싶을 때 사용한다.

예를 들어 이미 배포된 ERC20, DeFi protocol, proxy contract 상태를 그대로 가져와서 테스트할 수 있다.


3.10 vm.selectFork()

역할

여러 fork 중 현재 사용할 fork를 선택한다.

문법

vm.selectFork(forkId);

예시

uint256 mainnetFork = vm.createFork(MAINNET_RPC_URL);
uint256 sepoliaFork = vm.createFork(SEPOLIA_RPC_URL);

vm.selectFork(mainnetFork);

설명

여러 체인 또는 여러 블록 상태를 테스트할 때 사용한다.


3.11 vm.envString(), vm.envUint(), vm.envAddress()

역할

환경변수를 읽는다.

문법

vm.envString("RPC_URL");
vm.envUint("PK");
vm.envAddress("OWNER");

예시

string memory rpc = vm.envString("RPC_URL");
uint256 privateKey = vm.envUint("PK");
address owner = vm.envAddress("OWNER");

설명

.env 파일이나 터미널 환경변수에 저장된 값을 Solidity 테스트나 스크립트 안에서 읽을 때 사용한다.

자주 쓰는 상황

  • RPC URL 읽기
  • private key 읽기
  • 배포자 주소 읽기
  • 테스트 대상 컨트랙트 주소 읽기

3.12 vm.startBroadcast() / vm.stopBroadcast()

역할

배포 스크립트에서 실제 트랜잭션을 broadcast한다.

주로 Script.sol에서 사용한다.

문법

vm.startBroadcast(privateKey);
...
vm.stopBroadcast();

예시

import "forge-std/Script.sol";

contract Deploy is Script {
    function run() external {
        uint256 pk = vm.envUint("PK");

        vm.startBroadcast(pk);

        new MyContract();

        vm.stopBroadcast();
    }
}

설명

forge script를 사용해서 실제 네트워크에 컨트랙트를 배포할 때 사용한다.

테스트보다는 배포 자동화에서 더 자주 사용한다.


3.13 vm.expectEmit()

역할

이벤트가 정상적으로 발생하는지 검증한다.

문법

vm.expectEmit();

좀 더 세밀하게 topic/data 검증 여부를 지정할 수 있다.

vm.expectEmit(
    checkTopic1,
    checkTopic2,
    checkTopic3,
    checkData
);

예시

vm.expectEmit(true, true, false, true);
emit Transfer(alice, bob, 100);

token.transfer(bob, 100);

설명

위 코드는 token.transfer()를 호출했을 때 예상한 Transfer 이벤트가 발생하는지 검증한다.


3.14 vm.expectCall()

역할

특정 컨트랙트에 특정 call이 발생하는지 검증한다.

문법

vm.expectCall(target, data);

예시

vm.expectCall(
    address(token),
    abi.encodeWithSelector(token.transfer.selector, bob, 100)
);

vault.withdraw(bob, 100);

설명

vault.withdraw() 내부에서 token.transfer(bob, 100)이 호출되는지 검증할 수 있다.

자주 쓰는 상황

  • ERC20 transfer 호출 여부 확인
  • 외부 컨트랙트 call 검증
  • protocol 내부 상호작용 검증

4. console.logvm.의 차이

console.logvm. cheatcode가 아니다.

하지만 Foundry 테스트에서 자주 같이 사용한다.

import "forge-std/console.sol";

예시:

console.log("owner:", owner);
console.log("balance:", address(vault).balance);

console.log는 테스트 중 값을 확인하기 위한 디버깅 도구다.

반면 vm.은 EVM 상태를 조작하거나 검증하는 cheatcode다.


5. Vault 테스트 기준으로 꼭 알아야 할 것

Vault를 직접 구현하고 테스트하는 단계라면 우선 아래 4개만 제대로 익혀도 된다.

vm.deal(user, 1 ether);
vm.prank(user);
vm.expectRevert();
assertEq(actual, expected);

정리하면 다음과 같다.

기능사용 예시의미
ETH 지급vm.deal(alice, 1 ether)alice에게 테스트용 ETH 지급
호출자 변경vm.prank(alice)다음 호출의 msg.sender를 alice로 변경
실패 검증vm.expectRevert()다음 호출이 revert 되어야 함
값 검증assertEq(a, b)a와 b가 같은지 확인

6. Vault deposit 테스트 예시

function testDeposit() public {
    address alice = makeAddr("alice");

    vm.deal(alice, 1 ether);

    vm.prank(alice);
    vault.deposit{value: 1 ether}();

    assertEq(vault.balances(alice), 1 ether);
}

코드 설명

1
2
3
4
5
1. makeAddr("alice")로 테스트용 alice 주소를 만든다.
2. vm.deal(alice, 1 ether)로 alice에게 1 ETH를 준다.
3. vm.prank(alice)로 다음 호출의 msg.sender를 alice로 바꾼다.
4. alice가 vault.deposit{value: 1 ether}()를 호출한 것처럼 만든다.
5. vault.balances(alice)가 1 ether인지 assertEq로 확인한다.

7. Vault withdraw 실패 테스트 예시

function testWithdrawRevertIfInsufficientBalance() public {
    address alice = makeAddr("alice");

    vm.prank(alice);
    vm.expectRevert();
    vault.withdraw(1 ether);
}

코드 설명

1
2
3
4
1. alice 주소를 만든다.
2. alice가 호출한 것처럼 msg.sender를 바꾼다.
3. 다음 호출이 revert 되어야 한다고 지정한다.
4. alice는 deposit한 ETH가 없으므로 withdraw(1 ether)는 실패해야 한다.
This post is licensed under CC BY 4.0 by the author.