Skip to content

The Foundry documentation translated in Korean. Foundry is a toolkit for Ethereum application development, which is written in Rust. Foundry 공식 문서에 대한 한글 번역본입니다. 의역이 다소 포함되어 있으며, 모든 정보가 정확하지 않을 수도 있습니다.

Notifications You must be signed in to change notification settings

siwon-huh/Foundry-Book-Korean

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 

Repository files navigation

Foundry Book (Korean Ver.) by 허시원

** 의역이 다소 포함되어 있음.

You can access my GitBook through this URL:

https://dream-academy.gitbook.io/foundry-book-korean-ver.-by/

or this Notion page if you perfer (super slow at initial access):

https://magic-iris-860.notion.site/Foundry-Book-Korean-Ver-by-dc48c3bc94214278906c6a57267b85ab

Installation

  • 프로젝트 생성
    forge init <project_name>
  • 기존 다른 프로젝트에 생성
    forge install

Dependency Management

  • Dependency 추가 / 제거 / 업데이트

    forge {install, remove, update} <library_name>
  • Dependency Remapping

    forge remappings
    
    >> forge-std/=lib/forge-std/src
    	 ...
  • Dependency 커스텀

    • 프로젝트 루트에 remappings.txt 생성
  • Hardhat-style도 지원함

    • node_modules가 lib, src가 contracts의 역할을 수행
    • --hh flag로 enable할 수 있음
    • --hh flag == --lib-paths node_modules --contracts contracts

Test

테스트 코드 작성법

  • Test 대상

    • “test”로 시작하는 함수가 있으면 모두 test 대상
    • 디렉토리는 상관없음. src폴더에 있어도 test 대상임.
    • test 디렉토리와 .t.sol은 컨벤션으로서의 역할만 수행
  • 특정 테스트만 수행

    forge test --match-contract <contract_name> --match-test <function_name>
    
    forge test --match-path <path>
  • 특정 테스트 / path만 제외하는 것도 가능

    --no-match-contract / --no-match-test / --no-match-path

  • Trace depth

    • -v 플래그로 조절
    • -vv: Log 찍으면 보여줌. assertion error 등도 포함
    • -vvv: Fail한 테스트에 대한 stack trace
    • -vvvv: 모든 테스트에 대한 stack trace + fail한 테스트에 대한 setup trace
    • -vvvvv: 모든 테스트에 대한 stack trace & setup trace
  • Watch mode

    • 파일에 변경사항이 생기면 영향이 미치는 테스트만 다시 돌려줌
    forge test --watch
    forge test --watch --run-all (rerun all the test)
  • Forge-std의 Test.sol을 이용함

  • 디자인 패턴

    • test_Revert[ If | When ]_Condition
      function test_CannotSubtract43() public {
          vm.expectRevert(stdError.arithmeticError);
          testNumber -= 43;
      }
    • 상속을 통해 setup을 공유하거나 할 수도 있음

Forge-std 라이브러리 (Comprehensive)

Basic

  • vm.sol
    • cheatcodes interface
  • console.sol
    • Hardhat style logger
  • script.sol
    • Supports solidity scripting
  • Test.sol
    • Standard libraries, including vm.sol and console.sol
  • cheatcode
    // prank alice and give 100 ether
    hoax(alice, 100 ether);

Log

  • DSTest가 지원하는 log를 모두 지원 / log는 event로서 작동함
    log(string)
    logs(bytes)
    log_(named)_{address, bytes32, int, uint, bytes, string}
  • DSTest가 지원하지 않는 array 타입에 대한 log 지원
    • log_array / log_named_array

Console Logging

  • 한번에 4개까지 찍을 수 있음
  • uint, string, bool, address 타입 지원
  • 포맷팅 지원
    • %s: string으로 변환, uint256, int256, bytes는 0x붙어서 hex로 출력
    • %d, %i: 32바이트 상수에 대해 %s와 동일
    • %e: 32바이트 상수에 대해 exponential로 표현해줌
    • %x: 32바이트 상수에 대해 hex로 표현해줌
    • %o: Object → JS 오브젝트 스타일로 표현해줌. 보통 “” 붙은 string
    • %%: “%” 써야할때.. 포맷아님

Assert

  • DSTest가 지원하는 assert를 모두 지원

  • Error 메시지 출력 지원 (argument에 string memory err 추가)

    // 1. Equal
    assertEq(address, bytes32, int, uint)
    
    // 2. Equal Decimal
    assertEqDecimal(int, uint)
    assertEqDecimal(int a, int b, uint decimals)
    
    // 3. Equal 32 bytes
    assertEq32(bytes32)
    
    // 4. ?
    assertEq0(bytes memory)
    
    // 5. > / >= / < / <=
    assertGt / assertGe / assertLt / assertLe
    + Decimal
  • 추가 assert

    • fail
      • 특정 execution point에 도달하면 fail을 발생시킴
    • assertFalse
    • assertEq on bool, bytes, int256, uint256
    • assertEqUint
      • 다른 길이의 int에 대한 비교 지원
      • e.g. assertEqUint(uint8(1), uint256(1))
    • assertApproxEqAbs
      • 두 값의 차이가 일정 수준 이하임
    • assertApproxEqRel
      • 두 값의 비가 일정 퍼센트 이하임
      • 1e18 = 100%

Cheats

  • Skip

    • block.timestamp를 N초만큼 skip
      assertEq(block.timestamp, 0);
      skip(3600);
      assertEq(block.timestamp, 3600);
  • rewind

    • block.timestamp를 N초만큼 되돌림
    • exactly opposite of Skip()
  • hoax

    • 특정 주소를 ETH와 함께 prank함
    • ETH 수량을 정하지 않으면 2^128 wei(매우 많음)를 지급
      hoax(address who, address origin, uint256 give)
      origin은 ETH가 특정 주소로부터 지급될 수 있도록 하는듯
  • startHoax

    • 임시 hoax
    • startHoax(who, origin, give) 뒤 stopHoax()
  • deal

    • 특정 주소에 ERC-20 토큰을 지급함
    deal(address(dai), alice, 10000e18);
    assertEq(dai.balanceOf(alice), 10000e18);
  • deployCode

    • artifacts 디렉토리에 있는 컨트랙트의 바이트코드를 fetch해 컨트랙트를 배포함
    • val parameter로 msg.value 패싱 가능함
  • bound

    • fuzz test 할때 input을 특정 bound로 enum할 수 있음

      input = bound(input, 99, 101);
      
      // Returns 99 for input 0.
      // Returns 100 for input 1.
      // Returns 101 for input 2.
      // Returns 99 for input 3.
  • changePrank

    • 현재 prank된 주소를 끄고 다른 주소를 prank함
  • makeAddr

    • 어떤 이름이 부여된 주소를 생성할 수 있음
    • 주소 생성하면 이름이 그 주소에 대한 label이라는 속성으로 저장됨
      address alice = makeAddr("alice");
      emit log_address(alice); // 0x328809bc894f92807417d2dad6b7c998c1afdac6
  • makeAddrAndKey

    • 주소와 개인키 생성
      (address alice, uint256 key) = makeAddrAndKey("alice");
      emit log_address(alice); // 0x328809bc894f92807417d2dad6b7c998c1afdac6
      emit log_uint(key); // 70564938991660933374592024341600875602376452319261984317470407481576058979585
  • noGasMetering

    • modifier
    • 함수 실행동안 gas가 측정되지 않음 → gasleft() 연산에서 제외됨

Error

  • 쓰는 이유: 에러 타입을 지정해서 assert할 수 있음
    vm.expectRevert(stdError.arithmeticError)
  • assertionError
    • assert 실패시 발생
  • arithmeticError
    • arithmetic over/underflow
  • divisionError
    • division by zero
  • enumConversionError
    • 상수를 enum으로 변환하려할 때 발생
    • enum의 variant보다 상수의 값이 더 크면 에러 발생
    • !예시 필요
  • encodeStorageError
    • 스토리지의 데이터가 corrupt되었을 때 발생
    • assembly가 사용되지 않았다면 이런 일은 없긴 함
  • popError
    • 빈 배열에서 pop하면 발생
  • index00BError
    • 배열 out of bound 에러
  • memOverflowError
    • 동적 메모리 배열에 2^64-1개 이상의 원소를 넣으면 발생
  • zeroVarError
    • init 안된 function pointer 호출하면 발생

Storage

  • 스토리지에 query / write 등 다양한 작업 수행 가능

  • packed slot에 접근하는 것은 불가능

  • type를 저장하는 튜블이 32바이트보다 짧으면 slot 조회 안되는 버그 있음

  • 스토리지를 조회하기 위해 다음과 같은 descriptor 사용

    • target
      • 타겟 컨트랙트의 주소 설정
      • 디폴트는 0번으로 설정
    • sig
      • 함수 호출을 위한 4바이트 selector
    • with_key
      • 함수 호출을 위한 argument passing
      • 여러개일 경우 여러번 불러야함. 순서 상관 있음.
    • depth
      • 튜플 내의 position을 설정
      • 디폴트는 0으로 설정
      • !예시 필요
    • checked_write
      • 스토리지에 쓰여질 데이터 설정
      • bool, address, bytes32, uint256
    • find
      • target, sig, with_key, depth를 이용해 스토리지 슬롯 번호 조회
    • read
      • 스토리지 내 값을 읽어옴
      • bool, address, bytes32, int256, uint256
  • Storage Manipulation

    • 위에서 언급한 descriptor를 사용해 스토리지를 조회하고, 접근해 값을 바꾸는 작업 수행 가능
    using stdStorage for StdStorage;
    
    ...
    
    // `game` 컨트랙트의 `score` 함수 호출
    // and change its value to 10
    stdstore
        .target(address(game))
        .sig(game.score.selector)
        .checked_write(10);

Math

  • Solidity에서 지원하지 않는 math 기능 지원
  • 지원하는 기능은 abs, delta, percentDelta
    • abs
      • 절대값 반환
      uint256 ten = stdMath.abs(-10);
    • delta
      • 두 값의 차이 반환
      • arithmeticError-safe
      uint256 four = stdMath.delta(-1, 3);
    • percentDelta
      • delta의 퍼센트버전
      • 1e18 = 100%
      uint256 percent150 = stdMath.percentDelta(uint256(125), 50);
      uint256 percent60 = stdMath.percentDelta(uint256(50), 125);

Script

  • computeCreateAddress

    • 배포자 주소와 nonce로 생성될 주소를 계산해줌
    • 컨트랙트가 배포될 주소를 미리 알 수 있음
  • deriveRememberKey

    • 니모닉으로부터 private key를 불러와 forge의 local wallet에 저장함
    • 주소와 private key를 반환함
    • 니모닉은 BIP39 지정 단어만 지원
    string memory mnemonic = "test test test test test test test test test test test junk";
    
    (address deployer, uint256 privateKey) = deriveRememberKey(mnemonic, 0);
    
    bytes32 hash = keccak256("Signed by deployer");
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash);
    
    vm.startBroadcast(deployer);
    ...
    vm.stopBroadcast();

Fork Testing

  • 특정 환경을 포크해서 테스트해볼 수 있음
  • 두가지 접근법이 있음
    • forge test —fork-url을 사용한 싱글 포크
    • forking cheatcode를 사용한 멀티 포크

싱글 포크

  • 포크해오면 포크한 환경에 맞게 다음 변수들의 값이 바뀜
    • block_number
    • chain_id
    • gas_limit
    • gas_price
    • block_base_fee_per_gas
    • block_coinbase
    • block_timestamp
    • block_difficulty
    • 블록과 가스에 대한 값은 다 바뀐다고 보면 됨. 시점이 바뀌니까
  • 포크해올 블록을 지정할 수 있음
    forge test --fork-url <your_rpc_url> --fork-block-number 1
  • 이더스캔 API key 들고오면 보다 정확한 정보 받아올 수 있음
    forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key>
  • --fork-url과 --fork-block-number가 둘다 지정되면 해당 블록 데이터는 추후 테스트에 캐시됨
    • 저장 위치: ~/.foundry/cache/rpc/<chain_name>/<block_number>
    • forge clean으로 캐시 지울 수 있음. 근데 빌드한 artifact도 같이 날아감
    • 캐시 필요 없을 때: 커맨드에 --no-storage-caching 플래그 넣거나 foundry.toml에 no_storage_caching / rpc_storage_caching 넣으면 캐시 무시함

멀티 포크

  • 왜 쓰나?
    • 테스트별로 포크 다르게 해올 수 있음. 좋네
    • 각 포크는 uint256 identifier로 매핑됨
  • setUp에서 포크 여러개해두고 identifier로 갖다 쓰는 방법도 사용 가능
  • 관련 메서드
    • createFork / selectFork / createSelectFork
      • 포크 생성 / 선택 / 생성+선택
    • activeFork
      • 현재 설정된 포크 출력
    • rollFork
      • 현재 포크의 블록넘버 설정 가능
    • makePersistent
      • 특정 계정을 테스터와 같이 persistent하게 만들어줌
      • 반대로 revokePersistent(address)도 지원
    • isPersistent
      • 특정 주소가 persistent인지 출력
    • allowCheatcodes
      • 특정 주소에 대해 cheatcode의 사용을 허가함
      • 디폴트로 테스트 컨트랙트와 배포자가 cheatcode 사용이 허가되어있음
      • cheatcode 권한이 있는 주소에 컨트랙트 배포시 cheatcode 권한은 자동 부여
      • 복잡한 setup이 설정된 경우에 대해서 유용할 수 있음
      • ! 예시 및 추가설명 필요
    • transact
      • 트랜잭션 해시를 이용해 트랜잭션을 끌어와서 현 state에서 내가 실행할 수 있음
      • 신기함
  • 각 포크는 독립적인 EVM으로서 동작함
    • 스토리지도 따로임
    • 하지만 msg.sender와 persistent account의 상태, test contract의 state는 공유함

Cast와 Anvil을 이용한 Mainnet Fork Example

  • Cast와 Anvil을 사용하면 실제 네트워크를 포크해서 테스트할 수 있다.

  • Dai 토큰을 Anvil에서 만든 계정으로 보내는 튜토리얼을 해보자.

    • 메인넷 포크해오기

      $ anvil --fork-url https://mainnet.infura.io/v3/$INFURA_KEY
      • 10개의 계정 주소와 private key를 생성해줌
    • 이더스캔에 가서 Dai 토큰 홀더를 아무나 찾아보기 (LUCKY_USER)

      • unlucky 아님?
    • 환경변수 설정

      $ export ALICE=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
      $ export DAI=0x6b175474e89094c44da98b954eedeac495271d0f
      $ export LUCKY_USER=0xad0135af20fa82e106607257143d0060a7eb5cbf
    • cast 로 내 balance 확인

      $ cast call $DAI \
        "balanceOf(address)(uint256)" \
        $ALICE
      >> 0
    • cast로 lucky_user balance 확인

      $ cast call $DAI \
        "balanceOf(address)(uint256)" \
        $LUCKY_USER
      >> 71686045944718512103110072
    • cast send로 lucky user의 DAI를 나한테 보냄

      # This calls Anvil and lets us impersonate our lucky user
      $ cast rpc anvil_impersonateAccount $LUCKY_USER
      $ cast send $DAI \
      --from $LUCKY_USER \
        "transfer(address,uint256)(bool)" \
        $ALICE \
        1686045944718512103110072
      blockHash               0xbf31c45f6935a0714bb4f709b5e3850ab0cc2f8bffe895fefb653d154e0aa062
      blockNumber             15052891
      ...
    • 토큰 보내진걸 확인 가능

      cast call $DAI \
        "balanceOf(address)(uint256)" \
        $ALICE
      1686045944718512103110072
      
      $ cast call $DAI \
        "balanceOf(address)(uint256)" \
        $LUCKY_USER
      70000000000000000000000000
      • 어찌됐건 테스트환경에서만 일어난 일이다.

Advanced - Fuzz

  • Property-based 테스트에 대한 지원
  • Parametric testing은 지원하지 않고 Fuzzing만 가능하다.
  • 고정값이 아닌 변수에 대한 테스트가 가능하다는 장점이 있다.
  • 예시
    • 고정값을 사용한 테스트. 1 ether가 잘 전해지는지 테스트한다.
    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);
    }
    • property-based 테스트. 모든 amount에 대한 테스트를 통과해야한다.
    function testFuzz_Withdraw(uint256 amount) public {
      payable(address(safe)).transfer(amount);
      uint256 preBalance = address(this).balance;
      safe.withdraw();
      uint256 postBalance = address(this).balance;
      assertEq(preBalance + amount, postBalance);
    }
    • 실제로 위 테스트는 2^96-1 wei 이상에서 통과하지 못한다.
    • uint96을 사용하거나 assume을 사용해 amount의 바운더리를 지정 가능
    function testFuzz_Withdraw(uint96 amount) public {
        vm.assume(amount > 0.1 ether);
        // snip
    }
  • FOUNDRY_FUZZ_RUNS 설정을 통해 퍼징 시나리오 개수 조절 가능
  • “mu” → 퍼징동안 사용된 가스 평균
  • “~” → 퍼징동안 사용된 가스 중간값

Advanced - Invariant

  • 왜 쓰나?

    • 지정된 function call을 랜덤한 순서로 실행함
    • 지켜져야만 하는 조건들이 복잡한 동작동안 잘 지켜지는지 테스트할 수 있음
    • 로직 버그를 찾는데 유용함
    • invariant로 지정된 변수가 함수 호출때마다 assert됨
  • runs 와 depth로 dimension 설정

    • runs: function call sequence가 생성되는 횟수
    • depth: 한 run에 대해 수행하는 function call의 횟수
      • revert 해도 카운트함
  • 최대한 많은 Invariant를 설정하는 것이 좋은 invariant testing suite라고 할 수 있음

  • Invariant 설정 방법에는 여러가지가 있음

    • Direct assertion
      • 컨트랙트를 쿼리해 값을 직접 비교한다.
        assertGe(token.totalAssets(), token.totalSupply())
    • Ghost variable assertion
      • 컨트랙트를 쿼리해 값을 불러오고 test 환경에서 설정한 값과 맞는지 비교한다.
        assertEq(token.totalSupply(), sumBalanceOf)
    • Deoptimizing (Naive Implementation Assertions)
      • 컨트랙트를 쿼리해서 값을 불러온다.
      • 매우 가스 비효율적인 로직으로 가져온 값과 비교한다.
      assertEq(pool.outstandingInterest(), test.naiveInterest())
  • Conditional Invariant

    • invariant가 반드시 true여야만 할 필요는 없다.

    • 특정 컨디션에 따라 제거되거나 변해야만 하는 variable 또한 있기 때문 (예: 청산)

    • 하지만 invariant assertion에 조건문을 넣는 것은 바람직하지 않다.

      • 테스트 코드를 잘못 짜면 false positive가 발생할 수도 있기 때문이다.
      function invariant_example() external {
        if (protocolCondition) return;
      
        assertEq(val1, val2);
      }
      • 위 코드는 특정 조건에 따라 아예 assert가 안될 수 있다.
      • 차라리 조건문 넣을거면 로직마다 assert를 넣어주는 것이 좋다.
      function invariant_example() external {
          if (protocolCondition) {
              assertLe(val1, val2);
              return;
          };
          assertEq(val1, val2);
      }
    • setUp을 통해 여러 환경을 설정해두고 시나리오마다 invariant testing contract를 짜는 방법도 있긴 하다.

      • 하지만 invariant target으로 하여금 fuzzer가 특정 결과만을 뱉도록 유도하는 편이 훨씬 더 powerful한 테스트가 될 것이다.
  • Invariant Target

    • Target은 contract / sender / selector / artifacts(ABI) / artifact selector 등으로 찾음

    • Function call probability distribution

      • 타겟 컨트랙트의 함수들은 랜덤으로 호출됨
      • 컨트랙트 단위 / 함수 단위로 확률 분배됨
      targetContract1: 50%
      ├─ function1: 50% (25%)
      └─ function2: 50% (25%)
      
      targetContract2: 50%
      ├─ function1: 25% (12.5%)
      ├─ function2: 25% (12.5%)
      ├─ function3: 25% (12.5%)
      └─ function4: 25% (12.5%)
    • 따라서 함수 적은 컨트랙트는 각 함수가 더 자주 호출될 것임에 유의해야함

    • 컨트랙트 간 함수 개수의 편차가 큰 경우 함수가 많은 컨트랙트는 적게 테스트될 수 있음

  • Helper functions

    • Forge-std에 포함되어 있음
    excludeContract(address)
    excludeSender(address)
    excludeArtifact(string memory)
    targetArtifact(string memory)
    targetArtifactSelector(FuzzSelector memory)
    targetContract(address)
    targetSelector(FuzzSelector memory)
    targetSender(address)
    • targetContract에 수작업으로 대상 컨트랙트 넣어줘야함
    • setUp 함수로 배포된 컨트랙트는 자동으로 대상 컨트랙트로 지정됨
      • 주의) 수작업으로 지정된 컨트랙트 없어야됨
    • 대상에서 제외하고 싶으면 excludeContract로 빼줘야함
  • Handler Function

    • 초기조건 없이는 무조건 revert나는 함수들이 있음
      • deposit, transfer 등
    • 하지만 컨트랙트 자체의 기능에 문제가 있는 것은 아님
    • 실행에 필요한 조건들을 Handler 컨트랙트에 넣어서 올바른 동작을 유도해야함
    • foundry가 제공하는 그림을 보면서 이해해보자. Untitled Untitled
    • 초기조건이 필요한 함수에 대한 호출을 모두 handler 컨트랙트로 프록시해서 호출할 수 있도록 만들어주면 에러 없이 실행할 수 있다.
    • Ghost variable에 대한 테스트도 가능 (사전에 지정된 상수와 match하는지)
  • Bounded / Unbounded Function

    • 퍼징을 수행할 때 input으로 넣을 값에 대한 bound를 설정할 수 있다.
    • assume과 같은 기능을 하지만 성능이 좀 더 좋다나
  • Actor Management

    • prank할 주소를 배열로 만들어두고 여러 주소에 대한 반복적인 테스트를 수행할 수 있다.
    address[] public actors;
    
    address internal currentActor;
    
    modifier useActor(uint256 actorIndexSeed) {
        currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
        vm.startPrank(currentActor);
        _;
        vm.stopPrank();
    }
    
    // Unbounded
    function deposit(
        uint256 assets,
        uint256 actorIndexSeed
    ) public virtual useActor(actorIndexSeed) {
        asset.mint(currentActor, assets);
    
        asset.approve(address(token), assets);
    
        uint256 beforeBalance = asset.balanceOf(address(this));
    
        uint256 shares = token.deposit(assets, address(this));
    
        assertEq(asset.balanceOf(address(this)), beforeBalance - assets);
    
        sumBalanceOf += shares;
    
        sumDeposits[currentActor] += assets
    }
    
    // Bounded
    function deposit(uint256 assets, uint256 actorIndexSeed) external {
        assets = bound(assets, 0, 1e30);
    
        super.deposit(assets, actorIndexSeed);
    }

Advanced - Differential

  • 왜 쓰나?

    • 같은 함수에 대한 여러가지 implementation을 두고 이들의 output을 비교할 때 사용한다.
    • 둘의 output이 다르면 둘 중 하나는 잘못 구현되어 있음을 알 수 있다.
    • 원래는 differential testing이라고 하는데, forge는 differential testing을 이용한 fuzzing 또한 지원한다. 좋네
  • ffi cheatcode를 사용하면 non-EVM 환경에 대한 테스트도 수행할 수 있다.

  • ffi 치트코드는 쉘 커맨드를 실행하고 결과값을 가져오는 기능을 수행한다.

    import "forge-std/Test.sol";
    
    contract TestContract is Test {
    
        function testMyFFI () public {
            string[] memory cmds = new string[](2);
            cmds[0] = "cat";
            cmds[1] = "address.txt"; // assume contains abi-encoded address.
            bytes memory result = vm.ffi(cmds);
            address loadedAddress = abi.decode(result, (address));
            // Do something with the address
            // ...
        }
    }
  • 퍼징이 언제나 옳은 것은 아니다. 엣지케이스도 존재하므로 유의할 것.

  • ffi 커맨드로 잘 알려진 테스트케이스를 들고와서 벤치마킹하는 데에 쓸 수도 있다. 유용

    • 가스 스냅샷 찍는데 사용할 수도 있다.
    bytes32[100] data;
    uint256[8] leaves = [4, 8, 15, 16, 23, 42, 69, 88];
    
    function setUp() public {
        string[] memory inputs = new string[](2);
        inputs[0] = "cat";
        inputs[1] = "src/test/standard_data/StandardInput.txt";
        bytes memory result =  vm.ffi(inputs);
        data = abi.decode(result, (bytes32[100]));
        m = new Merkle();
    }
    
    function testMerkleGenerateProofStandard() public view {
        bytes32[] memory _data = _getData();
        for (uint i = 0; i < leaves.length; ++i) {
            m.getProof(_data, leaves[i]);
        }
    }
  • Differential Testing이 더 궁금하다면 패러다임의 GDA를 보면서 갖고 놀아보자. Gradual Dutch Auctions - Paradigm

  • 솔리디티 퍼징 템플릿도 있다. https://github.com/patrickd-/solidity-fuzzing-boilerplate

Deploy

  • 한번에 한 컨트랙트만 배포 가능
    $ 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

  • forge create로 배포한 컨트랙트에 대해 자동으로 verify 가능

  • 이더스캔 API 키 설정 필수

    forge verify-contract \
        --chain-id 42 \
        --num-of-optimizations 1000000 \
        --watch \
        --constructor-args $(cast abi-encode "constructor(string,string,uint256,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000) \
        --etherscan-api-key <your_etherscan_api_key> \
        --compiler-version v0.8.10+commit.fc410830 \
        <the_contract_address> \
        src/MyToken.sol:MyToken
    
    Submitted contract for verification:
      Response: `OK`
      GUID: `a6yrbjp5prvakia6bqp5qdacczyfhkyi5j1r6qbds1js41ak1a`
      url: https://kovan.etherscan.io//address/0x6a54…3a4c#code
  • verification result 폴링해서 바로 가져오려면 --watch 플래그 사용해야함. 플래그 없으면 그냥 verify 여부만 알려줌.

  • 위의 예시에서는 ABI encode를 위해 cast를 사용했는데, 이러면 좀 더 편함

  • verify 이후 발생 가능한 에러

    • Invalid character ‘x’ at position 1
      • 비밀키가 0x로 시작 안함
    • EIP-1559 not activated
      • RPC 노드에서 EIP-1559 지원 안함. --legacy 플래그 주면 EIP-1559아닌 레거시 트랜잭션 날려줌.
      • 로컬 개발환경이면 가나슈 대신 하드햇 쓰렴
    • Failed to parse tokens
      • 인자 타입 확인하렴
    • signature error
      • 비밀키 맞는지 확인하렴
    • compiler version commit for verify
      • 컴파일러 버전 확인해보렴
        ~/.svm/0.x.y/solc-0.x.y --version
  • 관련 버그

    • 이더스캔과의 호환 문제(1)
      • Forge는 src/lib/test 등 디렉토리 단위로 소스를 가져옴.
      • 그런데 이더스캔은 이런 식으로 소스 recompile 지원 안함.
      • 상대주소로 변경해서 컴파일하면 지원하니까 이렇게 하는걸 권장
    • 이더스캔과의 호환 문제(2)
      • 바이트코드 해시 없는 컨트랙트는 verify 지원 안함

Gas Tracking

  • Gas Report, Gas Snapshot 두가지 기능 제공

  • 개념 정리

    • Gas Report
      • Forge가 연산한 함수 단위의 gas 소모치
      • 느리지만 더 자세한 insight 제공
    • Gas Snapshot
      • 각 테스트가 소모한 gas
      • 빌트인 툴이 더 많음. report만큼 자세하진 않지만 빠름
  • Gas Report

    • foundry.toml에 대상 컨트랙트를 지정하면 테스트시 가스 소모치를 출력해줌
    // foundry.toml
    gas_reports = ["MyContract", "MyContractFactory"]
    
    // shell
    forge test --match-test testBurn --gas-report

    Untitled

    • foundry.toml에 gas_reports_ignore로 넣으면 무시할 컨트랙트도 설정 가능
  • Gas Snapshot

    • test function에 대한 gas fee를 출력해줌.
    • optimization 이후 가스 소모치를 비교하거나 배포 전 가스소모치를 예측하는 데 유용함.
    $ forge snapshot
    $ cat .gas-snapshot
    
    ERC20Test:testApprove() (gas: 31162)
    ERC20Test:testBurn() (gas: 59875)
    ERC20Test:testFailTransferFromInsufficientAllowance() (gas: 81034)
    ERC20Test:testFailTransferFromInsufficientBalance() (gas: 81662)
    ERC20Test:testFailTransferInsufficientBalance() (gas: 52882)
    ERC20Test:testInfiniteApproveTransferFrom() (gas: 90167)
    ERC20Test:testMetadata() (gas: 14606)
    ERC20Test:testMint() (gas: 53830)
    ERC20Test:testTransfer() (gas: 60473)
    ERC20Test:testTransferFrom() (gas: 84152)
    • 결과값을 파일로 저장하고 싶으면 forge snapshot --snap <file_name>

    • 결과값 필터링 기능

      • --asc / --desc: 오름차순, 내림차순 출력
      • --min / --max : 특정 값 이상 / 이하의 결과만 출력
    • 결과값 비교 기능

      • --diff로 저장해둔 스냅샷과 가스 소모치 비교 가능

        $ forge snapshot --diff .gas-snapshot2
        
        Running 10 tests for src/test/ERC20.t.sol:ERC20Test
        [PASS] testApprove() (gas: 31162)
        [PASS] testBurn() (gas: 59875)
        [PASS] testFailTransferFromInsufficientAllowance() (gas: 81034)
        [PASS] testFailTransferFromInsufficientBalance() (gas: 81662)
        [PASS] testFailTransferInsufficientBalance() (gas: 52882)
        [PASS] testInfiniteApproveTransferFrom() (gas: 90167)
        [PASS] testMetadata() (gas: 14606)
        [PASS] testMint() (gas: 53830)
        [PASS] testTransfer() (gas: 60473)
        [PASS] testTransferFrom() (gas: 84152)
        Test result: ok. 10 passed; 0 failed; finished in 2.86ms
        testBurn() (gas: 0 (0.000%))
        testFailTransferFromInsufficientAllowance() (gas: 0 (0.000%))
        testFailTransferFromInsufficientBalance() (gas: 0 (0.000%))
        testFailTransferInsufficientBalance() (gas: 0 (0.000%))
        testInfiniteApproveTransferFrom() (gas: 0 (0.000%))
        testMetadata() (gas: 0 (0.000%))
        testMint() (gas: 0 (0.000%))
        testTransfer() (gas: 0 (0.000%))
        testTransferFrom() (gas: 0 (0.000%))
        testApprove() (gas: -8 (-0.000%))
        Overall gas change: -8 (-0.000%)
      • --check로 갖고 있는 스냅샷과 비교해 다른 값만 출력할 수도 있음

        $ forge snapshot --check .gas-snapshot2
        
        Running 10 tests for src/test/ERC20.t.sol:ERC20Test
        [PASS] testApprove() (gas: 31162)
        [PASS] testBurn() (gas: 59875)
        [PASS] testFailTransferFromInsufficientAllowance() (gas: 81034)
        [PASS] testFailTransferFromInsufficientBalance() (gas: 81662)
        [PASS] testFailTransferInsufficientBalance() (gas: 52882)
        [PASS] testInfiniteApproveTransferFrom() (gas: 90167)
        [PASS] testMetadata() (gas: 14606)
        [PASS] testMint() (gas: 53830)
        [PASS] testTransfer() (gas: 60473)
        [PASS] testTransferFrom() (gas: 84152)
        Test result: ok. 10 passed; 0 failed; finished in 2.47ms
        Diff in "ERC20Test::testApprove()": consumed "(gas: 31162)" gas, expected "(gas: 31170)" gas

Debugger

  • 자체 디버거를 제공함. 왜 몰랐지
  • forge debug를 통해 사용 가능
    $ forge test --debug $FUNC
    $ forge debug --debug $FILE --sig $FUNC
    // example
    $ forge debug --debug src/SomeContract.sol --sig "myFunc(uint256,string)" 123 "hello"
  • Fuzzing test에 대한 디버깅도 지원한다.
    • Fail 뜨는 경우 첫번째 fail 시나리오를 출력
    • 모두 통과하는 경우 마지막에 통과한 시나리오를 출력
  • 디버거는 사분면으로 나뉘어 정보를 출력 Untitled
    • 1번: opcode 출력, 현재 opcode가 하이라이트 처리
    • 2번: 현재 스택과 스택 사이즈 출력
    • 3번: 소스코드
    • 4번: EVM 메모리 상태 출력
    • 스택이나 메모리에 가끔 색깔이 칠해질 때가 있는데,
      • 빨강색: 현재 opcode로 값이 쓰여질 예정
      • 초록색: 직전 opcode로 값이 쓰여짐
      • 파랑색: 메모리의 경우 현재 opcode로 값이 읽힘 / 스택의 경우 읽히거나 pop당함
  • 디버거 navigating 단축키
    • vim이랑 비스무리
    • General
      • q: 끄기
    • OPCODE view
      • 숫자 + k: N개 라인 위로 이동
      • 숫자 + j: N개 라인 아래로 이동
      • g: 트랜잭션 시작으로 이동
      • G: 트랜잭션 끝으로 이동
      • c: 직전 call-type instruction으로 이동 (call, staticcall, delegatecall, callcode 등)
      • C: 다음 call-type instruction으로 이동
      • a: 직전 JUMP/JUMPI instruction으로 이동
      • s: 다음 JUMPDEST instruction으로 이동
    • Memory view
      • cmd + k: 위로 스크롤
      • cmd + j: 아래로 스크롤
      • m: 메모리를 UTF-8 인코딩해서 보여줌
    • Stack view
      • K: 위로 스크롤
      • J: 아래로 스크롤
      • t: 스택 라벨 보여줌. 현재 opcode가 건드릴 아이템 확인용으로 사용

Cheatcode (Comprehensive)

  • 왜 쓰나?
    • Powerful assertion 제공
  • 지정된 cheatcode address를 통해 제공
    • 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
    • 좀 규모가 큰 퍼징을 할때는 주소 겹칠수도 있으니까 vm.assume(address ≠ ~~)로 제한해주는 것도 좋을 것
  • Forge-std와 겹치는 기능도 일부 있을 수 있음

Environment 관련

  • block 관련

    • warp : block.timestamp 설정
    • roll : block.number 설정
    • fee: block.basefee 설정
    • difficulty: block.difficulty 설정
    • chainId: block.chainid 설정
    • coinbase: block.coinbase 설정
  • account 관련

    • store(account, slot, value): account의 slot에 value를 저장함

    • load(account, slot): account의 slot에 들어가있는 값을 불러옴

      /// contract LeetContract {
      ///     uint256 private leet = 1337; // slot 0
      /// }
      
      vm.store(address(leetContract), bytes32(uint256(0)), bytes32(uint256(31337)));
      bytes32 leet = vm.load(address(leetContract), bytes32(uint256(0)));
      emit log_uint(uint256(leet)); // 31337
    • etch(who, code): code가 배포될 주소를 지정할 수 있음

      bytes memory code = address(awesomeContract).code;
      address targetAddr = address(1);
      vm.etch(targetAddr, code);
      log_bytes(address(targetAddr).code); // 0x60806040523480156...
    • deal: ETH나 ERC20을 배당

    • prank / startPrank / stopPrank: msg.sender 설정

    • setNonce / getNonce: 트랜잭션을 위한 nonce 설정

  • record 관련

    • record / accesses

      • record: 모든 storage read/write를 기록하도록 함

      • 기록된 정보는 accesses를 통해 접근 가능

      • write는 additional read로 count된다.

        /// contract NumsContract {
        ///     uint256 public num1 = 100; // slot 0
        ///     uint256 public num2 = 200; // slot 1
        /// }
        
        vm.record();
        numsContract.num2();
        (bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(
          address(numsContract)
        );
        emit log_uint(uint256(reads[0])); // 1
      • 만약 reads[1]에 접근한다면 num2를 선언할때 1번 쓰고 1번 읽었으니 2를 출력할 것.

      • 왜 write횟수를 read에 더하는걸까? 불편할듯

    • recordLogs / getRecordedLogs

      • emit이 발생한 모든 이벤트를 기록하고 불러옴

        /// event LogCompleted(
        ///   uint256 indexed topic1,
        ///   bytes data
        /// );
        
        vm.recordLogs();
        
        emit LogCompleted(10, "operation completed");
        
        Vm.Log[] memory entries = vm.getRecordedLogs();
        
        assertEq(entries.length, 1);
        assertEq(entries[0].topics[0], keccak256("LogCompleted(uint256,bytes)"));
        assertEq(entries[0].topics[1], bytes32(uint256(10)));
        assertEq(abi.decode(entries[0].data, (string)), "operation completed");
  • mockCall

    • mock: 흉내 / mockingbird: 앵무새
    • mockCall(where, value, data, retdata)
    • where로 향하는 call을 따라함
      • calldata가 data와 어느정도 매치해야되고, retdata를 반환하는 call에 대해서만
      • msg.value가 있는 경우 value 인자 사용
    • 우선순위
      • calldata와 data 일치 여부
      • calldata와 data의 유사성 여부 (첫 바이트부터 순차적으로 확인)
      • 유사성이 발견되면 call을 날리고 반환값을 retdata에 저장
      • msg.value가 필요하면 calldata 확인 이후 후순위 확인
    • 알려진 이슈
      • where가 CA가 아닐 경우 revert될 수 있음 (extcodesize 확인하는 경우 때문)
      • internal call의 경우 동작 하지 않을 수 있음
    • Example
      function testMockCall() public {
          assertEq(example.pay{value: 10}(1), 1);
          assertEq(example.pay{value: 1}(2), 2);
          vm.mockCall(
              address(example),
              10,
              abi.encodeWithSelector(example.pay.selector),
              abi.encode(99)
          );
          assertEq(example.pay{value: 10}(1), 99);
          assertEq(example.pay{value: 1}(2), 2);
      }
    • mockCall은 clearMockedCalls 호출 전까지 유효함
  • Broadcast

    • 현재 call depth에서 다음 call에 msg.sender로서 역할을 수행할 account를 지정

    • address / private_key 둘 다 사용 가능

      function deploy() public {
          cheats.broadcast(ACCOUNT_A);
          Test test = new Test();
      
          // this won't generate tx to sign
          uint256 b = test.t(4);
      
          // this will
          cheats.broadcast(ACCOUNT_B);
          test.t(2);
      
          // this also will, using a private key from your environment variables
          cheats.broadcast(vm.envUint("PRIVATE_KEY"));
          test.t(3);
      }
      • 변수 선언해서 집어넣는거는 현재 depth에서 벗어남 ⇒ X
      • broadcast 이후 바로 테스트 수행하는건 가능
    • startBroadcast / stopBroadcast를 사용해 조절 가능

      function t(uint256 a) public returns (uint256) {
          uint256 b = 0;
          emit log_string("here");
          return b;
      }
      
      function deployOther() public {
          vm.startBroadcast(ACCOUNT_A);
          Test test = new Test();
      
          // will trigger a transaction
          test.t(1);
      
          vm.stopBroadcast();
      
          // broadcast again, this time using a private key from your environment variables
          vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
          test.t(3);
          vm.stopBroadcast();
      }
  • Gas Metering

    • pauseGasMetering / resumeGasMetering
    • 특정 구역을 가스 연산에서 제외시켜 실행할 수 있음
    • 필요 없는 코드에 대한 gas metering을 꺼서 gas cost에 대한 정확한 이해 가능
    • 하지만 이 경우 DoS에 서비스를 그대로 노출시킬 수 있음. 배포해 테스트하려고 할 경우 Timeout을 설정해야 할 것

Assertion 관련

  • expectRevert / expectEmit / expectCall

  • expectRevert

    • 직후의 call이 revert되지 않으면 자신이 revert
    • 정해진 메시지대로 revert하지 않으면 revert를 내게 할 수도 있음
  • expectEmit

    • 한 이벤트의 Emit에 대해 최대 3개의 Topic check를 강제함
    • Topic은 type에 “indexed”를 붙여서 관리함
    event Transfer(address indexed from, address indexed to, uint256 amount);
    
    function testERC20EmitsTransfer() public {
        // We check that the token is the event emitter by passing the address as the fifth argument.
        vm.expectEmit(true, true, false, true, address(myToken));
        emit MyToken.Transfer(address(this), address(1), 10);
    
        // We perform the call.
        myToken.transfer(address(1), 10);
    }
    • 1st(true): from 주소가 출력되어야 함
    • 2nd(true): to 주소가 출력되어야 함
    • 3rd(false): indexed가 붙은 변수는 두개임. 3번째는 뭐든 상관 없음.
    • 4th(true): 4번째는 data가 들어감. amount 출력하는지 확인
    • 5th(address): 이벤트 출력하는 개체 주소 일치 여부 확인
  • expectCall

    • mockCall과 거의 비슷, 설명도 아예 같다.

Fuzzing 관련

  • assume
    • fuzzing input으로 들어올 값에 대해 특정 조건을 설정한다.
    • 매우 narrow한 값을 지정할 때 사용되며, 보통은 bound로 관리함.
    • bound가 느리기 때문에 작은 범위 테스트가 필요할때는 assume 써도 됨.

Forking 관련

  • Test > Fork Testing > 멀티포크를 통해 같은 설명을 볼 수 있음.

External

General

  • ffi

    • Differential Test 때 설명한 바 있다.

    • 쉘 커맨드를 실행할 수 있다.

    • 유의사항

      • 띄어쓰기를 지원하지 않으므로 배열로 선언해줘야 한다.

        string[] memory inputs = new string[](3);
        inputs[0] = "echo";
        inputs[1] = "-n";
        inputs[2] = "gm";
        
        bytes memory res = vm.ffi(inputs);
        assertEq(string(res), "gm");
      • output은 \n 포함 안되어있으니 유의

      • 작성한 쉘코드는 프로젝트 root에서 실행됨에 유의

    • toString 치트코드로 아무 데이터나 string으로 변환할 수 있으니 변환해서 argument로 패싱하면 더 유용하게 사용 가능할 것

  • projectRoot

    • 현재 foundry 프로젝트의 루트 디렉토리를 반환

Bytecode 관련

  • getCode

    • 프로젝트 내 컨트랙트의 path를 input으로 받아 바이트코드를 반환함

    • 배포 안해도 생성될 예정인 바이트코드 받아올 수 있음

    • 컨트랙트 디렉토리에 대해 읽기 권한 필요함.

      // set in foundry.toml
      fs_permissions = [{ access = "read", path = "./out"}]
    • example

      // Deploy
      bytes memory args = abi.encode(arg1, arg2);
      bytes memory bytecode = abi.encodePacked(vm.getCode("MyContract.sol:MyContract"), args);
      address deployed;
      assembly {
          deployed := create(0, add(bytecode, 0x20), mload(bytecode))
      }
      
      // Set the bytecode of an arbitrary address
      vm.etch(targetAddr, deployed.code);
  • getDeployedCode

    • 이미 배포된 컨트랙트에 대한 바이트코드 받아옴
    • 프로젝트에 들어있는 컨트랙트만 가능
    • 컨트랙트 디렉토리에 대해 읽기 권한 필요함.

환경변수 관련

  • setEnv

    • 환경변수 설정 가능.
      string memory key = "hello";
      string memory val = "world";
      cheats.setEnv(key, val);
    • key에 =, null 불가능, value에 null 불가능
  • envOr

    • 환경변수 읽어옴
    • 어떤 타입이든 상관없음
    • delimiter로 배열 읽어올 수도 있음
    • explicit casting과 dynamic array 사용 권장
    // single
    address owner;
    
    function setUp() {
      owner = vm.envOr("OWNER", address(this));
    }
    
    // multi
    function envBadTokens() public {
      address[] memory defaultBadTokens = new address[](0);
      address[] memory badTokens = vm.envOr("BAD_TOKENS", ",", defaultBadTokens);
    }
  • env{Bool, Uint, Int, Address, Bytes32, String, Bytes}

    • 환경변수를 특정 타입으로 변환하여 출력을 강제할 수도 있음

JSON 관련

JSON Parse

  • vm.parseJson(json, key)

  • 특징

    • JSON 파일을 string 형태로 파싱함
    • vm.readFile()과 붙어서 사용함
    • 더 나은 UX를 원하면 forge-std의 stdJson 붙여서 사용 가능
    • key 사용하면 원하는 value만 출력 가능, 아닌 경우 전체 json 출력
    • abi-encoded bytes array의 형태로 출력해줌
  • Encoding Rule

    • 타입에 따라 인코딩되는 룰이 다름
      • 양수: uint256
      • 음수: int256
      • H160으로 디코딩 가능한 0x로 시작하는 string: address
      • 0x로 시작하는 string: bytes
      • 0x로 시작하면서 길이가 66인 string: bytes32
      • address, bytes가 아닌 string: string
      • 배열: dynamic array, 타입은 첫 원소의 타입으로 지정
      • object( {} ): tuple
    • 타입을 변환하는 과정에서 유연하지 못한 점들이 있을 수 있음.. strict rule에 따라 타입이 강제되기 때문
      {
        "hexUint": "0x12C980",
        "stringUint": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
        "numberUint": 115792089237316195423570985008687907853269984665640564039457584007913129639935
      }
      • 셋 다 같은 값이지만 bytes, string, uint256으로 다른 타입으로 지정됨
  • JSON 오브젝트는 solidity struct로 디코딩.

    // input
    {
      "a": 43,
      "b": "sigma"
    }
    
    // output
    struct Json {
        uint256 a;
        string b;
    }
    • 타입을 잘못 지정해서 넣으면 revert 되지는 않는데 틀린 값이 들어간다. 유의할 것.
  • JSON 디코딩 꿀팁

    • JSON 오브젝트가 hex 값을 갖고 있는 경우 bytes로 디코딩될 것임.
    • uint로 변환하려면 bytes로 값을 받아오는 중간 struct를 하나 더 선언해주면 됨.
      • hex → bytes → uint로 변환될 것임. forge-std의 치트코드를 사용해서 bytes를 uint로 변환할 수 있음
  • StdJson 사용 방법

    import "../StdJson.sol";
    string: using stdJson for string;
    
    string memory root = vm.projectRoot();
    string memory path = string.concat(root, "/src/test/fixtures/broadcast.log.json");
    string memory json = vm.readFile(path);
    bytes memory transactionDetails = json.parseRaw(".transactions[0].tx");
    RawTx1559Detail memory rawTxDetail = abi.decode(transactionDetails, (RawTx1559Detail));
  • 유의사항

    • 현재까지는 EIP-1559 호환가능한 체인의 artifact에 대해서만 지원함
    • 전체 broadcast.json artifact에 대한 파싱은 현재 지원하지 않고, transactions나 receipt 등 단일 값에 대한 파싱만을 지원함
    function testReadEIP1559Transactions() public {
      string memory root = vm.projectRoot();
      string memory path = string.concat(root, "/src/test/fixtures/broadcast.log.json");
      Tx1559[] memory transactions = readTx1559s(path);
    }
    
    struct Tx1559 {
      string[] arguments;
      address contractAddress;
      string contractName;
      string functionSig;
      bytes32 hash;
      Tx1559Detail txDetail;
      string opcode;
    }
    
    struct Tx1559Detail {
      AccessList[] accessList;
      bytes data;
      address from;
      uint256 gas;
      uint256 nonce;
      address to;
      uint256 txType;
      uint256 value;
    }

JSON Serialize / Write

  • vm.serializeJson(objectKey, valueKey, value)
  • value를 serialized JSON 오브젝트로 변환해줌
  • serialize한 value를 writeJson을 통해 json 파일로 저장할 수 있음
// exampel data: { "boolean": true, "number": 342, "object": { "title": "finally json serialization" } }
string memory obj1 = "some key";
vm.serializeBool(obj1, "boolean", true);
vm.serializeUint(obj1, "number", uint256(342));

string memory obj2 = "some other key";
string memory output = vm.serializeString(obj2, "title", "finally json serialization");

// serialize 시도, 성공
// serialize 실패시 그냥 string으로 저장됨에 유의
string memory finalJson = vm.serializeString(obj1, "object", output);

vm.writeJson(finalJson, "./output/example.json");

Utilities

  • forge-std에 있는 함수들과 유사한 부분이 많음

  • addr

    • private key를 받아 주소를 연산해줌
    address alice = vm.addr(1);
    emit log_address(alice); // 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
  • sign

    • private key와 hash를 받아 v, r, s를 반환함
    address alice = vm.addr(1);
    bytes32 hash = keccak256("Signed by Alice");
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash);
    address signer = ecrecover(hash, v, r, s);
    assertEq(alice, signer); // [PASS]
  • label

    • 특정 addr에 이름을 붙여줌
  • deriveKey

    • 니모닉으로부터 private key를 불러옴
    • path와 index를 지정할 수 있음
    string memory mnemonic = "test test test test test test test test test test test junk";
    
    // function deriveKey(
    //  string calldata mnemonic,
    //  string calldata path,
    //  uint32 index
    // ) external returns (uint256);
    
    // path : m/44'/60'/0'/0/{index}
    uint256 privateKey = vm.deriveKey(mnemonic, "m/44'/60'/0'/1/", 0);
  • rememberKey

    • private key를 forge 로컬 지갑에 저장함
    • broadcasting에 쓰일 주소를 반환 (저장한 private key로 생성한 주소를 반환)
  • parse{Bytes, Address, Uint, Int, Bytes32, Bool}

    • string을 받아서 특정 타입으로 변환함
  • toString

    • 임의의 데이터를 받아서 string으로 변환함
    • ffi 등 string을 요구하는 작업에 매우 유용함

Snapshot

  • vm.snapshot()
    • 현재 블록체인의 state를 저장함.
    • uint256 타입의 identifier를 부여함.
  • vm.revertTo(snapshotNum)
    • 찍어둔 스냅샷으로 롤백
    • state가 원상복구된다.

RPC 관련

  • foundry.toml에 저장된 모든 rpc_endpoints에 대한 접근 기능을 제공한다.
// in foundry.toml...
[rpc_endpoints]
optimism = "https://optimism.alchemyapi.io/v2/..."
mainnet = "${RPC_MAINNET}"

// contract
string memory url = vm.rpcUrl("optimism");
assertEq(url, "https://optimism.alchemyapi.io/v2/...");

string[2][] memory allUrls = vm.rpcUrls();
assertEq(allUrls.length, 2);

string[2] memory val = allUrls[0];
assertEq(val[0], "optimism");

string[2] memory env = allUrls[1];
assertEq(env[0], "mainnet");

File 관련

  • {read, write, close, remove}File(string calldata) returns (string meory)

  • 파일에 대한 읽기, 쓰기, 닫기, 지우기 등 전반적인 기능 제공

    string memory path = "file.txt";
    string memory data = "hello world";
    vm.writeFile(path, data);
    
    assertEq(vm.readFile(path), data);
  • readLine, writeLine 가능

    string memory path = "output.txt";
    
    string memory line1 = "first line";
    vm.writeLine(path, line1);
    
    string memory line2 = "second line";
    vm.writeLine(path, line2);
  • foundry.toml에 파일 접근 권한에 대한 설정 필요함.

    fs_permissions = [{ access = "read", path = "./a"}]
    fs_permissions = [{ access = "read-write", path = "./b"}]
    fs_permissions = [{ access = "write", path = "./c"}]
    ...

Configuration (Comprehensive)

Overview

  • Profile 단위로 config 가능
    • [profile.${name}] 등으로 이름 부여해서 설정
  • 홈 디렉토리에 foundry.toml 만들어두면 글로벌 설정 가능.
    • 프로젝트 루트가 아닌 home!
  • FOUNDRY* 나 DAPP*를 prefix로 붙이면 기존 환경변수를 override할 수 있다.
    • 예외) FOUNDRY_FFI, DAPP_FFI는 안됨.

Project 관련

  • src, test, out, libs, cache, cache_path, force
  • 모두 FOUNDRY나 DAPP prefix로 위치 변경 가능
    • !broadcast는 따로 변경이 되는지 안되는지에 대한 설명이 없어 확인이 필요함.

Solidity Compiler 관련

General

  • remappings

    • 프로젝트 루트에 remappings.txt 저장하면 알아서 가져가긴 함.

    • 쓸모있는 예시

      @openzeppelin/=node_modules/@openzeppelin/openzeppelin-contracts/
      
      // =>
      import "@openzeppelin/contracts/utils/Context.sol";
      // ==
      import "node_modules/@openzeppelin/openzeppelin-contracts/contracts/utils/Context.sol";
  • auto_detect_remappings

    • 기본적으로 true로 설정되어 있음
    • remappings.txt나 foundry.toml에 설정해두지 않아도 라이브러리 가져오는 이유가 이 때문임.
    • 만약 이를 통제하고 싶다면 이 config를 false로 설정해주면 됨
  • allow_path

    • 소스코드를 다른 디렉토리에서 가져올 수 있도록 경로를 넣어줄 수 있다.
    • pnmp 등 복잡한 workspace에서 작업할 때 사용할 수 있다.
  • include_path

    • import에 대한 콜백 경로를 지정할 수 있다.
    • 써드파티 라이브러리를 통해 컨트랙트를 가져와야하거나 하는 경우 경로가 고정되어 있지 않으므로 상위 디렉토리를 include하는 방법이 있겠다.
  • libraries

    • 특정 컨트랙트를 라이브러리로 매핑할 수 있다.
    libraries = [src/MyLibrary.sol:MyLibrary:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4, ...]
  • solc_version

    • solidity compiler 버전 지정
  • auto_detect_solc

    • 컴파일러 버전 자동 감지, 기본값으로 true가 설정되어있다.
  • offline

    • true로 설정되어 있을 경우 solc 버전을 새로 받아오지 않는다.
    • offline과 auto_detect_solc가 둘다 true로 설정되어 있을 경우 required version은 받아오지만 missing version은 다운받지 않는다.
  • ignored_error_codes

    • 특정 에러코드를 무시한다.
    • 유효한 에러코드는 다음과 같다.
      license: 1878
      code-size: 5574
      func-mutability: 2018
      unused-var: 2072
      unused-param: 5667
      unused-return: 9302
      virtual-interfaces: 5815
      missing-receive-ether: 3628
      shadowing: 2519
      same-varname: 8760
      unnamed-return: 6321
      unreachable: 5740
      pragma-solidity: 3420
  • deny_warnings

    • warning을 끈다.
  • evm_version

    • value는 반드시 EVM 하드포크 명명이어야 한다.
    • london, byzantium 등등
    • 기본값은 london으로 설정되어있다.
  • revert_strings

    • revert시 출력되는 메시지에 대한 관리를 할 수 있다.
    • default: 유저가 지정한 메시지만을 출력한다.
    • strip: 모든 메시지를 끈다.
    • debug: 디버그 모드로, 컴파일러가 생성한 revert 메시지를 출력한다. 메시지가 없을 경우 유저가 지정한 메시지를 출력한다.
    • verboseDebug: 유저가 지정한 메시지에 추가적으로 정보를 출력한다. (아직 구현 안됨)
  • extra_output_files

    • artifacts 디렉토리에 추가적인 output을 생성한다.
    • metadata, ir, irOptimized, ewasm, evm.assembly 등의 파일을 생성할 수 있다.
    extra_output = [
      "abi",
      "evm.bytecode",
      "evm.deployedBytecode",
      "evm.methodIdentifiers",
    ]
  • bytecode_hash

    • bytecode에 붙는 metadata hash의 method를 지정한다.
    • 기본값은 ipfs이고, bzzr1 등의 값을 넣을 수 있다.
  • sparse_mode

    • 빌드할 때 sparse mode를 허용한다고 하는데, 참조 doc에 들어가니 삭제되어있다. TBU

Optimizer

  • optimizer
    • 최적화 허용 여부
  • optimizer_runs
    • 최적화 몇번 수행할지
  • via_ir
    • Intermediate Representation을 이용한 최적화 허용 여부
  • [optimizer_details]
    • optimizer의 행동을 조절할 수 있음
    • peephole, inliner, jumpdest_remover, order_literal, deduplicate, cse, constant_optimizer, yul 등이 있다.
    • 자세한건 추후 작성할 solidity 공식 문서에 추가할 것. Using the Compiler — Solidity 0.8.20 documentation
  • [optimizer_details.yul_details]
    • Yul에 대한 detail을 설정할 수 있다.
    • stack_allocation: stack slot에 대한 free를 더 일찍함으로써 allocation 성능 향상
    • optimizer_steps: 최적화 정도 설정

Solidity Model Checker

  • 맥 OS와 리눅스에서 사용 가능한 빌트인 체커 모듈이다.
  • 특정한 에러나 동작을 잡을지 말지에 대해 설정할 수 있다.
  • [model_checker] 로 config 가능
    [profile.default.model_checker]
    contracts = {'/path/to/project/src/Contract.sol' = ['Contract']}
    engine = 'chc'
    timeout = 10000
    targets = ['assert']
  • verify할 컨트랙트를 따로 지정할 수 있다는 장점이 있음
  • 엔진은 chc가 권장되지만 bmc, all 또한 사용 가능
  • model_checker.invariants
    • contract: 상수 불변값 지정
    • reentrancy: reentrancy 속성 지정
  • model_checker.show_unproved
    • unproved target에 대해 output을 만들지 말지 설정
  • model_checker.solver
    • solver 지정
    • cvc4, eld, smtlib2, z3 설정 가능
  • targets
    • 모델 체커 타겟 설정 가능
    • 기본값은 assert로 설정
      • assert
      • over/underflow
      • divByZero
      • constantCondition
      • popEmptyArray
      • outOfBounds
      • default (all above)

Testing

  • test 관련된 여러 config가 가능. 기본값에 대해 알아둘 필요도 있다.

General

  • verbosity
    • 기본값: 0
    • -v, -vv 등 테스트 레벨 설정
  • ffi
    • 기본값: false
    • ffi 치트코드를 기본으로 켤지 말지에 대해 설정
    • 환경 내 아무 프로그램이나 실행시켜 보안 이슈가 생길 수 있으므로 유의
  • sender, tx_origin
    • msg.sender, tx.origin 값 설정
    • 기본값: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
  • initial_balance
    • 기본값: 0xffffffffffffffffffffffff
  • block_number
    • 기본값: 1
  • chain_id
    • 기본값: 31337
  • gas_limit
    • 기본값: 9223372036854775807
    • forge로 인해 gas limit은 u64.max를 넘을 수 없다.
    • 각 테스트케이스 별 gas limit이다.
  • gas_price, block_base_fee_per_gas, block_difficulty
    • 기본값: 0
  • block_coinbase
    • 기본값: 0x0000000000000000000000000000000000000000
  • block_timestamp
    • 기본값: 1
  • gas_reports
    • 기본값: 전부
  • no_storage_caching
    • 기본값: false
    • $HOME/.foundry/cache/<chain id>/<block number>에 기본적으로 저장
  • rpc_storage_caching
    • chains
      • 기본값: all
      • 체인 이름 list 들어갈 수 있음
    • endpoints
      • 기본값: remote
      • 캐시될 RPC 지정
      • all, regex 패턴 지정 가능
    • eth_rpc_url
      • rpc 호출에 사용될 rpc 서버 url 지정
    • etherscan_api_key
    • match-test / match-contract / no-match-test / no-match-contract / match-path / no-match-path
      • 테스트 포함/제외할 메서드, 컨트랙트, 경로 지정
    • block_gas_limit
    • memory_limit
      • 기본값: 33554432 bytes
    • names
      • 컴파일된 컨트랙트 이름 출력 여부
    • sizes
      • 컴파일된 컨트랙트 크기 출력 여부

Fuzz

  • [fuzz] 섹션으로 관리 가능
  • runs
    • 기본값: 256
    • 퍼징 돌릴 횟수
  • max_test_rejects
    • 기본값: 65536
    • 이 횟수 넘기면 퍼징 강제종료함
  • seed
    • 난수생성기에 사용할 시드
  • dictionary_weight
    • 기본값: 40
  • include_storage
    • storage 내의 value를 포함시킬지 여부
    • 기본값: true
  • include_push_bytes
    • bytes 값에 대한 push를 허용할지 여부
    • 기본값: true

Invariant

  • [invariant] 섹션으로 관리 가능
  • runs
    • 기본값 256
    • invariant test group 몇번돌릴지
  • depth
    • 한 run에 call 몇번할건지
  • fail_on_revert
    • revert나면 invariant fuzzing 중단
  • call_override
    • unsafe external call을 오버라이드함
    • reentrancy check 할 때 등등
    • 기본값은 false
  • dictionary_weight
    • 0 ~ 100
    • 기본값은 80
  • include_storage / include_push_bytes
    • fuzz와 같은 기능

Formatter

  • [fmt] 섹션으로 관리 가능
  • line_length / 120
  • tab_width / 4
  • bracket_spacing / false
  • int_types
    • 기본값: long
    • long, short, preserve(소스코드가 정한대로) 가능
  • multiline_func_header
    • 함수 헤더가 한줄로 안끝나는 경우 포맷팅
    • 기본값: attributes_first
    • attributes_first, params_first, all 가능
    • all은 전부 multiline 해버리기
  • quote_style
    • 기본값: double
    • double, single, preserve(소스코드에 지정된대로) 가능
  • number_underscore
    • 기본값: preserve
    • preserve, thousands, remove 가능
    • thousands: 1_000_000 등 1000단위마다 underscore
  • override_spacing
    • override attribute에 대해 spacing할지말지
  • wrap_comments
    • line_length에 도달한 comment를 wrap할지말지
  • ignore
    • 포맷팅 무시할 파일에 대한 리스트

Document Generator

  • [doc] 섹션으로 관리
  • out
    • 기본값: docs 폴더
    • doc 저장 위치 관리
  • title
  • book
    • book.toml이 저장될 경로 지정
  • repository
    • git repo 주소
    • git 소스파일과 link하기 위해 사용함
    • 지정된 값이 없는 경우 최근 origin url을 사용함
  • ignore
    • document generation에서 제외할 파일의 리스트

Etherscan

  • 각 체인별로 API key, chain 이름, 이더스캔 API url 저장
[etherscan]
mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" }
mainnet2 = { key = "ABCDEFG", chain = "mainnet" }
optimism = { key = "1234567" }
unknown_chain = { key = "ABCDEFG", url = "<etherscan api url for this chain>" }

CI/CD

  • CI를 위한 다양한 툴 지원

  • Github Actions

    on: [push]
    
    name: test
    
    jobs:
      check:
        name: Foundry project
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
            with:
              submodules: recursive
    
          - name: Install Foundry
            uses: foundry-rs/foundry-toolchain@v1
            with:
              version: nightly
    
          - name: Run tests
            run: forge test -vvv
  • Travis CI

    language: rust
    cache:
      cargo: true
      directories:
        - $HOME/.foundry
    
    install:
      - curl -L https://foundry.paradigm.xyz | bash
      - export PATH=$PATH:$HOME/.foundry/bin
      - foundryup -b master
    
    script:
      - forge test -vvv
  • GitLab CI

    variables:
      GIT_SUBMODULE_STRATEGY: recursive
    
    jobs:
      image: ghcr.io/foundry-rs/foundry
      script:
        - forge install
        - forge test -vvv

VSCode와의 연동

  • https://github.com/juanfranblanco/vscode-solidity 이 익스텐션을 사용하는 것을 권장함
  • 위 익스텐션이 Foundry와 잘 연동되기 위해서 약간씩 수정해야할 것들이 있음
    • Remappings
      • remappings를 remappings.txt에 저장
      • foundry.toml에 저장되어 있다면 remappings.txt에 복사해놓는걸 권장
      • 그냥 foundry가 제공하는거 쓰고싶다면
        forge remappings > remappings.txt
    • Dependencies
      • .vscode/settings.json 에 dependency 추가할 수도 있음
        // source code와 dependency에 대한 디렉토리 설정
        {
          "solidity.packageDefaultDependenciesContractsDirectory": "src",
          "solidity.packageDefaultDependenciesDirectory": "lib"
        }
    • Formatter
      • Foundry의 빌트인 포맷터를 format on save로 사용하고 싶다면 .vscode/settings.json 에 다음 내용 추가
        {
          "editor.formatOnSave": true,
          "[solidity]": {
            "editor.defaultFormatter": "JuanBlanco.solidity"
          },
          "solidity.formatter": "forge",
        }
    • Solc Version
      • foundry.toml의 default profile에 다음 내용 설정
        solc = "0.8.17"

Shell 자동완성

  • bash, elvish, fish, powershell, zsh에 대한 자동완성 지원

  • zsh

    // ~/.zshrc
    autoload -U compinit
    compinit -i
    
    // Non-ARM
    $ forge completions zsh > /usr/local/share/zsh/site-functions/_forge
    $ cast completions zsh > /usr/local/share/zsh/site-functions/_cast
    $ anvil completions zsh > /usr/local/share/zsh/site-functions/_anvil
    
    // ARM
    $ forge completions zsh > /opt/homebrew/completions/zsh/_forge
    $ cast completions zsh > /opt/homebrew/completions/zsh/_cast
    $ anvil completions zsh > /opt/homebrew/completions/zsh/_anvil
  • fish

    $ mkdir -p $HOME/.config/fish/completions
    $ forge completions fish > $HOME/.config/fish/completions/forge.fish
    $ cast completions fish > $HOME/.config/fish/completions/cast.fish
    $ anvil completions fish > $HOME/.config/fish/completions/anvil.fish
    $ source $HOME/.config/fish/config.fish
  • bash

    $ mkdir -p $HOME/.local/share/bash-completion/completions
    $ forge completions bash > $HOME/.local/share/bash-completion/completions/forge
    $ cast completions bash > $HOME/.local/share/bash-completion/completions/cast
    $ anvil completions bash > $HOME/.local/share/bash-completion/completions/anvil
    $ exec bash

Static Analyzer와의 연동

  • 오픈소스 정적분석기인 Slither와 Mythril과의 연동을 지원한다.

Slither

  • slither.config.json 파일에 다음을 추가한다.
    {
      "filter_paths": "lib"
    }
  • 전체 프로젝트에 대해 slither 수행시
    $ slither .
  • slither는 foundry 프로젝트에서 자동으로 remappings를 탐지하므로 따로 구성해줄 필요 없음
  • 특정 .sol 파일을 검사하고 싶을 때
    {
      "solc_remaps": [
        "ds-test/=lib/ds-test/src/",
        "forge-std/=lib/forge-std/src/"
      ]
    }
  • 추가적으로 slither의 solc 컴파일러를 foundry 버전과 같은 버전으로 맞춰줘야 함
    pip3 install slither-analyzer
    pip3 install solc-select
    solc-select install 0.8.13
    solc-select use 0.8.13
    slither .
  • slither --config-file <path>/file.config.json . 커맨드로 config 파일 위치 조절 가능

Mythril

  • mythril.config.json에 다음 내용 추가
    {
      "remappings": [
        "ds-test/=lib/ds-test/src/",
        "forge-std/=lib/forge-std/src/"
      ],
      "optimizer": {
        "enabled": true,
        "runs": 200
      }
    }
  • mythril 설치를 위해 rustc를 nightly로 변경
    rustup default nightly
    pip3 install mythril
    myth analyze src/Contract.sol --solc-json mythril.config.json
  • 마찬가지로 myth analyze src/Counter.sol --solc-json mythril.config.json를 통해 config 위치 조절 가능

Hardhat과의 연동

작업중인 foundry 프로젝트에 hardhat 붙이는 방법

  • 왜 호환성이 안좋을까?

    • hardhat은 node_modules를 통해 라이브러리를 관리하는데, foundry의 lib에 들어간 라이브러리를 node_modules의 디렉토리 구조에 맞게 바꾸기 쉽지 않음.
  • 그래서 컨트랙트가 solc 컴파일러로 들어가기 전에 hardhat 플러그인을 통해 전처리시키는 방법을 권장함.

    • hardhat-preprocessor npm: hardhat-preprocessor
    • remappings.txt 파일을 읽어와서 lib 폴더 내의 라이브러리 구조를 node에 맞게 고쳐버림
    • 메모리에서만 일어나는 일이므로 실제 foundry 프로젝트가 바뀌는 일은 없다고 보면 된다.
    • 현재 hardhat은 foundry에 설치된 라이브러리에 대한 거의 대부분의 호환성을 가짐
  • 좋은 example repo 없니? https://github.com/foundry-rs/hardhat-foundry-template

    // init hardhat in forge project
    $ npm init
    $ npm install --save-dev hardhat
    $ npx hardhat
    $ forge remappings > remappings.txt
    
    $ npm install --save-dev hardhat-processor
    
    //// hardhat.config.ts ...
    import "hardhat-preprocessor";
    import fs from "fs";
    
    // add getRemappings function ...
    function getRemappings() {
      return fs
        .readFileSync("remappings.txt", "utf8")
        .split("\n")
        .filter(Boolean) // remove empty lines
        .map((line) => line.trim().split("="));
    }
    
    // add HardhatUserConfig object ...
    ...
    preprocess: {
      eachLine: (hre) => ({
        transform: (line: string) => {
          if (line.match(/^\s*import /i)) {
            for (const [from, to] of getRemappings()) {
              if (line.includes(from)) {
                line = line.replace(from, to);
                break;
              }
            }
          }
          return line;
        },
      }),
    },
    paths: {
      sources: "./src",
      cache: "./cache_hardhat",
    },
    ...

작업중인 hardhat 프로젝트에 foundry 붙이는 방법

  • foundry 빈 프로젝트 생성

  • lib/forge-std를 hardhat 프로젝트에 붙여넣음

  • foundry.toml을 hardhat 프로젝트 루트 디렉토리에 붙여넣음

    • src, out, test, cache_path를 다음과 같이 바꿈
    [profile.default]
    src = 'contracts'
    out = 'out'
    libs = ['node_modules', 'lib']
    test = 'test/foundry'
    cache_path  = 'forge-cache'
    
    # See more config options https://book.getfoundry.sh/reference/config.html
  • remappings.txt 생성 → vscode와의 호환성 맞추기 위해

    ds-test/=lib/forge-std/lib/ds-test/src/
    forge-std/=lib/forge-std/src/
  • test/foundry 디렉토리 만들고 foundry test 작성

    • Contract.t.sol 작성하고
      forge test
      돌리면 끝!

Docker와의 연동

  • Dockerfile 생성

    # Use the latest foundry image
    FROM ghcr.io/foundry-rs/foundry
    
    # Copy our source code into the container
    WORKDIR /app
    
    # Build and test the source code
    COPY . .
    RUN forge build
    RUN forge test
  • 도커 빌드

    $ docker build --no-cache --progress=plain -t <docker_name> .
    
    // example
    $ docker run nft-deployer --rpc-url $RPC_URL --constructor-args "ForgeNFT" "FNFT" "https://ethereum.org" --private-key $PRIVATE_KEY ./src/NFT.sol:NFT
    No files changed, compilation skipped
    Deployer: 0x496e09fcb240c33b8fda3b4b74d81697c03b6b3d
    Deployed to: 0x23d465eaa80ad2e5cdb1a2345e4b54edd12560d3
    Transaction hash: 0xf88c68c4a03a86b0e7ecb05cae8dea36f2896cd342a6af978cab11101c6224a9
  • 도커 실행

    $ docker run foundry "<COMMAND> --rpc-url $RPC_URL latest"
    
    // example
    $ docker run foundry "cast block --rpc-url $RPC_URL latest"
    baseFeePerGas        "0xb634241e3"
    difficulty           "0x2e482bdf51572b"
    extraData            "0x486976656f6e20686b"
    gasLimit             "0x1c9c380"
    gasUsed              "0x652993"
    hash                 "0x181748772da2f968bcc91940c8523bb6218a7d57669ded06648c9a9fb6839db5"
    logsBloom            "0x406010046100001198c220108002b606400029444814008210820c04012804131847150080312500300051044208430002008029880029011520380060262400001c538d00440a885a02219d49624aa110000003094500022c003600a00258009610c410323580032000849a0408a81a0a060100022505202280c61880c80020e080244400440404520d210429a0000400010089410c8408162903609c920014028a94019088681018c909980701019201808040004100000080540610a9144d050020220c10a24c01c000002005400400022420140e18100400e10254926144c43a200cc008142080854088100128844003010020c344402386a8c011819408"
    miner                "0x1ad91ee08f21be3de0ba2ba6918e714da6b45836"
    mixHash              "0xb920857687476c1bcb21557c5f6196762a46038924c5f82dc66300347a1cfc01"
    nonce                "0x1ce6929033fbba90"
    number               "0xdd3309"
    parentHash           "0x39c6e1aa997d18a655c6317131589fd327ae814ef84e784f5eb1ab54b9941212"
    receiptsRoot         "0x4724f3b270dcc970f141e493d8dc46aeba6fffe57688210051580ac960fe0037"
    sealFields           []
    sha3Uncles           "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
    size                 "0x1d6bb"
    stateRoot            "0x0d4b714990132cf0f21801e2931b78454b26aad706fc6dc16b64e04f0c14737a"
    timestamp            "0x6246259b"
    totalDifficulty      "0x9923da68627095fd2e7"
    transactions         [...]
    uncles               []

Solidity Scripting

  • 제약이 많고 불친절한 forge create 대신 solidity로 컨트랙트를 배포할 수 있는 방법

  • 4개의 phase로 구성

    1. Local simulation
      • 로컬 EVM에서 컨트랙트 실행, vm.broadcast를 통한 외부 call 추가됨
      • rpc나 fork url이 주어진 경우 거기서 script 실행
    2. Onchain simulation
      • Optional
      • rpc나 fork url이 주어진 경우 1번 phase에서 수집한 트랜잭션을 순차적으로 수행
    3. Broadcasting
      • Optional
      • --broadcasting 플래그가 설정되어있고 2번 phase가 성공할 경우 2번에서 simulate한 트랜잭션을 broadcast함
    4. Verification
      • Optional
      • --verify 플래그가 설정되어 있고, API 키가 있고, 3번 phase가 성공했을 경우 etherscan 등을 통해 컨트랙트를 검증
    • 하지만 이런 플로우를 통해 트랜잭션을 검증하더라도 외부 state나 actor등의 작업(예: 프론트러닝) 등으로 인해 step2에서 시뮬레이팅한 것과 다른 결과값이 나올 수 있음에 유의해야 한다.
  • Setup

    • 필요한 환경변수 설정

      // .env
      GOERLI_RPC_URL=
      PRIVATE_KEY=
      ETHERSCAN_API_KEY=
    • foundry.toml 설정

      [rpc_endpoints]
      goerli = "${GOERLI_RPC_URL}"
      
      [etherscan]
      goerli = { key = "${ETHERSCAN_API_KEY}" }
    • 배포 스크립트 작성

      // script/NFT.s.sol
      
      // SPDX-License-Identifier: UNLICENSED
      pragma solidity ^0.8.13;
      
      import "forge-std/Script.sol";
      import "../src/NFT.sol";
      
      contract MyScript is Script {
          function run() external {
              uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
              vm.startBroadcast(deployerPrivateKey);
      
              NFT nft = new NFT("NFT_tutorial", "TUT", "baseUri");
      
              vm.stopBroadcast();
          }
      }
      • 핵심은 startBroadcast부터임
      • NFT 컨트랙트를 생성하고, start 이전에 가져온 private key를 이용해서 log를 찍고 배포한다.
      # To load the variables in the .env file
      source .env
      
      # To deploy and verify our contract
      forge script script/NFT.s.sol:MyScript --rpc-url $GOERLI_RPC_URL --broadcast --verify -vvvv
      • 로컬 ~ broadcast ~ verify 까지 일련의 동작이 수행된 이후 배포된다. Untitled
      • 고얼리 네트워크에 NFT 컨트랙트가 배포되는 것을 확인할 수 있다.
    • 로컬에 배포하기

      • 로컬 테스트넷인 Anvil에 배포할 수 있다.
      $ anvil
      $ forge script script/NFT.s.sol:MyScript --fork-url http://localhost:8545 --broadcast
      
      $ MNEMONIC=
      
      $ source .env
      $ anvil -m $MNEMONIC
      
      $ forge script script/NFT.s.sol:MyScript --fork-url http://localhost:8545 --broadcast

Cast 커맨드 모음

  • Cast란?

    • 이더리움 RPC call을 위한 CLI 툴
    • 컨트랙트 호출, 트랜잭션 발생, chain data 호출 등 거의 모든 기능 지원
  • Cast 커맨드 정리

    General

    • cast completions ${shell}
      • shell completion 지원
      cast completions zsh > $HOME/.oh-my-zsh/completions/_cast

    Chain 관련

    • cast chain-id
      • RPC에 연결되어 있는 chain ID 가져옴
      • --flashbots 플래그로 플래시봇 rpc도 확인 가능
    • cast chain
      • 체인 이름 가져옴
    • cast client
      • RPC 클라이언트 버전 가져옴

    Transaction 관련

    • cast publish

      • raw pre-signed 트랜잭션을 네트워크에 제출
      • 옵션
        • --async / --cast-async : 결과값 안기다림
      $ cast publish --rpc-url $RPC $TX
      $ cast publish --flashbots $TX
    • cast receipt

      • 트랜잭션 해시에 대한 receipt 제공
      $ cast receipt $TX_HASH
      $ cast receipt $TX_HASH blockNumber
    • cast send

      • 트랜잭션을 sign하고 제출함
      • 여러 옵션 부여 가능.. 너무 많음
      • 유용해보이는 옵션 정리
        • --resend
          • 직전에 사용한 논스 재사용
        • --create code [args]
          • 컨트랙트를 바이트코드로 배포
        • --ledger / --trezor / -f / --from
          • 하드웨어 또는 원격 월렛 설정
      $ cast send --ledger vitalik.eth --value 0.1ether
      $ cast send --ledger 0x... "deposit(address,uint256)" 0x... 1
      $ cast send 0x... "myfunction((address,uint256))" "(0x...,1)"
    • cast call

      • 트랜잭션을 보내지 않고 call 수행
      // ex1)
      cast call 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
        "balanceOf(address)(uint256)" 0x...
      
      // ex2)
      export CONTRACT=0xca7ca7bcc765f77339be2d648ba53ce9c8a262bd
      export TOKEN_ID=19938
      cast call $CONTRACT "tokenURI(uint256)(string)" $TOKEN_ID
      
      // ex3)
      cast call 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D \
       "getAmountsOut(uint,address[])" 1 "[0x6b...0f,0xc0...c2]"
    • cast rpc

      • JSON-PRC POST request 보냄
      $ cast rpc eth_getBlockByNumber "latest" "false"
      
      $ cast rpc --rpc-url https://mainnet.infura.io/v3/ eth_getBlockByNumber "latest" "false"
    • cast tx

      • 트랜잭션에 대한 정보 요청
      // get sender of transaction
      cast tx $TX_HASH from
      
      // get info of transaction
      cast tx $TX_HASH
    • cast run

      • 제출된 트랜잭션을 로컬 환경에서 실행하고 trace를 출력
      // 1. 그냥 간단한 transfer
      cast run 0xd15e0237413d7b824b784e1bbc3926e52f4726e5e5af30418803b8b327b4f8ca
      
      // 2. 트랜잭션 재실행
      cast run --quick \
      0xd15e0237413d7b824b784e1bbc3926e52f4726e5e5af30418803b8b327b4f8ca
      
      // 3. address label로 트랜잭션 재실행
      cast run \
      --label 0xc564ee9f21ed8a2d8e7e76c085740d5e4c5fafbe:sender \
      --label 0x40950267d12e979ad42974be5ac9a7e452f9505e:recipient \
      --label 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2:weth \
      0xd15e0237413d7b824b784e1bbc3926e52f4726e5e5af30418803b8b327b4f8ca
      
      // 4. 디버거 붙여서 트랜잭션 재실행
      cast run --debug \
      0xd15e0237413d7b824b784e1bbc3926e52f4726e5e5af30418803b8b327b4f8ca
    • cast estimate

      • 트랜잭션 가스비 연산 (estimated gas)
      cast estimate 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
        --value 0.1ether "deposit()"
    • cast access-list

      • 트랜잭션에 대한 접근 리스트 출력

    Block 관련

    • cast find-block
      • timestamp에 대해 가장 가까운 블록 넘버 찾아줌
      // 2021 new year!
      cast find-block 1609459200
    • cast gas-price
      • 현재 시점 가스 가격
    • cast block-number
      • 현재 시점 가장 최근 블록 넘버
    • cast basefee
      • 블록의 basefee 출력
      • 여러 태그로 관리할 수 있음
        • earliest, finalized, safe, latest, pending
        • 기본값은 latest
    • cast block
      • 블록 정보 출력
    • cast age
      • 블록의 timestamp 출력

    Account 관련

    • cast balance
      • wei 단위 balance 출력
    • cast storage
      • 특정 주소의 storage slot에 저장된 raw value 출력
      cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 {index}
    • cast proof
      • 특정 storage slot에 대한 storage proof 출력
      • accountProof, address, balance, codeHash, nonce, storageHash, …
    • cast nonce
    • cast code
      • 특정 주소의 컨트랙트에 대한 bytecode 출력

    ENS 관련

    • ENS: Ethereum Name Service (~ DNS)
    • cast lookup-address
      • ENS reverse lookup 수행
        // 1. ENS 받아오기
        $ cast lookup-address --rpc-url <rpc_url> $ADDRESS
        // 2. ENS 받아오고 ENS로부터 주소 받아와서 교차검증
        $ cast lookup-address --rpc-url <rpc_url> --verify $ADDRESS
    • cast resolve-name
      • ENS로 address 가져옴
        cast namehash --rpc-url <rpc_url> vitalik.eth
    • cast namehash
      • 특정이름에 대해 ENS namehash 연산해줌
        cast namehash vitalik.eth

    Etherscan 관련

    • cast etherscan-source

      • 이더스캔에서 컨트랙트 소스코드 가져옴
      // 1. WETH 컨트랙트 소스코드 가져옴
      cast etherscan-source 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
      
      // 2. WETH 소스코드 가져와서 weth 디렉토리에 저장
      cast etherscan-source -d weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

    ABI 관련

    • cast abi-encode
      • abi-encode를 통한 함수 호출에 사용할 인코딩 정보 계산
      $ cast abi-encode "someFunc(address,uint256)" ${address1} ${integer}
    • cast 4byte / cast 4byte-event
      • 4바이트를 주면 이더리움 sig 데이터베이스에서 함수명 / 이벤트명 가져옴 Signature Database
      $ cast 4byte 0x8cc5ce99
    • cast-4byte-decode
      • ABI-encoded calldata를 복호화해줌
    • cast calldata
      • function과 argument를 ABI-encode해줌
      $ cast calldata "someFunc(address,uint256)" ${address1} ${integer}
    • cast pretty-calldata
      • 위에서 언급한 데이터베이스로 calldata에 대한 디코딩을 시도함
      // calldata for transfer call
      $ cast pretty-calldata 0xa9059cbb000000000000000000000000e78388b4ce79068e89bf8aa7f218ef6b9ab0e9d00000000000000000000000000000000000000000000000000174b37380cea000
    • cast --abi-decode / --calldata-decode
      • 전자: ABI-encoded input 또는 output을 decode해줌
      • 후자: input만을 decode해줌
      $ cast --abi-decode --input "transfer(address,uint256)" \
        0xa9059cbb000000000000000000000000e78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0000000000000000000000000000000000000000000000000008a8e4b1a3d8000
    • cast upload-signature
      • 내가 정한 함수에 대한 signature를 위에 언급한 데이터베이스에 업로드함
      $ cast upload-signature 'function approve(address,uint256)' \
      'transfer(uint256)' 'event Transfer(uint256,address)'

    Type Conversion 관련

    • 리믹스라면 컨트랙트 배포해서 실험해봐야 했을 것들을 커맨드 한줄로 확인할 수 있음

    • cast --format-bytes32-string

    • cast --from-bin

      • bin to hex
    • cast --from-flp

      • fixed point to int
    • cast --from-rlp

      • decode RLP-encoded data
      • data: hexadecimal string
    • cast --from-utf8

      • UTF8 to hex
    • cast --parse-bytes32-string

      • bytes to ASCII
    • cast --to-ascii

      • hex to ASCII
    • cast --to-base

      • N진수로 변환
    • cast --to-bytes32

      • hex data를 32바이트로 패딩시켜줌
    • cast --to-fix

      • int to fixed point
    • cast --to-hexdata

      • 주어진 데이터를 hexdata로 바꿔줌
      cast --to-hexdata deadbeef
      >> 0xdeadbeef
      
      cast --to-hexdata "deadbeef:0xbeef"
      >> 0xdeadbeef, 0xbeef
      
      cast --to-hexdata "@MY_VAR"
      >> convert ${MY_VAR} to hex
    • cast --to-int256 / --to-uint256

      • num to hex-encoded int256 / uint256
    • cast --to-rlp

      • hex to hexstring
    • cast --to-unit

      • ETH 단위 치환 (ether, gwei, wei)
    • cast --to-wei

      • ETH 단위를 wei로 변환
    • cast --shl / --shr

      • shift left / right 수행
      • base(진수) 지정 가능

    Utility 관련

    • cast sig / sig-event

      • 4바이트 함수/이벤트 signature 계산
    • cast keccak

      • 주어진 데이터에 대해 keccak-256 해시 계산
    • cast compute-address

      • 배포자 주소와 nonce로 생성될 주소 계산
    • cast create2

      • deterministic한 컨트랙트 주소 사전에 계산
    • cast interface

      • 주어진 ABI를 사용해 solidity interface를 생성
      // JSON ABI
      cast interface ./path/to/abi.json
      
      // Etherscan
      cast interface -o IWETH.sol 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    • cast index

      • mapping entry에 대한 스토리지 슬롯 주소 계산

        // World.sol
        mapping (address => uint256) public mapping1;
        mapping (string => string) public mapping2;
        
        $ cast index string "hello" 1
        0x3556fc8e3c702d4479a1ab7928dd05d87508462a12f53307b5407c969223d1f8
        $ cast storage [address] 0x3556fc8e3c702d4479a1ab7928dd05d87508462a12f53307b5407c969223d1f8 world
    • cast --concat-hex

      • hex string끼리 합침
    • cast --max-int / --min-int / --max-uint

      • 32바이트 최대최소값 출력
    • cast --to-checksum-address

    Wallet 관련

    • cast wallet new
      • 랜덤한 keypair 생성
    • cast wallet address
      • private key를 address로 변환
      cast wallet address PRIVATE_KEY
    • cast wallet sign
      • 메시지를 sign함
      cast wallet sign message
    • cast wallet vanity
      • vanity: 허세
      • 지정한 단어로 시작하는 keypair 생성 가능
      cast wallet vanity --starts-with dead
      cast wallet vanity --ends-with beef
    • cast wallet verify
      • 메시지 서명 검증
      cast wallet verify --address ${address} ${message} ${signature}

Anvil 커맨드 모음

  • Anvil이란?

    • 스마트 컨트랙트 테스트 및 배포를 지원하는 로컬 테스트넷 노드
    • 다른 EVM 호환 네트워크도 포크해서 사용할 수 있다.
  • Mining Mode

    • 테스트넷에서 새로운 블록이 생성되는 주기를 조절할 수 있다.

      // 10초마다 새 블록 생성
      anvil --block-time 10
      
      // mining 끔
      anvil --no-mining
  • Transport Layer 지원

    • HTTP, WSS 지원
    • 포트 기본값은 8545로 설정
      anvil --port <PORT>
  • 이더리움 공식 문서가 지정한 JSON-RPC API의 spec을 모두 지원한다.

  • 추가적으로 anvil_ prefix가 붙은 메서드를 지원하는데, hardhat에 대한 alias이다.

  • 지원하는 메서드에 대한 정보가 필요하다면: JSON-RPC API | ethereum.org Foundry Book

  • anvil completions shell을 사용해 shell completion도 가능하다.

    anvil completions zsh > $HOME/.oh-my-zsh/completions/_anvil
  • 도커와의 호환

    • Github Action에서 도커 컨테이너에 붙여서 anvil을 돌려볼 수도 있다.
    • 하지만 이 경우 entrypoint에서 argument passing이 불가능하다.
    • 이때는 host의 IP를 ANVIL_IP_ADDR로 설정하면 된다.
      • ANVIL_IP_ADDR=0.0.0.0
    • 직접 해봐야 알듯

Chisel 사용법

  • Chisel이란?
    • Read-Eval-Print Loop 환경에서 Solidity input에 대해 테스트하고 보다 자세한 피드백을 준다.
    • 개발자로 하여금 foundry 없이 빠르게 Solidity code snippet에 대한 테스트를 할 수 있도록 만들어준다.
    • 파이썬같이 line-by-line으로 실행 결과를 보여주는 툴로 보인다.
     address addr
     assembly {
        addr := not(0)
    }
     addr
    Type: addressData: 0xffffffffffffffffffffffffffffffffffffffff
     !rs addr
    Type: bytes32Data: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    
    • 실행 방법: chisel 바이너리 다운 및 실행 → 코드 line by line 입력 → 결과값 확인

Misc.

Struct Encoding

  • Solidity에서 nested array와 struct는 ABIcoderV2를 통해서만 encoding이 가능하다.

  • Solidity 0.8 이후의 버전은 자동으로 activate 되어있지만, 이전 버전은 pragma experimental ABIEncoderV2를 통해 수동으로 enable해줘야 한다.

    pragma solidity =0.8.15;
    
    contract Test {
        struct MyStruct {
            address addr;
            uint256 amount;
        }
        function f(MyStruct memory t) public pure {}
    }
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "address",
                        "name": "addr",
                        "type": "address"
                    },
                    {
                        "internalType": "uint256",
                        "name": "amount",
                        "type": "uint256"
                    }
                ],
                "internalType": "struct Test.MyStruct",
                "name": "t",
                "type": "tuple"
            }
        ],
        "name": "f",
        "outputs": [],
        "stateMutability": "pure",
        "type": "function"
    }
    • 이런 식으로 인코딩 되는 것을 볼 수 있다.

About

The Foundry documentation translated in Korean. Foundry is a toolkit for Ethereum application development, which is written in Rust. Foundry 공식 문서에 대한 한글 번역본입니다. 의역이 다소 포함되어 있으며, 모든 정보가 정확하지 않을 수도 있습니다.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published