Rust로 만들어진 초고속 Solidity 개발 프레임워크로,
Solidity 프로젝트를 컴파일 → 테스트 → 배포 → 검증까지 전부 지원하는 툴이다.
$ forge init hello_foundry
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib
├── script
├── src
└── test
5 directories
forge build
forge test
이더스캔 등에 코드가 공개된 유명한 컨트랙트 같은 경우 복제해올 수 있다.
forge clone 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 ProjectName
프로젝트 내에서 외부 라이브러리 사용하는 것
forge install vectorized/solady
lib 폴더 안에 다운로드됨.
Import 쉽게 사용 가능하다.
solidity 문법으로는 import "@openzeppelin/contracts/token/ERC20/ERC20.sol”
경로로 파일을 불러오는데, Foundry에서 사용하려면 로컬 경로로 바꿔야 하니까 Remapping이 필요함.
forge remappings
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
solady/=lib/solady/src/
만약 따로 설정하고 싶다면, remappings.txt
혹은 foundry.toml
에서 설정 가능
경로 전체를 입력안하고 약어로 접근 가능하다.
만약 라이브러리 2개가
내부적으로 @openzepplin 사용한다면, Foundry는 내부적으로 하나의 remapping만 생성한다.
예를 들어
$ forge remappings
@openzeppelin/=lib/lib_1/node_modules/@openzeppelin/
lib/two 내부의 @openzeppelin는 무시된다.
해결법
Remappings.txt
를 따로 프로젝트 루트에 만든다.
라이브러리마다 고유한 Rempping 설정해야 된다.
lib/lib_1/:@openzeppelin/=lib/lib_1/node_modules/@openzeppelin/
lib/lib_2/:@openzeppelin/=lib/lib_2/node_modules/@openzeppelin/
forge update
forge remove solady
forge remappings > remappings.txt
리매핑 경로를 txt 파일로 저장
NPM 같은 버전 기반 패키지 관리 시스템 제공
지금까지는 gitsubmodules 형식으로 외부 라이브러리를 가져오는 형식이었음.
.
├── README.md
├── foundry.toml
├── lib
│ └── forge-std
│ ├── CONTRIBUTING.md
│ ├── LICENSE-APACHE
│ ├── LICENSE-MIT
│ ├── README.md
│ ├── foundry.toml
│ ├── package.json
│ ├── scripts
│ ├── src
│ └── test
├── script // 컨트랙트 배포하너가 실제환경에서 실행할 스크립트 저장
│ └── Counter.s.sol
├── src
│ └── Counter.sol
└── test
└── Counter.t.sol
9 directories, 11 files
forge test
// 특정 테스트 함수만 실행
forge test --match-contract ComplicatedContractTest --match-test test_Deposit
// 특정 경로만 실행
forge test --match-path test/ContractB.t.sol
// 테스트 파일 저장할 때 마다 테스트 실행
forge test --watch.
레벨 | 명령어 | 출력 내용 | 주 용도 |
---|---|---|---|
1 (기본) | forge test | 통과/실패 요약만 | 결과만 빠르게 보고 싶을 때 |
2 | forge test -vv | console.log 등 로그 + assertion 실패 메시지 | 로그 찍어서 값 확인하거나 expected vs actual 비교할 때 |
3 | forge test -vvv | 위 + 실패한 테스트의 스택 트레이스 | 실패 위치와 호출 흐름까지 보고 싶을 때 |
4 | forge test -vvvv | 위 + setup 트레이스까지 보여줌 (실패 테스트 한정) | 테스트 준비 코드까지 분석하고 싶을 때 |
5 | forge test -vvvvv | 모든 테스트의 스택 & setup 트레이스 | 성공한 테스트까지 전부 추적하고 싶을 때 (풀 디버깅) |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol"; // Test 상속받아 실행
import "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract myContract; // Sol 객체 가져오기
function setUp() public { //각 테스트 전에 실행되어 촟기상태 설정
myContract = new MyContract(); // 컨트랙트 배포
vm.addr(1); //실행자 1로 변경
}
function testSetVal() public { // 테스트 함수 : public or external
myContract.setVal(50); // 함수 실행
assertEq(myContract.val(), 50); // 비교
}
function testRevert_Subtract43() public {
vm.expectRevert();
testNumber -= 43;
}
function test_CannotSubtract43() public {
vm.expectRevert(stdError.arithmeticError); // 좀 더 정확한 에러 파악
testNumber -= 43;
}
}
forge test
forge test
는 각 테스트 함수가 독립적으로 실행. 따라서 전파가 안됨.
그러나 테스트 함수 간 의존성 만들거나 공유 상태를 사용해야 할 때 있음.
beforeTestSetup()
: 특정 테스트 실행 전에 다른 트랜잭션 먼저 실행시키는 기능
contract ContractTest is Test {
uint256 a;
uint256 b;
function beforeTestSetup(
bytes4 testSelector
) public pure returns (bytes[] memory beforeTestCalldata) {
if (testSelector == this.testC.selector) {
beforeTestCalldata = new bytes[](2);
beforeTestCalldata[0] = abi.encodePacked(this.testA.selector);
beforeTestCalldata[1] = abi.encodeWithSignature("setB(uint256)", 1);
}
}
function testA() public {
require(a == 0);
a += 1;
}
function setB(uint256 value) public {
b = value;
}
function testC() public {
assertEq(a, 1);
assertEq(b, 1);
}
}
아 여러 컨트랙트 객체 작성할 때 setup을 한번만 코드 쓰게 하려고
abstract contract HelperContract {
address constant IMPORTANT_ADDRESS = 0x543d...;
SomeContract someContract;
constructor() { // 생성자를 이용해 기본 설정을 적용할 수도 있음
// 기본 상태 초기화 가능
}
}
contract MyContractTest is Test, HelperContract {
function setUp() public override {
someContract = new SomeContract(0, IMPORTANT_ADDRESS);
}
}
contract MyOtherContractTest is Test, HelperContract {
function setUp() public override {
someContract = new SomeContract(1000, IMPORTANT_ADDRESS);
}
}
테스트 중 EVM 상태 조작, 시뮬레이션 가능
블록 번호 변경, 계정 스푸핑
vm
인스턴스로 접근
테스트를 좀더 쉽게 작성할 수 있도록 돕는 컨트랙트 모음
치트코드 | 설명 |
---|---|
vm.prank(addr) | 다음 트랜잭션을 addr 이 보낸 것처럼 실행 |
vm.startPrank(addr) / vm.stopPrank() | 연속해서 prank |
vm.warp(time) | 블록 타임 변경 |
vm.roll(blockNum) | 블록 넘버 변경 |
vm.deal(addr, amount) | 주소에 ETH 입금 |
vm.expectRevert() | revert 예상될 때 감싸는 코드 |
vm.snapshot() / vm.revertTo(id) | 상태 저장 & 되돌리기 (shared setup 용도 등) |
트랜잭션 상세한 실행 과정을 추적하여 디버깅, 최적화 돕는 Trace
실제 블록체인 상태 복제하여 로컬에서 테스트 수행
기본은 로컬에서 내가 만든 컨트랙트만 테스트 하는 건데,
실제 체인을 내 로컬로 포크해서 테스트 할 수 있게.
즉, 특정 시점의 이더리움 상태를 읽을 수 있는 환경을 만들고, 로컬에서 EVM 시뮬레이션으로 처리.
메인넷 배포된 USDC 컨트랙트를 로컬에 세팅해 상호작용 가능
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
}
contract ForkUSDCBalance is Test {
address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address target = address(0xde589C867174C349d00e9b582867aF5c13A74679);
function setUp() public {
// 메인넷 포크해서 선택
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
}
function testUSDCBalance() public {
uint256 balance = IERC20(usdc).balanceOf(target);
console.log("USDC balance of target:", balance);
assertEq(balance, 0);
}
}
이전 테스트 실행에서 실패한 테스트만을 선택적으로 다시 실행 가능
forge test --rerun
fuzz 테스트에서 발견된 실패 사례는 ~/.foundry/cache/fuzz/failures
에 저장됨.
이를 통해 재실행 가능
단일 시나리오 아닌 일반적인 동작 검증
다양한 입력값에 대해 일관된 결과를 반환하는지 확인 가능
퍼즈테스트 설정은 foundry.toml에서
[fuzz]
runs = 1000
개별 함수마다 다른 설정은
contract MyTest is Test {
/// forge-config: default.fuzz.runs = 100
/// forge-config: ci.fuzz.runs = 500
/// forge-config: default.fuzz.show-logs = true
function test_SimpleFuzzTest(uint256 x) public {
// 테스트 코드
vm.assume(amount > 0.1 ether); // 이 이상만 fuzz 실행
// snip
}
}
컨트랙트에서 특정 조건이 항상 유지되는지 검증하는 테스트 방식.
x * y = k
라는 공식이 항상 성립해야 하고,totalSupply
와 같아야 해.랜덤한 함수 호출 시퀀스 작성 ⇒ 각 호출 후에 정의된 불변 조건 검증.
runs
: 테스트를 몇 번 반복할지 설정. 기본값은 256회.depth
: 각 반복에서 몇 개의 함수 호출을 할지 설정. 기본값은 500회.invariant_*
함수: 불변 조건을 정의하는 함수들. 예를 들어:[invariant]
runs = 300
depth = 600
fail_on_revert = true
혹은 테스트 파일 내에서
/// forge-config: default.invariant.runs = 300
/// forge-config: default.invariant.depth = 600
항목 | 반드시 지정? | 설명 |
---|---|---|
✅ 테스트 함수 이름 접두어 | ✅ invariant_ 로 시작해야 인식됨 | |
✅ targetContract() | ✅ 어떤 컨트랙트에서 함수들을 fuzz할지 지정 | |
또는 targetSelector() | ⛔ 선택 (함수별로 더 좁히고 싶을 때만) | |
또는 targetArtifacts() | ⛔ 선택 (멀티컨트랙트 테스트 시) | |
✅ 핸들러 contract (보통 필요) | ✅ 함수 호출을 위임할 핸들러가 있어야 실제 동작 | |
is Test 상속 | ✅ 당연히 필요 (forge-std의 Test.sol) | |
.t.sol 확장자 | ✅ 아니면 자동으로 forge가 테스트로 인식 못 함 |
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Bank.sol";
contract BankHandler { // 무작위로 호출할 함수들의 집합
Bank public bank;
constructor(Bank _bank) {
bank = _bank;
}
function deposit() public payable { // 이 함수
bank.deposit{value: msg.value}();
}
function withdraw(uint256 amount) public { // 이 함수만 랜덤호출 수행.
try bank.withdraw(amount) {} catch {}
}
receive() external payable {}
}
contract BankInvariantTest is Test {
Bank bank;
BankHandler handler;
function setUp() public {
bank = new Bank();
handler = new BankHandler(bank);
// 대상 컨트랙트/핸들러 등록
targetContract(address(handler));
targetSender(address(1));
vm.deal(address(1), 100 ether); // 테스트 계정에 자금 지급
}
// 불변 조건: 전체 balances 합 = totalDeposits
function invariant_TotalBalanceEqualsTotalDeposits() public {
assertEq(address(bank).balance, bank.totalDeposits());
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Bank.sol";
contract BankInvariantTest is Test {
Bank bank;
function setUp() public {
bank = new Bank();
targetContract(address(bank)); // ✅ 직접 대상 지정
// ✅ 선택적으로 실행할 함수들 지정
bytes4 ;
selectors[0] = Bank.deposit.selector;
selectors[1] = Bank.withdraw.selector;
targetSelectors(address(bank), selectors);
// 테스트 대상 주소에게 자금 주입
address testUser = address(1);
vm.deal(testUser, 100 ether);
targetSender(testUser);
}
function invariant_TotalDepositsMatchBalance() public {
assertEq(address(bank).balance, bank.totalDeposits());
}
}
forge test --match-path test/BankInvariant.t.sol -vvvv
특정입력에 대해 두 개 이상의 구현이 동일한 출력을 생성하는 지 확인하는 방식.
예를 들어 내가 Merkle tree를 만들었는데,
Openzepplin에도 같은 기능이 있다면, 동작을 비교해야 돼.
JS,Rust 구현 등이라면 외부 프로그램을 실행해서 결과를 가져오는 FFI를 실행.
forge create --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> src/MyContract.sol:MyContract
# 예시 출력:
# Deployer: 0x123...
# Deployed to: 0xabc...
# Transaction hash: 0xdef...​:contentReference[oaicite:4]{index=4}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/MyContract.sol";
contract DeployScript is Script {
function run() public {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
new MyContract();
vm.stopBroadcast();
}
}​:contentReference[oaicite:16]{index=16}
forge verify-contract \
--chain-id <CHAIN_ID> \
--constructor-args $(cast abi-encode "constructor(string,string,uint8,uint256)" "TokenName" "SYM" 18 1000000000000000000000) \
--etherscan-api-key <ETHERSCAN_API_KEY> \
--compiler-version v0.8.10+commit.fc410830 \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract​:contentReference[oaicite:26]{index=26}
contract MultiChainDeploy is Script {
function run() public {
vm.createSelectFork("sepolia");
vm.startBroadcast();
new MyContract();
vm.stopBroadcast();
vm.createSelectFork("base-sepolia");
vm.startBroadcast();
new MyContract();
vm.stopBroadcast();
}
}​:contentReference[oaicite:40]{index=40}
--libraries src/Math.sol:MathLib:<LIBRARY_ADDRESS>​:contentReference[oaicite:46]{index=46}
forge test --match-test "test_Increment" --gas-report
forge test --gas-report
테스트 함수 단위로
forge snapshot
forge snapshot --diff
forge snapshot --check
.snapshot 파일에
forge test --debug "테스트함수명"
이더리움 앱과 상호작용 가능한 명령줄 도구
cast block-number --rpc-url https://eth.merkle.io
cast balance vitalik.eth --ether --rpc-url https://eth.merkle.io
cast balance vitalik.eth --ether --rpc-url https://eth.merkle.io
cast call 0x컨트랙트주소 "함수명()(반환타입)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/YourApiKey
로컬 이더리움 노드 or 라이브 이더리움 네트워크 노드
anvil --fork-url https:주소
코드를 대화형으로 작성하고 테스트할 수 있는 REPL(Read Eval Print Loop)
컨트랙트의 변수, 함수 등 작은 단위를 만들 때 빠르게 실험해 자세한 정보를 타나내기.
chisel
한 문장 칠 때마다 에러 확인 가능, 변수 입력시 다양한 정보 보여줌.
!source
입력시 현재까지 코드 확인 가능
!save
시 현재 세션 저장
!traces
: 트랜잭션 실행과정 추적