본문으로 건너뛰기
0%

Yul storage는 어떻게 동작하는가?

Mark Jonathas headshot

작성자 Mark Jonathas

2023년 8월 1일에 게시됨3분 읽기

Yul스마트 컨트랙트 내부에서 어셈블리 언어 형태로 작성할 수 있는 중간 수준의 프로그래밍 언어입니다. Yul을 이해하면 스마트 컨트랙트를 한 단계 발전시킬 수 있고 사용자의 가스 비용을 절감하는 데 도움이 됩니다. Yul로 스마트 컨트랙트를 작성하기 시작하려면 스토리지가 어떻게 동작하는지 이해하는 것이 중요합니다.

Yul 스토리지는 어떻게 동작하는가?

Yul이 어떻게 동작하는지 더 깊이 들어가기 전에, 스마트 컨트랙트에서 스토리지가 어떻게 동작하는지 잘 이해해야 합니다. 스토리지는 일련의 슬롯들로 구성됩니다. 하나의 스마트 컨트랙트에는 2²⁵⁶개의 슬롯이 있습니다.

변수를 선언할 때는 슬롯 0부터 시작해서 순차적으로 증가시킵니다. 각 슬롯은 256비트(32바이트) 길이이며, 여기서 uint256과 bytes32라는 이름이 유래했습니다. 모든 변수는 16진수로 변환됩니다.

uint128과 같은 변수를 사용하는 경우 슬롯 전체를 차지하지 않습니다. 대신 왼쪽에 0으로 패딩됩니다. 예제를 통해 더 자세히 살펴보겠습니다.

var1: uint256 변수는 32바이트에 해당하므로, var1은 슬롯 0 전체를 차지합니다.

슬롯 0에 저장되는 내용은 다음과 같습니다:

0x0000000000000000000000000000000000000000000000000000000000000100

var2: 주소는 조금 더 복잡합니다. 20바이트의 저장 공간만 차지하기 때문에, 주소는 왼쪽에 0으로 패딩됩니다.

슬롯 1에 저장되는 내용은 다음과 같습니다:

0x0000000000000000000000009acc1d6aa9b846083e8a497a661853aae07f0f00.

var3: 이것은 간단해 보일 수 있는데, 슬롯 2는 bytes32 변수 전체로 채워집니다.

var4 & var5: uint128이 0으로 패딩된다고 언급했던 것을 기억하시나요? 변수들의 저장 공간 합이 32바이트 미만이 되도록 순서를 정하면, 이들을 한 슬롯에 함께 담을 수 있습니다! 이를 변수 패킹(packing)이라고 하며, 가스를 절약할 수 있습니다.

슬롯 3에 저장되는 내용을 살펴보겠습니다:

0x0000000000000000000000000000000200000000000000000000000000000001. 

0x0000000000000000000000000000020x000000000000000000000000000001가 같은 슬롯에 정확히 맞아떨어지는 것을 확인할 수 있습니다. 이는 두 변수 모두 16바이트(슬롯의 절반)를 차지하기 때문입니다.

Instruction
Explanation

sload(p)

Loads the variable in slot p from storage.

sstore(p,v)

Assigns storage slot p value v.

v.slot

Returns the storage slot of variable v.

v.offset

Returns the index in bytes of where variable v begins in a storage slot. Variables are packed from right to left.

다른 예제도 살펴보겠습니다!

x = 3. var5가 슬롯 3에 패킹되어 있다는 것을 알고 있으므로 이는 타당합니다.

y = 16. var4가 슬롯 3의 절반을 차지한다는 것을 알고 있으므로 이 역시 타당합니다. 변수는 오른쪽에서 왼쪽으로 패킹되므로 var5의 시작 인덱스로 바이트 16을 얻게 됩니다.

z = 1. sstore()는 슬롯 0에 값 1을 할당합니다. 그런 다음 sload()로 슬롯 0의 값을 z에 할당합니다.

계속 진행하기 전에, 이 함수를 remix 파일에 추가해 보시기 바랍니다. 각 스토리지 슬롯에 저장된 값을 확인하는 데 도움이 될 것입니다.

이제 좀 더 복잡한 데이터 구조를 살펴보겠습니다!

정적 배열을 다룰 때, EVM은 데이터에 몇 개의 슬롯을 할당해야 하는지 알고 있습니다. 이 배열에서는 슬롯당 2개의 요소를 패킹하고 있습니다.

getValInHex\(4\)를 호출하면 0x0000000000000000000000000000000100000000000000000000000000000000이 반환됩니다.

예상대로 오른쪽에서 왼쪽으로 읽으면 값 0과 값 1이 보입니다.

슬롯 5는 0x0000000000000000000000000000000300000000000000000000000000000002을 포함합니다.

다음으로 동적 배열을 살펴보겠습니다.

getValInHex\(6\)을 호출해 보세요. 0x00이 반환되는 것을 확인할 수 있습니다. EVM이 배열에 몇 개의 스토리지 슬롯을 할당해야 하는지 알 수 없기 때문에 여기에 배열을 저장할 수 없습니다.

대신, 현재 스토리지 슬롯(슬롯 6)의 keccak256 해시가 배열의 시작 인덱스로 사용됩니다. 여기서부터는 원하는 요소의 인덱스를 더하기만 하면 값을 가져올 수 있습니다.

다음은 동적 배열의 요소를 찾는 방법을 보여주는 코드 예제입니다:

여기서는 배열의 슬롯을 가져온 다음, add() 연산과 sload()를 함께 수행하여 원하는 배열 요소의 값을 가져옵니다.

다른 변수의 슬롯과 충돌하는 것을 무엇이 막아주는지 궁금하실 수 있습니다.

이런 충돌은 충분히 가능하지만, 2²⁵⁶이 매우 큰 숫자이기 때문에 발생할 확률은 극히 낮습니다.

매핑은 슬롯을 키와 함께 해시한다는 점을 제외하면 동적 배열과 유사하게 동작합니다.

이 예제를 위해 매핑 값을 var8[1] = 2로 설정했습니다. 이제 매핑에서 키의 값을 가져오는 방법을 살펴보겠습니다.

보시다시피 코드는 동적 배열에서 요소를 찾을 때와 매우 유사합니다. 주된 차이점은 키와 슬롯을 함께 해시한다는 것입니다.

스토리지 섹션의 마지막 부분은 중첩 매핑에 대해 배우는 것입니다. 계속 읽기 전에, 지금까지 배운 내용을 바탕으로 중첩 맵 값을 읽는 방법을 직접 구현해 보시기를 권장합니다.

이 예제를 위해 매핑 값을 var9[0][1] = 2로 설정했습니다.

코드는 다음과 같습니다. 함께 살펴보겠습니다!

진행 과정은 다음과 같습니다:

  1. 먼저 첫 번째 키(0)의 해시를 구합니다.
  2. 그런 다음 이 해시를 두 번째 키(1)와 함께 다시 해시합니다.
  3. 마지막으로 스토리지에서 슬롯을 로드하여 값을 가져옵니다.

축하합니다, Yul을 이용한 스토리지 섹션을 완료하셨습니다!

Yul Memory, Yul Contract Calls, Yul Packed Storage Variables도 읽어보시기 바랍니다.

Background gradient

블록체인 매직을 만드세요

Alchemy는 가장 강력한 Web3 개발자 제품 및 도구를 리소스, 커뮤니티, 그리고 전설적인 지원과 결합합니다.