Foundry

기존의 스마트컨트렉트 개발은 Truffle, hardhat 등의 툴을 사용하여 개발을 하였습니다. 특히 테스트를 위해선 Hardhat을 통하여 많이 진행했는데, Paradim에서 개발한 Foundry가 이러한 테스트에 새로운 대안으로 등장하였습니다.

Foundry는 쉽게 설명하자면 Solidity 언어를 Solidity로 테스트 가능하게 도와주는 툴입니다. 기존의 툴들을 통해 테스트를 진행하기 위해서는 테스트 코드를 Javascript 혹은 Typescript로 작성해야했기 때문에 많은 의존성과 config들이 필요하였습니다. C언어를 javascript로 테스트한다고 개발자들에게 말하면 비웃음을 살지도 모르지만, 지금까지 Solidty는 그 언어 자체를 이용해 테스트되지 못했습니다. 이러한 불편함의 예시로, 솔리디티를 자바스크립트에서 테스트할 경우엔 BigNumber 라이브러리를 써가며 숫자를 다루는 불편함이 있었습니다. 이러한 불편함과 혼란성을 Foundry가 해결해줍니다!

Foundry의 Forge를 통한 간단한 테스트 코드 예시는 아래와 같습니다.

contract Foo {
  uint256 public x = 1;
  function set(uint256 _x) external {
    x = _x;
  }

  function double() external {
    x = 2 * x;
  }
}

contract FooTest {
  Foo foo;

  // The state of the contract gets reset before each
  // test is run, with the setUp() function being called
  // each time after deployment. Think of this like a JavaScript
  // beforeEach block
  function setUp() public {
    foo = new Foo();
  }

  // A simple unit test
  function testDouble() public {
    require(foo.x() == 1);
    foo.double();
    require(foo.x() == 2);
  }

  // A failing unit test (function name starts with testFail)
  function testFailDouble() public {
    require(foo.x() == 1);
    foo.double();
    require(foo.x() == 4);
  }
}

다양한 테스트 기능 지원

테스트를 하다보면 생각하지도 못한 에러사항이 발생하게됩니다. 사람이 모든 테스트 영역을 커버하기는 쉽지 않으므로 무작위의 input을 통해 자동화된 테스트를 수행하는 퍼징 기능(무작위 데이터 입력)이 필요할 수 있습니다. 아래는 Foundry에서 지원하는 퍼징 기능의 한 예시입니다.

function testDoubleWithFuzzing(uint256 x) public {
  foo.set(x);
  require(foo.x() == x);
  foo.double();
  require(foo.x() == 4 * x);
}

위에서 x에 자동으로 여러 값을 대입해 테스트하고 에러가 발생할 시 카운터 예제 값인 x를 아래와같이 보여줍니다.

[FAIL. Counterexample: calldata=0x44735ef10000000000000000000000000000000000000000000000000000000000000001, args=[Uint(1)]] testDoubleWithFuzzingCounterExample (gas: [fuzztest])

이 외에도, VM 상태 값을 변경하는 기능과 라이브 네트워크의 상태를 반영한 테스트 기능들을 수행할 수 있습니다. 아래 예제는 block timestamp 값을 100으로 변경하여 테스트하는 예제입니다.

address constant CHEATCODE_ADDRESS = 0x7cFA93148B0B13d88c1DcE8880bd4e175fb0DeDF;

interace Vm {
  // Sets the block.timestamp to `x`.
  function warp(uint256 x) external;
}

contract MyTest {
  Vm vm = Vm(CHEATCODE_ADDRESS);

  function testWarp() public {
    vm.warp(100);
    require(block.timestamp == 100);
  }
}

위에 코드에서 보듯이, wrap() 함수를 통해 block timestamp 값을 변경하여 테스트가 가능합니다. 이 외에도 block diffculty, block number, slot 값 변경 및 읽기 등 다양한 기능들을 제공하며 더 자세한 사항은 Readme를 참고하시면 됩니다. 다른 개발툴처럼, 실시간 네트워크와의 연동은 node URL을 명시해줌으로써 쉽게 가능합니다.

forge test --fork-url <your node url> [--fork-block-number <the block number you want>].

이처럼 다양한 기능들을 지원하고 있기 때문에 많은 프로젝트들이 점차적으로 Typescript와 더불어 Foundry를 통한 테스트 코드를 작성하는 추세입니다. 저 같은 경우 TreasureDAO가 해당 툴을 통해 디버깅하는 것을 보고 접하게 되었습니다. 그럼 이제 어떻게 설치하는지 살펴봅시다.

Foundry 설치

Foundry의 Github에서 설치 관련 내용을 확인할 수 있습니다. 저는 Window의 WSL2의 우분투환경이므로 아래 명령어를 통해 설치해보도록 하겠습니다.

curl -L https://foundry.paradigm.xyz | bash

docker를 통해 설치할 경우엔 아래와 같은 명령어를 입력해주면 됩니다. foundry의 도커 이미지를 통한 자세한 사용법은 docker 섹션에서 확인할 수 있습니다.

docker pull ghcr.io/foundry-rs/foundry:latest

저처럼 Curl을 통해 다운로드할 경우 아래와 같은 결과가 나옵니다.

Drawing

메시지에 나온 것처럼, 추가적으로 source /home/사용자/.bashrc를 입력해준 후 foundryup 명령어를 입력해줍시다. 그럼 아래와 같이 정상적으로 설치가 완료됩니다.

Drawing

설치가 완료된 후에는 forge, cast, anvil 명령어를 command 창에 입력하여 help 사용법을 확인할 수 있습니다. 아래는 forge를 입력했을 때 출력되는 help 메시지들입니다.

Drawing

예제

그럼 이제 간단히 forge를 어떻게 사용하는지 살펴보도록 하겠습니다.

Reference

  1. Paradim’s Blog Post for Foundry
  2. Foundry Book