curl -L https://foundry.paradigm.xyz | bash
env 새로고침
foundryup
forge init [프로젝트명]
forge build # 컴파일
forge test # 테스트코드 실행 (함수명 ^test로 시작해야함)
forge install OpenZeppelin/openzeppelin-contracts # 외부 라이브러리 추가 / 뒤에 @v0.0.1 형태로 버전 명시 가능
forge remappings # 설치한 라이브러리 경로 재매핑
forge remove [라이브러리명]
forge clone [주소] [폴더명] # on-chain상에 존재하는 컨트랙트 코드 올라가 있는 것 클론
forge script script/[스크립트].s.sol # 배포 스크립트
<your_rpc_url> --private-key <your_private_key> src/MyContract.sol:MyContract # 스마트계약 배포
soldeer 패키지 dependencies 찾기
forge soldeer install 활용참고
--hh 옵션은 컴파일 결과물을 Hardhat과 호환되는 형식으로 생성하도록 해주는 옵션, --lib-paths node_modules --contracts contracts 와 함께 사용배포
:MyContract 형태로 특정 컨트랙트를 지정할 수 있다.--constructor-args flag 이용해서 생성자 인수 전달 가능// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {ERC20} from "solmate/tokens/ERC20.sol";
contract MyToken is ERC20 {
constructor(
string memory name,
string memory symbol,
uint8 decimals,
uint256 initialSupply
) ERC20(name, symbol, decimals) {
_mint(msg.sender, initialSupply);
}
}
아래와 같이 인수 넘겨주면 됨
$ forge create --rpc-url <your_rpc_url> \
--constructor-args "ForgeUSD" "FUSD" 18 1000000000000000000000 \
--private-key <your_private_key> \
--etherscan-api-key <your_etherscan_api_key> \
--verify \
src/MyToken.sol:MyToken
--verify 옵션을 통해 etherscan api등을 쓰는 경우, 검증된 컨트랙트임을 함께 푸쉬 하는 옵션Mulit-Chain Deploy
[rpc_endpoints]
sepolia = "${SEPOLIA_URL}"
base-sepolia = "${BASE_SEPOLIA_URL}"
[etherscan]
sepolia = { key = "${SEPOLIA_KEY}" }
base-sepolia = { key = "${BASE_SEPOLIA_KEY}" }
Script는 다음과 같이 작성해주면 된다. Sepolia Mainnet fork를 생성하여 컨트랙트 배포 및 검증하고, Base Sepolia로 체인을 전환해서 배포를 한다. Script는 다음과 같이 작성해주면 된다.
contract CounterScript is Script {
function run() public {
vm.createSelectFork("sepolia");
vm.startBroadcast();
new Counter();
vm.stopBroadcast();
vm.createSelectFork("base-sepolia");
vm.startBroadcast();
new Counter();
vm.stopBroadcast();
}
}
forge script script/CounterScript.s.sol --slow --multi --broadcast --private-key <your_private_key> --verify
이외에 필요에 따라 forge verify-contract 를 통해 이미 배포된 컨트랙트를 검증할 수 있다.
foundry 프로젝트의 test/ 경로에 위치하며, [파일명].t.sol 형태로 관리한다.
특정 test 컨트랙트 실행하고 싶을 때
forge test --match-contract [컨트랙트명] --match-test [^test함수]특정 test 컨트랙트 빼고 실행하고 싶을 때
forge test --no-match-contract [컨트랙트명] --no-match-test특정 test 스크립트 실행하고 싶을 때
forge test --match-path test/[파일명]특정 test 스크립트 빼고 실행하고 싶을 때
forge test --no-match-path test/[파일명]로그 레벨
-vv : Logs emitted during tests are also displayed. That includes assertion errors from tests, showing information such as expected vs actual.-vvv : Stack traces for failing tests are also displayed.-vvvv : Stack traces for all tests are displayed, and setup traces for failing tests are displayed.-vvvvv : Stack traces and setup traces are always displayed.watch 모드
forge test --watch : 테스트 실행 후에도 프로젝트 내 파일(스마트 컨트랙트 코드나 테스트 파일)의 변경 사항을 지속적으로 모니터링하여, 변경이 감지되면 자동으로 테스트를 다시 실행함수명
setUp() : 테스트 케이스 실행전 실행되는 생성자 느낌test*() : 성공해야하는 케이스testFail*() : 실패해야하는 케이스import
import {Test} from "forge-std/Test.sol";beforeTestSetup, Shared setups
CheatCodes
0x7109709ECfa91a80626fF3989D68f67F5b1DD12D 주소의 함수를 호출하는 것이랑 동일하다.vm 인스턴스를 통해 Cheatcode에 접근할 수 있다.Test Code
pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
error Unauthorized();
contract OwnerUpOnly {
address public immutable owner;
uint256 public count;
constructor() {
owner = msg.sender;
}
function increment() external {
if (msg.sender != owner) {
revert Unauthorized();
}
count++;
}
}
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
function setUp() public {
upOnly = new OwnerUpOnly();
}
function test_IncrementAsOwner() public {
assertEq(upOnly.count(), 0);
upOnly.increment();
assertEq(upOnly.count(), 1);
}
function test_RevertWhen_CallerIsNotOwner() public {
vm.startPrank(address(0x123));
vm.expectRevert(Unauthorized.selector);
upOnly.increment();
vm.stopPrank();
}
function test_RevertWhen_CallerIsNotOwner2() public {
vm.expectRevert(Unauthorized.selector);
vm.prank(address(0x0));
upOnly.increment();
vm.stopPrank();
}
}
실행 결과 PASS는 됐지만 Stack Trace를 보면 Revert가 된걸 확인할 수 있다.
foundry/counter [ forge test --match-path test/test1.t.sol -vvvv
[⠢] Compiling...
[⠑] Compiling 1 files with Solc 0.8.10
[⠃] Solc 0.8.10 finished in 421.70ms
Compiler run successful!
Ran 3 tests for test/test1.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 32537)
Traces:
[32537] OwnerUpOnlyTest::test_IncrementAsOwner()
├─ [2262] OwnerUpOnly::count() [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0) [staticcall]
│ └─ ← [Return]
├─ [20398] OwnerUpOnly::increment()
│ └─ ← [Stop]
├─ [262] OwnerUpOnly::count() [staticcall]
│ └─ ← [Return] 1
├─ [0] VM::assertEq(1, 1) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
[PASS] test_RevertWhen_CallerIsNotOwner() (gas: 9075)
Traces:
[9075] OwnerUpOnlyTest::test_RevertWhen_CallerIsNotOwner()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xc31eb0e0: 82b4290000000000000000000000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [247] OwnerUpOnly::increment()
│ └─ ← [Revert] Unauthorized()
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
[PASS] test_RevertWhen_CallerIsNotOwner2() (gas: 9018)
Traces:
[9018] OwnerUpOnlyTest::test_RevertWhen_CallerIsNotOwner2()
├─ [0] VM::expectRevert(custom error 0xc31eb0e0: 82b4290000000000000000000000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [247] OwnerUpOnly::increment()
│ └─ ← [Revert] Unauthorized()
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 632.64µs (390.33µs CPU time)
Ran 1 test suite in 5.04ms (632.64µs CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
Event Validate
vm.expectEmit는 다음에 발생할 이벤트의 특정 요소들을 검증하기 위한 함수다.event Transfer(address indexed from, address indexed to, uint256 amount); 가 있다고 가정한다.vm.expectEmit(true, true, false, true); 를 호출하게되면 from, to 토픽 검증, false는 3번째 인자인데 indexed된게 아니므로 패스하고, 4번째 true는 uint256 amount를 가르키게 된다.test_ExpectEmit_DoNotCheckData의 경우에 emit Transfer(address(this), address(1337), 1338); 형태로 호출하지만, emitter.t()를 호출했을 때 발생하는 amount가 1337이다. 하지만 amount 데이터는 false로 설정해놨으므로, 테스트 통과하게 된다.pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
contract EmitContractTest is Test {
event Transfer(address indexed from, address indexed to, uint256 amount);
function test_ExpectEmit() public {
ExpectEmit emitter = new ExpectEmit();
// Check that topic 1, topic 2, and data are the same as the following emitted event.
// Checking topic 3 here doesn't matter, because `Transfer` only has 2 indexed topics.
vm.expectEmit(true, true, false, true);
// The event we expect
emit Transfer(address(this), address(1337), 1337);
// The event we get
emitter.t();
}
function test_ExpectEmit_DoNotCheckData() public {
ExpectEmit emitter = new ExpectEmit();
// Check topic 1 and topic 2, but do not check data
vm.expectEmit(true, true, false, false);
// The event we expect
emit Transfer(address(this), address(1337), 1338);
// The event we get
emitter.t();
}
}
contract ExpectEmit {
event Transfer(address indexed from, address indexed to, uint256 amount);
function t() public {
emit Transfer(msg.sender, address(1337), 1337);
}
}
vm.expectEmit(true, true, false, true);, emit Transfer(address(this), address(1337), 1338); 로 수정하고 test하니 revert가 뜨는 것을 확인 가능하다.foundry/counter [ forge test --match-path test/test2.t.sol -vvvv
[⠢] Compiling...
[⠑] Compiling 1 files with Solc 0.8.10
[⠃] Solc 0.8.10 finished in 408.91ms
Compiler run successful!
Ran 2 tests for test/test2.t.sol:EmitContractTest
[FAIL: log != expected log] test_ExpectEmit() (gas: 72648)
Traces:
[72648] EmitContractTest::test_ExpectEmit()
├─ [33287] → new ExpectEmit@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 166 bytes of code
├─ [0] VM::expectEmit(true, true, false, true)
│ └─ ← [Return]
├─ emit Transfer(from: EmitContractTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000539, amount: 1338)
├─ [1940] ExpectEmit::t()
│ ├─ emit Transfer(from: EmitContractTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000539, amount: 1337)
│ └─ ← [Stop]
└─ ← [Revert] log != expected log
[PASS] test_ExpectEmit_DoNotCheckData() (gas: 72697)
Traces:
[72697] EmitContractTest::test_ExpectEmit_DoNotCheckData()
├─ [33287] → new ExpectEmit@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 166 bytes of code
├─ [0] VM::expectEmit(true, true, false, false)
│ └─ ← [Return]
├─ emit Transfer(from: EmitContractTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000539, amount: 1338)
├─ [1940] ExpectEmit::t()
│ ├─ emit Transfer(from: EmitContractTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000539, amount: 1337)
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 403.38µs (170.30µs CPU time)
Ran 1 test suite in 586.63ms (403.38µs CPU time): 1 tests passed, 1 failed, 0 skipped (2 total tests)
Failing tests:
Encountered 1 failing test in test/test2.t.sol:EmitContractTest
[FAIL: log != expected log] test_ExpectEmit() (gas: 72648)
Encountered a total of 1 failing tests, 1 tests succeeded
Forge Std
import {Test} from "forge-std/Test.sol"; 형태로 import하면 해당 Test를 컨트랙트에 상속해서 이용한다. (contract Name is Test { ... })Test Code Trace
[<Gas Usage>] <Contract>::<Function>(<Parameters>)
├─ [<Gas Usage>] <Contract>::<Function>(<Parameters>)
│ └─ ← <Return Value>
└─ ← <Return Value>

Green: For calls that do not revert
Red: For reverting calls
Blue: For calls to cheat codes
Cyan: For emitted logs
Yellow: For contract deployments
상위 트레이스의 gas양은 하위 트레이스의 gas양들의 합보다 좀 많은데, 이건 내부에서 산술 연산이나, r/w 등 때문에 gas양이 추가되서 그렇다.
ABI 추적이 안되는 경우는 calldata가 그대로 로그에 나온다.
[<Gas Usage>] <Address>::<Calldata>
└─ ← <Return Data>
실패한 코드만 재실행
forge test --rerun~/.foundry/cache/test-failures 경로에 저장된다. 성공한 테스트케이스를 제외하고, 실패 테스트케이스만 실행하고 싶을 때 사용하면 된다.Remix처럼 Foundry에서도 스마트 컨트랙트가 gas를 얼마나 소모할지 추정할 수 있다.
우선 Forge에서 계약에 대한 가스 보고서를 생성할 수 있는데 foundry.toml의 gas_reports 필드를 통해 특정 계약의 gas 보고서를 생성할 수 있다.
gas_reports = ["MyContract", "MyContractFactory"] or gas_reports = ["*"]gas_reports_ignore = ["FaucetContract"] # 특정 계약 가스 보고서 생성 xforge test --gas-report 옵션 사용
모든 테스트 함수 기능에 대한 gas snapshot이라는 기능을 제공하는데, 이는 계약에 따라 얼마나 많은 가스가 소모되는지, 최적화 등에 대한 비교하는데 주로 사용되는 것 같다.
forge snapshot --snap [output_filename] [--asc/--desc] [--min {value}/--max {value}]forge snapshot [--diff/--check {filename}] # --diff는 전체 출력, --check는 바뀐 부분만 출력Forge에서는 테스트 함수 내에서 임의의 구간에 대해 gas 사용량 스냅샷을 캡처할 수 있다. 이를 통해 외부 호출과 내부 연산에 소비되는 gas를 세밀하게 측정할 수 있으며, 코드 최적화나 성능 모니터링에 유용하다. cheatcodes를 활용하면 된다.
스냅샷 시작 (startSnapshotGas)
startSnapshotGas(string calldata name) : 현재까지 사용된 gas를 캡처 시작하며, 여기서 name은 스냅샷의 이름으로, 기본적으로 계약 이름에서 그룹이 파생된다.startSnapshotGas(string calldata group, string calldata name) : 사용자 지정 그룹 이름(group)과 스냅샷 이름(name)을 함께 사용해 캡처를 시작할 수 있다.스냅샷 종료 (stopSnapshotGas)
stopSnapshotGas() : 가장 최근에 시작된 스냅샷을 종료하고, 시작 시점 이후로 사용된 gas의 양을 반환한다.stopSnapshotGas(string calldata name), stopSnapshotGas(string calldata group, string calldata name) : 특정 스냅샷(또는 그룹 내 스냅샷)을 종료하고 사용된 gas를 측정한다.임의 값 캡처 (snapshotValue)
snapshotValue(string calldata name, uint256 value) : 숫자 값(예: 컨트랙트의 바이트코드 크기 등)을 스냅샷으로 기록한다.snapshotValue(string calldata group, string calldata name, uint256 value) : 사용자 지정 그룹 내에서 값을 기록할 수 있다.마지막 호출의 gas 측정 (snapshotGasLastCall)
snapshotGasLastCall(string calldata name) : 최근 호출의 gas 사용량을 캡처하여 반환한다.snapshotGasLastCall(string calldata group, string calldata name) : 그룹 내에서 마지막 호출의 gas 사용량을 캡처한다.테스트 실행 시 gas 사용량이 이전 스냅샷과 달라졌는지 비교할 수 있다. 이를 위해 아래 중 하나를 설정하면 된다. 이 설정이 활성화되면 이전 스냅샷과의 차이가 있을 경우 테스트가 실패된다.
FORGE_SNAPSHOT_CHECK=truegas_snapshot_check = true 설정--gas-snapshot-check=true스냅샷 생성을 원하지 않는다면 아래 중 하나를 사용해 비활성화할 수 있다.
FORGE_SNAPSHOT_EMIT=falsegas_snapshot_emit = false 설정--gas-snapshot-emit=false이 cheatcode들은 Isolated Test 모드에서 정확하게 작동한다. Isolated Test를 활성화하려면 --isolate 플래그를 사용하거나 테스트 함수에 /// forge-config: default.isolate = true를 추가해야한다.
예시1(외부 호출의 gas 사용량 측정)
contract SnapshotGasTest is Test {
Flare public flare;
function setUp() public {
flare = new Flare();
}
function testSnapshotGas() public {
vm.startSnapshotGas("externalA");
flare.run(256);
uint256 gasUsed = vm.stopSnapshotGas();
}
}
예시2(내부 상태 변경의 여러 섹션 측정)
contract SnapshotGasTest is Test {
uint256 public slot0;
function testSnapshotGas() public {
vm.startSnapshotGas("internalA");
slot0 = 1;
vm.stopSnapshotGas();
vm.startSnapshotGas("internalB");
slot0 = 2;
vm.stopSnapshotGas();
vm.startSnapshotGas("internalC");
slot0 = 0;
vm.stopSnapshotGas();
}
}
예시3(내부 상태 변경과 외부 호출을 동시에 측정)
contract SnapshotGasTest is Test {
uint256 public slot0;
Flare public flare;
function setUp() public {
flare = new Flare();
}
function testSnapshotGas() public {
vm.startSnapshotGas("combinedA");
flare.run(256);
slot0 = 1;
vm.stopSnapshotGas();
}
}
예시4(임의값 스냅샷)
contract SnapshotGasTest is Test {
uint256 public slot0;
function testSnapshotValue() public {
uint256 a = 123;
uint256 b = 456;
uint256 c = 789;
vm.snapshotValue("valueA", a);
vm.snapshotValue("valueB", b);
vm.snapshotValue("valueC", c);
}
}
예시5(마지막 호출의 gas 사용량 측정)
contract SnapshotGasTest is Test {
Flare public flare;
function setUp() public {
flare = new Flare();
}
function testSnapshotGasLastCall() public {
flare.run(1);
vm.snapshotGasLastCall("lastCallA");
}
}
예시6(그룹을 이용한 스냅샷 기록)
contract SnapshotGasTest is Test {
uint256 public slot0;
function testSnapshotGas() public {
vm.startSnapshotGas("CustomGroup", "internalA");
slot0 = 1;
vm.stopSnapshotGas();
vm.startSnapshotGas("CustomGroup", "internalB");
slot0 = 2;
vm.stopSnapshotGas();
vm.startSnapshotGas("CustomGroup", "internalC");
slot0 = 0;
vm.stopSnapshotGas();
}
}
Fork는 실제 블록체인 네트워크의 현재 상태를 복제하여 로컬 테스트 환경에서 시뮬레이션할 수 있도록 하는 기능이다.
Forking Mode
forge test --fork-url flag를 이용해서 RPC URL을 전달하여 single fork가 가능하다.--fork-block-number [번호] 형태로 flag를 제공하면 특정 block number 포크가 가능하다.--fork-url, --fork-block-number를 사용하게되면 캐싱이 되는데, forge clean으로 아티팩트와 캐시 디렉토리 내용을 지울 수 있음 (~/.foundry/cache/rpc/<체인 이름>/<블록 번호>에 캐싱됨, 아니면 --no-storage-caching 옵션을 통해 cache 저장 안하도록 할 수 있다.)forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key> Forking Cheatcodes
Forking Cheatcodes는 Solidity 테스트 코드 내에서 프로그래밍 방식으로 forking 모드를 제어할 수 있도록 하는 Foundry의 기능이다.
각 fork는 완전히 독립적인 EVM으로 동작하며, 저장소도 분리되어 있다. 단, msg.sender와 테스트 컨트랙트의 상태(변수)는 모든 fork에서 공유된다. 필요한 경우 vm.makePersistent(address) 를 통해 특정 계정을 모든 fork에서 지속적으로 사용 가능하도록 만들 수 있다.
vm.createFork(RPC_URL) : 지정된 RPC URL을 기반으로 새로운 fork를 생성하며, 고유한 uint256 식별자(forkId)를 반환
vm.createSelectFork(RPC_URL) : fork를 생성하면서 동시에 선택하는 한 줄짜리 함수
vm.selectFork(forkId) : 특정 forkId를 가진 fork를 활성화
vm.activeFork() : 현재 활성화된 fork의 식별자를 반환, 테스트 내에서 여러 fork를 생성해두고 필요에 따라 전환할 수 있음
vm.rollFork(blockNumber) : 활성 fork의 블록 넘버를 설정할 수 있다.
테스트 코드
contract ForkTest is Test {
// the identifiers of the forks
uint256 mainnetFork;
uint256 optimismFork;
//Access variables from .env file via vm.envString("varname")
//Replace ALCHEMY_KEY by your alchemy key or Etherscan key, change RPC url if need
//inside your .env file e.g:
//MAINNET_RPC_URL = 'https://eth-mainnet.g.alchemy.com/v2/ALCHEMY_KEY'
//string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
//string OPTIMISM_RPC_URL = vm.envString("OPTIMISM_RPC_URL");
// create two _different_ forks during setup
function setUp() public {
mainnetFork = vm.createFork(MAINNET_RPC_URL);
optimismFork = vm.createFork(OPTIMISM_RPC_URL);
}
// demonstrate fork ids are unique
function testForkIdDiffer() public {
assert(mainnetFork != optimismFork);
}
// select a specific fork
function testCanSelectFork() public {
// select the fork
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);
// from here on data is fetched from the `mainnetFork` if the EVM requests it and written to the storage of `mainnetFork`
}
// manage multiple forks in the same test
function testCanSwitchForks() public {
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);
vm.selectFork(optimismFork);
assertEq(vm.activeFork(), optimismFork);
}
// forks can be created at all times
function testCanCreateAndSelectForkInOneStep() public {
// creates a new fork and also selects it
uint256 anotherFork = vm.createSelectFork(MAINNET_RPC_URL);
assertEq(vm.activeFork(), anotherFork);
}
// set `block.number` of a fork
function testCanSetForkBlockNumber() public {
vm.selectFork(mainnetFork);
vm.rollFork(1_337_000);
assertEq(block.number, 1_337_000);
}
}
Forge는 property-based testing을 지원한다. 이는, 개별 시나리오가 아닌 일반적인 행동을 테스트하는 방식이다.
단위테스트를 작성하고, 이를 property 기반 테스트로 변환해보겠다.
pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
contract Safe {
receive() external payable {}
function withdraw() external {
payable(msg.sender).transfer(address(this).balance);
}
}
contract SafeTest is Test {
Safe safe;
// 테스트 컨트랙트 자체가 이더를 받을 수 있어야 withdraw 시 이더를 받을 수 있음.
receive() external payable {}
function setUp() public {
safe = new Safe();
}
function test_Withdraw() public {
payable(address(safe)).transfer(1 ether);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}
}
위의 Unit Test는 safe 컨트랙트로부터 이더를 인출할 수 있음을 테스트하지만 1 eth에 대해서만 검증을 수행한다. 즉, safe 컨트랙트에 있는 모든 이더가 인출되어야 한다는 일반적인 property를 테스트하는 것이다. 일반적인 property는 “safe에 잔고가 있을 때, 인출하면 safe에 있는 모든 이더가 인출되어야 한다”이다.
Forge는 매개변수를 하나 이상 받는 테스트 함수를 프로퍼티 기반 테스트로 실행하기 때문에, 테스트를 다음과 같이 재작성할 수 있다.
테스트 컨트랙트에 기본으로 제공되는 이더의 양은 2**96 wei 이므로, 적당히 uint를 제한해줘야한다. uint256 amount로 매개변수를 받으면 실패한다.
특정 경우를 제외하고 싶다면 vm.assume cheatcode를 이용할 수 있다.
vm.assume(amount > 0.1 ether); 형태로 사용하게되면 amount가 0.1 ether 이상인 경우만 테스트하고, 밑의 나머지 코드는 패스한다.contract SafeTest is Test {
// ...
function testFuzz_Withdraw(uint96 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}
Fuzzing 테스트를 수행했을 때 결과
runs : Fuzzer가 테스트한 시나리오의 수 (일반적으로 256개지만, 수정 가능)μ : 해당 뮤는 퍼징 실행에서 사용한 평균 gas양을 나타낸다.~ : 해당 틸드는 모든 퍼징 실행에 사용한 중앙 gas양이다.Fuzzing 테스트할때 Fixtures 라는게 존재하는데, 퍼징 매개변수로 사용될 입력 값의 집합을 명시적으로 지정하고 싶을 때 정의할 수 있다.
Storage arrays
fixture 접두어와 퍼징할 변수이름을 함께 배열을 만들어주면 된다.uint32[] public fixtureAmount = [1, 5, 555];Function
fixture 접두어와 함께 정의하면 되는데, 함수는 고정 크기나 동적 배열 값을 returns 해야한다.function fixtureOwner() public returns (address[] memory)Fixture 사용해서 DSChief 취약점 재현
function etch(address yay) public returns (bytes32 slate) {
bytes32 hash = keccak256(abi.encodePacked(yay));
slates[hash] = yay;
return hash;
}
function voteSlate(bytes32 slate) public {
uint weight = deposits[msg.sender];
subWeight(weight, votes[msg.sender]);
votes[msg.sender] = slate;
addWeight(weight, votes[msg.sender]);
}
address[] public fixtureYay = [
makeAddr("yay1"),
makeAddr("yay2"),
makeAddr("yay3")
];
bytes32[] public fixtureSlate = [
keccak256(abi.encodePacked(makeAddr("yay1"))),
keccak256(abi.encodePacked(makeAddr("yay2"))),
keccak256(abi.encodePacked(makeAddr("yay3")))
];
Fixture 방식와 일반 Fuzzing을 구분해서 차이를 보여주는 그림이다.

이외에도 Invariant Testing, Differential Testing이 존재한다.
forge test, forge script를 사용할 때 --debug flag를 붙여 디버깅을 수행할 수 있다.
forge test --debug "testSomethingFunction()"
cast는 Ethereum 애플리케이션과 상호 작용하기 위한 도구로, 스마트 계약 호출을 하거나, 거래를 보내거나, 모든 유형의 체인 데이터를 검색할 수 있는 커맨드라인 도구다.
abi-encode : 인자들을 ABI 인코딩
cast abi-encode "constructor(string,string,uint8,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000이더리움 메인넷 최신 블록 번호 확인
cast block-number --rpc-url https://eth.merkle.io비탈릭 부테린 이더 개수 확인
cast balance vitalik.eth --ether --rpc-url https://eth.merkle.io거래 재생 및 추적
cast run 0x9c32042f5e997e27e67f82583839548eb19dc78c4769ad6218657c17f2a5ed31 --rpc-url https://eth.merkle.io 
DAI token 총 공급량 검색
cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf
calldata 디코딩
cast 4byte-decode 0x1F1F897F676d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e7
Anvil 계정간에 메시지 전송
cast send --private-key <PRIVATE_KEY> 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc $(cast from-utf8 "hello world") --rpc-url http://127.0.0.1:8545/Anvil은 로컬에 테스트 이더리움 넷을 구축할 수 있는 도구다. 그냥 anvil 치면 된다. 아니면 rpc url을 통해 fork도 가능하다. --fork-url https://eth.merkle.io 형태로 플래그 주면 된다.
Chisel은 솔리디티 REPL이다.
