Yul 컨트랙트 호출은 어떻게 작동하는가?
작성자 Mark Jonathas
Yul은 스마트 컨트랙트 내부에서 일종의 어셈블리 언어를 작성하는 데 사용할 수 있는 중간 수준 프로그래밍 언어입니다. 이제 문법, 스토리지, 그리고 Yul에서 메모리가 어떻게 동작하는지 이해했으니, 컨트랙트를 호출할 차례입니다!
Yul에서 컨트랙트 호출은 어떻게 동작하는가?
이 시리즈의 마지막 섹션에서는 Yul에서 컨트랙트 호출이 어떻게 동작하는지 살펴봅니다. 예제를 살펴보기 전에 몇 가지 Yul 연산을 더 배워야 합니다. 살펴보겠습니다.
이제 이 예제들을 위한 새로운 컨트랙트를 살펴보겠습니다. 먼저 우리가 호출할 컨트랙트를 살펴보겠습니다.
이 컨트랙트에는 각각 스토리지 슬롯 1과 2에 저장된 var1과 var2라는 두 개의 스토리지 변수가 있습니다.
함수 a()는 사용자가 컨트랙트에 최소 1 이더를 보낼 것을 요구하며, 그렇지 않으면 되돌립니다(revert).
다음으로, 함수 a()는 var1과 var2를 업데이트하고 이를 반환합니다.
함수 b()는 단순히 var1과 var2를 읽어 반환합니다.
CallMe 컨트랙트를 호출하는 우리의 컨트랙트로 넘어가기 전에, 함수 선택자(function selector)를 이해하는 데 시간을 좀 들여야 합니다.
트랜잭션 0x773d45e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002의 다음 calldata를 살펴보겠습니다.
calldata의 첫 4바이트는 함수 선택자(0x773d45e0)라고 불립니다. 이것이 EVM이 호출하려는 함수를 알아내는 방법입니다. 함수 시그니처 문자열의 해시에서 첫 4바이트를 가져와 함수 선택자를 도출합니다.
따라서 함수 a()의 시그니처는 a(uint256,uint256)이 됩니다.
이 문자열의 해시를 구하면: 0x773d45e097aa76a22159880d254a5f1db8365bc2d0f0987a82bda7dfd3b9c8aa이 나옵니다.
첫 4바이트를 보면 0x773d45e0과 같음을 알 수 있습니다.
시그니처에 공백이 없다는 점에 유의하세요. 공백을 추가하면 완전히 다른 해시가 나오기 때문에 이는 중요합니다. 우리의 코드 예제에서는 선택자를 직접 구할 필요가 없으며, 제가 제공하겠습니다.
먼저 스토리지 레이아웃을 살펴보겠습니다.
var1과 var2가 CallMe 컨트랙트와 동일한 레이아웃을 가지고 있음을 확인하세요. delegatecall()이 제대로 작동하려면 다른 컨트랙트와 레이아웃이 동일해야 한다고 제가 말씀드렸던 것을 기억하실 겁니다.
이러한 요구사항을 충족하면서도, 새로운 변수(selectorA & selectorB)가 끝에 추가되는 한 다른 변수를 가질 수 있습니다. 이는 스토리지 충돌을 방지합니다.
이제 첫 번째 컨트랙트 호출을 만들 준비가 되었습니다.
Yul에서 staticcall() 사용하는 방법
간단한 것부터 시작해보겠습니다, staticcall()입니다. 다음은 우리의 함수입니다:
여기서 일어나는 일은 다음과 같습니다:
- 슬롯 2를 로드하여 스토리지에서 b()의 함수 선택자를 가져옵니다(두 선택자 모두 하나의 슬롯에 패킹되어 있습니다)
- 4바이트(32비트) 오른쪽으로 시프트하여 selectorB를 분리합니다
- 함수 선택자를 메모리의 스크래치 공간에 저장합니다
- static call을 만듭니다
- gas()를 전달합니다(가스량을 직접 지정할 수도 있습니다)
- 컨트랙트 주소로 매개변수 _callMe를 전달합니다
- 함수 호출이 성공했는지 확인하고, 그렇지 않으면 데이터 없이 반환합니다
- 메모리에서 데이터를 반환하고 값 1과 2를 확인합니다
참고: 0x1c와 0x20은 스크래치 공간에 저장한 것의 마지막 4바이트를 전달하고자 함을 나타냅니다. staticcall()의 마지막 두 매개변수는 반환 데이터를 메모리 위치 0x80 - 0xc0에 저장하고자 함을 지정합니다.
Yul에서 call() 사용하는 방법
다음으로 call()을 살펴보겠습니다. CallMe에서 함수 a()를 호출할 것입니다. 컨트랙트에 최소 1 이더를 보내는 것을 잊지 마세요! 이 예제에서는 _var1과 _var2로 3과 4를 전달하겠습니다.
코드는 다음과 같습니다:
이전 예제와 마찬가지로 slot2를 로드해야 합니다. 하지만 이번에는 selectorB를 마스킹하여 selectorA를 분리할 것입니다.
이제 0x80에 선택자를 저장하겠습니다.
calldata에서 매개변수가 필요하므로 calldatacopy()를 사용하겠습니다. calldatacopy()에 다음을 지시합니다:
- calldata를 메모리 위치 0xa0에 저장
- 처음 36바이트를 건너뜀
- calldata의 크기에서 36바이트를 뺀 만큼을 저장
참고: 건너뛰는 처음 4바이트는 callA()의 함수 선택자이며, 다음 32바이트는 callMe의 주소입니다.
이제 컨트랙트 호출을 만들 준비가 되었습니다!
지난번과 마찬가지로 gas()와 _callMe를 전달합니다. 하지만 이번에는 0x9c(0x80 메모리 시리즈의 마지막 4바이트) - 0xe0에서 calldata를 전달하고, 데이터를 메모리 위치 0x100 - 0x120에 저장합니다.
다시 한번 호출이 성공했는지 확인하고 출력을 반환합니다. CallMe 컨트랙트를 확인하면 값이 성공적으로 3과 4로 업데이트되었음을 알 수 있습니다.
무슨 일이 일어나는지 더 명확히 하기 위해, 반환 직전의 메모리 레이아웃은 다음과 같습니다:
Yul에서 delegate call 사용하는 방법
마지막 섹션에서는 delegatecall()을 살펴보겠습니다. 코드는 딱 한 가지 변경 사항을 제외하고는 call()과 거의 동일해 보일 것입니다.
우리가 변경한 유일한 부분은 call()을 delegatecall()로 바꾸고 callvalue()를 제거한 것입니다.
callvalue()가 필요하지 않은 이유는 delegate call이 CallMe의 코드를 자신의 상태 안에서 실행하기 때문입니다. 따라서 a()의 require() 문은 이더가 우리의 Caller 컨트랙트로 전송되었는지를 확인합니다. CallMe에서 var1과 var2를 확인하면 변경 사항이 없음을 알 수 있습니다. 하지만 Caller 컨트랙트의 var1과 var2는 성공적으로 업데이트되었습니다.
이것으로 컨트랙트 호출 섹션을 마칩니다! 다음으로, Yul에서 메모리, 스토리지, 스마트 컨트랙트 호출이 어떻게 동작하는지 알아보세요.
관련 개요

블록체인 매직을 만드세요
Alchemy는 가장 강력한 Web3 개발자 제품 및 도구를 리소스, 커뮤니티, 그리고 전설적인 지원과 결합합니다.


