관리 메뉴

멍개의 연구소

[ethereum] foundry를 이용하여 스마트 컨트랙트 개발하기 본문

블록체인

[ethereum] foundry를 이용하여 스마트 컨트랙트 개발하기

멍개. 2025. 2. 17. 17:57

안녕하세요 멍개입니다.

이번글에서는 foundry을 이용하여 스마트 컨트랙트를 개발하는 방법을 다룹니다. foundry는 truffle, hardhat 처럼 스마트 컨트랙트 개발을 도와주는 도구입니다.

foundry는 4개의 명령 도구를 제공합니다.

  1. forge: test, compile, build, deploy
  2. cast: rpc 통신
  3. anvil: local testnet
  4. chisel: REPL

● 셋업

foundry는 공식문서를 통해 설치 가이드를 확인할 수 있습니다. foundry는 rust 기반이기 때문에 rust가 설치되어 있어야 합니다.

https://book.getfoundry.sh/getting-started/installation

 

Foundry Book

A book on all things Foundry

book.getfoundry.sh

 

foundry에서 스마트 컨트랙트 개발/테스트/배포하는 과정은 forge를 이용합니다.

· 프로젝트 생성

$ forge init helloworld-contracts

Initializing /Users/jeongtaepark/Desktop/helloworld-contracts...
Installing forge-std in /Users/jeongtaepark/Desktop/helloworld-contracts/lib/forge-std (url: Some("https://github.com/foundry-rs/forge-std"), tag: None)
'/Users/jeongtaepark/Desktop/helloworld-contracts/lib/forge-std'에 복제합니다...
remote: Enumerating objects: 1978, done.
remote: Counting objects: 100% (829/829), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1978 (delta 762), reused 642 (delta 638), pack-reused 1149 (from 5)
오브젝트를 받는 중: 100% (1978/1978), 692.79 KiB | 13.86 MiB/s, 완료.
델타를 알아내는 중: 100% (1291/1291), 완료.
    Installed forge-std v1.9.5
    Initialized forge project
$ tree .

.
├── README.md
├── foundry.toml
├── lib
│   └── forge-std
│       ├── CONTRIBUTING.md
│       ├── LICENSE-APACHE
│       ├── LICENSE-MIT
│       ├── README.md
│       ├── foundry.toml
│       ├── package.json
│       ├── scripts
│       │   └── vm.py
│       ├── src
│       │   ├── Base.sol
│       │   ├── Script.sol
│       │   ├── StdAssertions.sol
│       │   ├── StdChains.sol
│       │   ├── StdCheats.sol
│       │   ├── StdError.sol
│       │   ├── StdInvariant.sol
│       │   ├── StdJson.sol
│       │   ├── StdMath.sol
│       │   ├── StdStorage.sol
│       │   ├── StdStyle.sol
│       │   ├── StdToml.sol
│       │   ├── StdUtils.sol
│       │   ├── Test.sol
│       │   ├── Vm.sol
│       │   ├── console.sol
│       │   ├── console2.sol
│       │   ├── interfaces
│       │   │   ├── IERC1155.sol
│       │   │   ├── IERC165.sol
│       │   │   ├── IERC20.sol
│       │   │   ├── IERC4626.sol
│       │   │   ├── IERC721.sol
│       │   │   └── IMulticall3.sol
│       │   ├── mocks
│       │   │   ├── MockERC20.sol
│       │   │   └── MockERC721.sol
│       │   └── safeconsole.sol
│       └── test
│           ├── StdAssertions.t.sol
│           ├── StdChains.t.sol
│           ├── StdCheats.t.sol
│           ├── StdError.t.sol
│           ├── StdJson.t.sol
│           ├── StdMath.t.sol
│           ├── StdStorage.t.sol
│           ├── StdStyle.t.sol
│           ├── StdToml.t.sol
│           ├── StdUtils.t.sol
│           ├── Vm.t.sol
│           ├── compilation
│           │   ├── CompilationScript.sol
│           │   ├── CompilationScriptBase.sol
│           │   ├── CompilationTest.sol
│           │   └── CompilationTestBase.sol
│           ├── fixtures
│           │   ├── broadcast.log.json
│           │   ├── test.json
│           │   └── test.toml
│           └── mocks
│               ├── MockERC20.t.sol
│               └── MockERC721.t.sol
├── script
│   └── Counter.s.sol
├── src
│   └── Counter.sol
└── test
    └── Counter.t.sol

13 directories, 58 files

 

▶ foundry.toml: 프로젝트 정보를 가집니다. 해당 파일은 여러 옵션 정보를 가질 수 있으며, 깃허브에서 어떤 옵션 정보를 가질 수 있는지 확인할 수 있습니다.

https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

 

foundry/crates/config/README.md at master · foundry-rs/foundry

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. - foundry-rs/foundry

github.com

▶ lib: 깃허브로 다운받은 모듈을 의미합니다. 해당 모듈은 서브모듈(.gitmodules) 형태로 관리됩니다.

▶ scripts: forge script로 실행할 스크립트 코드를 관리합니다. 대표적으로 Deploy 관련 코드를 관리합니다. script 코드들은 solidity 기반으로 작성합니다.

▶ test: 테스트 코드를 관리합니다. 테스트 코드는 solidity 기반으로 작성합니다.

▶ src: 스마트 컨트랙트 소스코드를 관리합니다.

· 의존성 - submodule 기반

forge의 install을 이용하면 submodule 형태로 외부 라이브러리를 손쉽게 추가할 수 있습니다.

$ forge install transmissions11/solmate@v7

Installing solmate in /Users/jeongtaepark/Desktop/helloworld-contracts/lib/solmate (url: Some("https://github.com/transmissions11/solmate"), tag: Some("v7"))
'/Users/jeongtaepark/Desktop/helloworld-contracts/lib/solmate'에 복제합니다...
remote: Enumerating objects: 3085, done.
remote: Counting objects: 100% (177/177), done.
remote: Compressing objects: 100% (107/107), done.
remote: Total 3085 (delta 95), reused 120 (delta 69), pack-reused 2908 (from 1)
오브젝트를 받는 중: 100% (3085/3085), 832.96 KiB | 11.26 MiB/s, 완료.
델타를 알아내는 중: 100% (1973/1973), 완료.
'lib/ds-test' 경로에 대해 'lib/ds-test' (https://github.com/dapphub/ds-test) 하위 모듈 등록
'/Users/jeongtaepark/Desktop/helloworld-contracts/lib/solmate/lib/ds-test'에 복제합니다...
remote: Enumerating objects: 313, done.
remote: Counting objects: 100% (171/171), done.
remote: Compressing objects: 100% (79/79), done.
remote: Total 313 (delta 91), reused 132 (delta 83), pack-reused 142 (from 1)
오브젝트를 받는 중: 100% (313/313), 71.35 KiB | 17.84 MiB/s, 완료.
델타를 알아내는 중: 100% (130/130), 완료.
    Installed solmate v7

이렇게 성공적으로 라이브러리가 추가되었다면 2가지 변경사항이 존재합니다.

lib/ 아래에 설치한 라이브러리가 추가됩니다.

.gitmodules에 설치된 라이브러리의 정보가 추가됩니다.

forge install을 이용할 때 중요한 점이 있습니다. forge install이 수행되면 설치 / 추가 완료후 commit을 생성하게 됩니다. 이떄 만약 .git에서 작업 공간에 변경사항이 있을경우 정상적으로 설치되지 않습니다.

이 경우 두가지 해결법이 있습니다.

첫 번째는 현재 변경사항에 대한 커밋을 생성하여 작업 공간의 변경사항이 없는 상태에서 설치 진행하는 방법입니다.

두 번째는 forge install할 때 커밋을 생성하지 않도록 하는 방법입니다. --no-commit 옵션을 이용하면 커밋을 생성하지 않습니다.

 

$ forge install [패키지명] --no-commit

 

· 의존성 - Package Manager

파이썬의 pypi, 노드의 npm과 같이 forge에서는 soldeer를 이용합니다.

https://soldeer.xyz/

 

Soldeer - Solidity Package Manager

Soldeer is a lightweight package manager for Solidity built in Rust.

soldeer.xyz

$ forge soldeer install @openzeppelin-contracts~5.0.2

🦌 Running soldeer install 🦌

Dependency @openzeppelin-contracts-5.0.2.zip downloaded!
Writing @openzeppelin-contracts~5.0.2 to the lock file.
The dependency @openzeppelin-contracts-5.0.2 was unzipped!
Adding dependency @openzeppelin-contracts-5.0.2 to the config file
Added a new dependency to remappings @openzeppelin-contracts-5.0.2
$ tree . -L 2

.
├── README.md
├── dependencies
│   └── @openzeppelin-contracts-5.0.2
├── foundry.toml
├── lib
│   ├── forge-std
│   └── solmate
├── remappings.txt
├── script
│   └── Counter.s.sol
├── soldeer.lock
├── src
│   └── Counter.sol
└── test
    └── Counter.t.sol

8 directories, 7 files

 

soldeer로 설치된 외부 라이브러리는 dependencies에 추가됩니다. 그리고 remappings.txt와 soldeer.lock이 추가됩니다.remappings.txt는 dependencies에 추가된 라이브러리 목록을 가지고 있는 파일이며 soldeer.lock은 소스코드 경로, 라이브러리 이름, 버전, 체크섬 정보를 가지고 있는 파일입니다.

만약 깃허브 레포에서 forge로 구축된 프로젝트를 clone 했다면 soldeer로 관리중인 의존성 모듈을 설치해야 합니다. 이땐 update를 이용합니다.

$ forge soldeer update 
🦌 Running soldeer update 🦌

Dependency @openzeppelin-contracts-5.0.2.zip downloaded!
The dependency @openzeppelin-contracts-5.0.2 was unzipped!
Writing @openzeppelin-contracts~5.0.2 to the lock file.

● 개발프로세스

소스코드를 작성하고 컴파일 & 빌드를 하여 우리가 사용할 네트워크에 스마트 컨트랙트를 배포합니다. 그리고 이를 테스트는 개발 프로세스를 가집니다.

1. 소스코드 작성(with 테스트) 
2. 컴파일 & 빌드 
3. 배포 
4. 개별적으로 실행해보기 
5. 테스트 하기

출금패턴이 적용된 컨트랙트를 기반으로 테스트 합니다.

https://blog.naver.com/pjt3591oo/222594079769

 

[ethereum] solidity withdrawals pattern(출금패턴)

안녕하세요 멍개입니다. https://meongae.tistory.com/82으로 이관되었습니다. 이번 포스트는 solidity에서...

blog.naver.com

 

· 소스코드 작성

소스코드는 src/ 아래에서 관리합니다. forge 프로젝트 초기화 시 생성된 Counter.sol은 삭제합니다.

다음 코드는 출금패턴이 적용된 코드입니다.

▶ src/WithdrawalContract.sol

// SPDX-License-Identifier: GPL
pragma solidity 0.8.24;

error AlreadyWithdrawal();

contract WithdrawalContract {
  address payable public richest;
  uint public mostAmount = 0;
  uint public totalAmount = 0;

  mapping (address => uint) public pendingWithdrawals;

  receive() external payable {
    _receiveEther(msg.sender, msg.value);
  }

  constructor() payable {
    richest = payable(msg.sender);
    mostAmount = msg.value;
  }

  function becomeEther() public payable {
    _receiveEther(msg.sender, msg.value);
  }

  function _receiveEther(address sender, uint value) private {
    pendingWithdrawals[richest] += value;

    if (value > mostAmount) {
      richest = payable(sender);
      mostAmount = value;
    }

    totalAmount += value;
  }

  function withdraw() public {
    if (pendingWithdrawals[msg.sender] == 0) {
      revert AlreadyWithdrawal();
    }
    uint amount = pendingWithdrawals[msg.sender];
    // 리엔트란시(re-entrancy) 공격을 예방하기 위해
    // 송금하기 전에 보류중인 환불을 0으로 기억해 두십시오.
    pendingWithdrawals[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
  }
}

· 테스트 코드 작성

forge 프로젝트 초기화 시 생성된 Counter.t.sol은 삭제합니다.

테스트를 수행할 테스트 코드와 테스트 시 사용할 2개의 컨트랙트를 정의합니다.

테슽 코드는 test/ 아래에서 관리되며 파일명은 *.t.sol 형태입니다.

먼저, 출금 테스트시 사용할 2개의 컨트랙트를 만들어줍니다.

테스트 코드를 작성할 땐 forge-std/Test.sol, Console, Vm 등을 이용하게 됩니다.

깃허브를 통해 어떤 기능을 사용할 수 있는지 알아볼 수 있습니다.

https://github.com/foundry-rs/forge-std/tree/master/src

 

forge-std/src at master · foundry-rs/forge-std

Forge Standard Library is a collection of helpful contracts for use with forge and foundry. It leverages forge's cheatcodes to make writing tests easier and faster, while improving the UX of ch...

github.com

▶ ./test/NonPayableContract.sol

// SPDX-License-Identifier: GPL
pragma solidity 0.8.24;

import '../src/WithdrawalContract.sol';

contract NonPayableContract {
  // receive () external payable{}
  
  constructor () payable {}

  function send(WithdrawalContract _to, uint _amount) external {
    WithdrawalContract(_to).becomeEther{value: _amount}();
  }

  function withdraw(WithdrawalContract addr) public  {
    WithdrawalContract(addr).withdraw();
  }
}
 

해당 컨트랙트는 생성시를 제외하고는 ETH를 전송받을 수 없습니다.

constructor() payable{}가 존재하기 때문에 생성시 ETH를 받을 수 있습니다.

receive() payable{}가 없기 때문에 ETH를 전송받을 수 없습니다.

▶ ./test/Payable.sol

// SPDX-License-Identifier: GPL
pragma solidity 0.8.24;

import '../src/WithdrawalContract.sol';

contract PayableContract {
  receive () external payable{}
  
  constructor () payable {}

  function send(WithdrawalContract _to, uint _amount) external {
    WithdrawalContract(_to).becomeEther{value: _amount}();
  }

  function withdraw(WithdrawalContract addr) public  {
    WithdrawalContract(addr).withdraw();
  }

}
 

해당 컨트랙트는 언제든 ETH를 전송받을 수 있습니다.

constructor() payable{}가 존재하기 때문에 생성시 ETH를 받을 수 있습니다.

receive() payable{}가 있기 때문에 ETH를 전송받을 수 있습니다.

▶ ./test/WithdrawalContract.t.sol

test_*는 assertion을 수행하는 테스트 단위입니다.

testFail_*은 exception이 발생하는지 테스트합니다. exception의 경우는 vm.expectRevert를 통해 어떤 exception이 발생했는지 검사할 수 있습니다.

vm.expectRevert의 경우 testFail_*에서 수행될 경우 항상 pass가 되며, test_*로 작성된 테스트케이스에서 수행되어야 합니다.

setUp은 test_*, testFail_*와 같이 테스트가 수행되기 전에 매번 호출됩니다.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {WithdrawalContract, AlreadyWithdrawal} from "../src/WithdrawalContract.sol";

import {PayableContract} from "./PayableContract.sol";
import {NonPayableContract} from "./NonPayableContract.sol";

contract WithdrawalContractTest is Test {
    WithdrawalContract public withdrawalContract; // test target

    PayableContract public payableContract;
    NonPayableContract public nonPayableContract;
    
    function setUp() public pure {
      console.log("WithdrawalContractTest setUp");
    }

    function test_case0() public pure {
      console.log("WithdrawalContractTest test_case0");
    }
}

forge test를 통해 테스트 코드를 수행할 수 있습니다.

$ forge test -vvvv
[⠊] Compiling...
[⠢] Compiling 1 files with Solc 0.8.24
[⠆] Solc 0.8.24 finished in 2.09s
Compiler run successful!

Ran 1 test for test/WithdrawalContract.t.sol:WithdrawalContractTest
[PASS] test_case0() (gas: 3438)
Logs:
  WithdrawalContractTest setUp
  WithdrawalContractTest test_case0

Traces:
  [3438] WithdrawalContractTest::test_case0()
    ├─ [0] console::log("WithdrawalContractTest test_case0") [staticcall]
    │   └─ ← [Stop]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.04ms (98.53µs CPU time)

Ran 1 test suite in 357.14ms (1.04ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

출력 결과를 깔끔하게 보고싶다면 -vvvv를 조정하면 됩니다.

-vv

-vvv

-vvvv

-vvvvv

▶ setup

먼저 테스트를 수행하기 위해 setup에서 컨트랙트를 정의합니다.

function setUp() public {
  withdrawalContract = new WithdrawalContract{value: 0}();
 
  payableContract = new PayableContract{value: 1000}();
  nonPayableContract = new NonPayableContract{value: 1000}();
  console.log('test contract balanceof: ', address(this).balance);
}
 
$ forge test -vvvv
[⠒] Compiling...
[⠢] Compiling 1 files with Solc 0.8.24
[⠆] Solc 0.8.24 finished in 2.12s
Compiler run successful!

Ran 1 test for test/WithdrawalContract.t.sol:WithdrawalContractTest
[PASS] test_send() (gas: 188)
Logs:
  test contract balanceof:  79228162514264337593543948335

Traces:
  [188] WithdrawalContractTest::test_send()
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.06ms (86.42µs CPU time)

Ran 1 test suite in 364.00ms (1.06ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

▶ test_send

PayableContract와 NonPayableContract에서 send를 호출하여 WithdrawalContract로 ETH를 전송하는 테스트 코드를 작성합니다.

 function test_send() public  {
      payableContract.send(withdrawalContract, 120);
      nonPayableContract.send(withdrawalContract, 100);

      assertEq(address(payableContract).balance, 880);
      assertEq(address(nonPayableContract).balance, 900);
      assertEq(address(withdrawalContract).balance, 220);    

      assertEq(withdrawalContract.mostAmount(), 120);
      assertEq(withdrawalContract.totalAmount(), 220);

      assertEq(withdrawalContract.pendingWithdrawals(address(payableContract)), 120);
      assertEq(withdrawalContract.pendingWithdrawals(address(nonPayableContract)), 100);
    }

여기서 만약 버그가 있다면 버그를 고쳐줍시다.(사실 WIthdrawalContract에 버그를 하나 심어 놨습니다.)

forge test -vvvv
[⠊] Compiling...
[⠆] Compiling 4 files with Solc 0.8.24
[⠰] Solc 0.8.24 finished in 2.22s
Compiler run successful!

Ran 1 test for test/WithdrawalContract.t.sol:WithdrawalContractTest
[FAIL. Reason: assertion failed: 100 != 120] test_send() (gas: 132991)
Logs:
  test contract balanceof:  79228162514264337593543948335

Traces:
  [418244] WithdrawalContractTest::setUp()
    ├─ [164308] → new WithdrawalContract@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   └─ ← [Return] 688 bytes of code
    ├─ [53281] → new PayableContract@0x2e234DAe75C793f67A35089C9d99245E1C58470b
    │   └─ ← [Return] 266 bytes of code
    ├─ [51275] → new NonPayableContract@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
    │   └─ ← [Return] 256 bytes of code
    ├─ [0] console::log("test contract balanceof: ", 79228162514264337593543948335 [7.922e28]) [staticcall]
    │   └─ ← [Stop]
    └─ ← [Stop]

  [132991] WithdrawalContractTest::test_send()
    ├─ [81798] PayableContract::send(WithdrawalContract: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 120)
    │   ├─ [71972] WithdrawalContract::becomeEther{value: 120}()
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [30335] NonPayableContract::send(WithdrawalContract: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 100)
    │   ├─ [23009] WithdrawalContract::becomeEther{value: 100}()
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [0] VM::assertEq(880, 880) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertEq(900, 900) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertEq(220, 220) [staticcall]
    │   └─ ← [Return]
    ├─ [383] WithdrawalContract::mostAmount() [staticcall]
    │   └─ ← [Return] 120
    ├─ [0] VM::assertEq(120, 120) [staticcall]
    │   └─ ← [Return]
    ├─ [295] WithdrawalContract::totalAmount() [staticcall]
    │   └─ ← [Return] 220
    ├─ [0] VM::assertEq(220, 220) [staticcall]
    │   └─ ← [Return]
    ├─ [550] WithdrawalContract::pendingWithdrawals(PayableContract: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
    │   └─ ← [Return] 100
    ├─ [0] VM::assertEq(100, 120) [staticcall]
    │   └─ ← [Revert] assertion failed: 100 != 120
    └─ ← [Revert] assertion failed: 100 != 120

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.01ms (188.06µs CPU time)

Ran 1 test suite in 1.08s (1.01ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/WithdrawalContract.t.sol:WithdrawalContractTest
[FAIL. Reason: assertion failed: 100 != 120] test_send() (gas: 132991)

Encountered a total of 1 failing tests, 0 tests succeeded

에러가 발생합니다. 그 이유는 _receiveEther에서 첫 번째 호출되는 코드에 문제가 있기 떄문입니다.

pendingWithdrawals[richest] += value;

해당 코드를 다음과 같이 수정합니다. richest가 아닌 호출한 주소의 자산을 변경해주어야 합니다.

 

pendingWithdrawals[msg.sender] += value;

그리고 다시 테스트를 수행하면 테스트를 모두 통과합니다.

▶ withdraw 호출

withdraw를 호출하는 경우에 따라 케이스를 작성합니다.

send 이후 withdraw하는 경우

send를 하지 않고 withdraw하는 경우

send 이후 여러번의 withdraw하는 경우

function test_withdrawByRichest() public {
  payableContract.send(withdrawalContract, 120);
  payableContract.withdraw(withdrawalContract);
}
function testFail_withdrawByNoSend() public {
  payableContract.withdraw(withdrawalContract);
}
function testFail_multiWithdraw() public {
  payableContract.send(withdrawalContract, 120);
  payableContract.withdraw(withdrawalContract);
  payableContract.withdraw(withdrawalContract);
}

마지막으로 receive() external {}이 구현되지 않은 컨트랙트가 호출하는 경우를 작성합니다.

function testFail_withdrawByNonPayable() public {
  nonPayableContract.send(withdrawalContract, 100);
  nonPayableContract.withdraw(withdrawalContract);
}

여기서 중요한 점은 실패를 검증할 경우 testFail을 이용한다는 점 입니다.

· mocking

이 외에도 forge의 경우 vm을 통해 여러 모킹을 수행할수 있습니다.

▶ caller 변경

vm.prank(address(0)); // msg.sender가 변경됨

▶ block.timestamp 변경

vm.warp(1234); // 컨트랙트에서 block.timestamp를 호출할 때 값 변경

· Deploy

테스트 코드와 함께 컨트랙트 코드를 완성했습니다.

이제 우리는 forge를 이용하여 스마트 컨트랙트를 배포할 수 있습니다.

스마트 컨트랙트를 배포하기에 앞서서 anvil을 이용하여 가상의 블록체인 네트워크를 생성합니다.

$ anvil
 

forge create를 이용하면 src에 있는 특정 컨트랙트를 배포할 수 있습니다.

$ forge create ./src/WithdrawalContract.sol:WithdrawalContract \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

다음과 같이 실행 결과를 확인할 수 있습니다.

[⠊] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xdb6ecae0d35a247bc700222b02fa596ac8cb0f9748378f1a0975afcabe4ef028

하지만 우리가 하나의 컨트랙트가 아닌 여러 컨트랙트를 배포해야 하는 상황이 올 수 있습니다. 이때는 forge script를 이용합니다. script 아래에 Counter.s.sol은 지워준 후 WithdrawalContract.s.sol을 만들어 줍니다.

▶ script/WithdrawalContract.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {WithdrawalContract} from "../src/WithdrawalContract.sol";

contract WithdrawalContractScript is Script {
    function setUp() public {}

    function run() public {
      vm.broadcast();
      WithdrawalContract withdrawalContract = new WithdrawalContract();

      console.log("create withdrawalContract contract address: ", address(withdrawalContract));
      console.log("withdrawalContract richest: ", withdrawalContract.richest());
    }
}

vm.startBroadcast broadcast(배포)를 수행할 어카운트를 설정할 수 있습니다.

vm.envUint은 환경변수를 읽어올 수 있으며 정의되어있지 않다면 에러가 발생합니다.

/ SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {WithdrawalContract} from "../src/WithdrawalContract.sol";

contract WithdrawalContractScript is Script {
    function setUp() public {}

    function run() public {
      vm.startBroadcast(
        // vm.envUint("PRIVATE_KEY") // env check: if not set, will throw error
      );
      WithdrawalContract withdrawalContract = new WithdrawalContract();

      console.log("create withdrawalContract contract address: ", address(withdrawalContract));
      console.log("withdrawalContract richest: ", withdrawalContract.richest());

      vm.stopBroadcast();
    }
}
$ forge script ./script/WithdrawalContract.s.sol:WithdrawalContractScript \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  --broadcast
 

다음과 같이 실행 결과와 출력 결과를 확인할 수 있습니다. 혹여나 Counter.s.sol을 삭제하였는데도 해당 파일을 찾을 수 없다고 할 경우 cache/를 지우면 됩니다. 만약 --broadcast가 없다면 실제로 배포는 되지 않고 somulation만 수행합니다.

[⠒] Compiling...
No files changed, compilation skipped
EIP-3855 is not supported in one or more of the RPCs used.
Unsupported Chain IDs: 31337.
Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly.
For more information, please see https://eips.ethereum.org/EIPS/eip-3855
Script ran successfully.

== Logs ==
  create withdrawalContract contract address:  0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  withdrawalContract richest:  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

## Setting up 1 EVM.

==========================

Chain 31337

Estimated gas price: 1.753776101 gwei

Estimated total gas used for script: 294476

Estimated amount required: 0.000516444971118076 ETH

==========================

##### anvil-hardhat
✅  [Success]Hash: 0xc245d6b750a0aaf7782d99a3d57282c9eda0cb085b132105551c66514fffbecf
Contract Address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Block: 2
Paid: 0.000198673018162866 ETH (226566 gas * 0.876888051 gwei)

✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000198673018162866 ETH (226566 gas * avg 0.876888051 gwei)
                                                                                                                                                           

==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Transactions saved to: /Users/jeongtaepark/Desktop/helloworld-contracts/broadcast/WithdrawalContract.s.sol/31337/run-latest.json

Sensitive values saved to: /Users/jeongtaepark/Desktop/helloworld-contracts/cache/WithdrawalContract.s.sol/31337/run-latest.json

· cast

cast는 배포된 컨트랙트를 테스트하기 위해 rpc 통신을 수행하는 도구입니다.

조회를 위한 call과 트랜잭션 발생을 위한 send 명령어를 제공합니다.

$ cast call \
  [컨트랙트 주소]  \
  "richest()(address)" \
  --rpc-url http://localhost:8545
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

만약 트랜잭션을 발생하고 싶다면 send를 이용합니다. 이때 value를 지정하면 이더를 전송할 수 있습니다

  function test(uint _amount) public {
    pendingWithdrawals[msg.sender] += _amount;
  }

만약 앞의 test 함수를 호출하기 위해서는 다음과 같이 send를 호출할 수 있습니다.

$ cast send \
  [컨트랙트 주소] \
  --rpc-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  "test(uint)" 1000

cast는 abi-encode를 통해 함수 호출시 포함되어야 할 input 데이터를 생성할 수 있습니다.

$ cast abi-encode "test(uint)" 1000
0x00000000000000000000000000000000000000000000000000000000000003e8

cast는 이 외에도 block, transaction등과 같은 여러 명령들을 제공합니다.

https://book.getfoundry.sh/reference/cast/

 

Foundry Book

A book on all things Foundry

book.getfoundry.sh

 

· 코드 포맷

forge느 fmt를 이용하여 모든 코드의 포맷팅을 맞춰줍니다.

forge는 이 외에도 편의를 위한 여러 명령어를 제공하고 있습니다.

https://book.getfoundry.sh/reference/forge/

· anvil

anvil은 기본으로 설정된 mnemonic을 기반으로 로컬에 테스트 네트워크를 구축합니다.

test test test test test test test test test test test junk

다음과 같이 특정 네트워크를 포크하여 임의로 block 생성 주기를 변경하여 로컬 네트워크를 구축할 수 있습니다.

이는 이미 다른 네트워크에 있는 특정 스마트 컨트랙트를 테스트할 때 사용하면 상당히 도움이 많이 됩니다.

auto-impersonate 옵션은 임의로 블록을 생성하는 옵션이며 block-time은 블록 생성 주기입니다.

 

지금까지 foundry에서 제공하는 forge, anvil, cast를 다뤄보았습니다.