ERC-7201 네임스페이스 저장소 배치 (Namespaced Storage Layout)

Final · 기타 · 2023-06-20 작성

함께 쓰는 상태 변수를 구조체로 묶어 해시로 정한 독립된 저장 위치에 두고 그 위치를 소스 주석으로 표시하는 규칙입니다. 업그레이드 가능·모듈형 컨트랙트에서 저장소 충돌을 피하고 분석 도구가 데이터 위치를 읽을 수 있게 합니다.

왜 필요한가

Solidity나 Vyper 같은 스마트 컨트랙트 언어는 상태 변수를 슬롯 0부터 차례로 놓는 트리 모양 저장소 배치를 씁니다. 대부분의 컨트랙트에는 이것으로 충분하지만 몇 가지 설계 패턴에서는 문제가 됩니다. DELEGATECALL로 여러 컨트랙트의 코드를 실행하면서 같은 저장소를 공유하는 모듈형 설계에서는 각 코드가 저장소를 어떻게 쓸지 세심하게 조율해야 합니다. 업그레이드 가능 컨트랙트에서는 업그레이드 때 상태 변수를 추가하면 기존 변수의 저장 위치가 영향을 받을 수 있어 변수 추가가 어렵습니다. 그래서 해시로 얻은 의사난수 위치에 변수 묶음을 두는 방식이 쓰이지만 이렇게 둔 값은 Solidity 상태 변수로 선언되지 않아 컴파일러에 보이지 않습니다. 정적 분석기나 블록 탐색기 같은 도구는 컨트랙트 데이터가 어디 있는지 알아야 하는데, 위치를 정하는 방식이 표준화되어 있지 않으면 이런 컨트랙트를 올바르게 해석할 수 없습니다.

심층 설명

네임스페이스와 네임스페이스 id

네임스페이스는 순서가 있는 변수 집합입니다. 동적 배열이나 매핑을 포함할 수 있고 값은 기본 저장소 배치와 같은 규칙으로 놓이되 루트가 슬롯 0이 아닐 수 있습니다. 이 의사난수 루트는 기본 트리에 속하지 않도록 만들어지므로 새 저장소 트리의 시작점이 되어 서로 겹치지 않는 독립된 공간을 이룹니다. 네임스페이스를 써서 저장소를 구성하는 컨트랙트는 네임스페이스 저장소(namespaced storage)를 쓴다고 부릅니다. 네임스페이스 id는 컨트랙트 안에서 네임스페이스를 식별하는 문자열이며 공백 문자를 넣어서는 안 됩니다.

@custom:storage-location 주석

컨트랙트 안의 네임스페이스는 struct 타입으로 구현해야 합니다. 이 구조체에 NatSpec 태그 `@custom:storage-location <FORMULA_ID>:<NAMESPACE_ID>`를 답니다. `<FORMULA_ID>`는 네임스페이스 id로부터 루트 저장 위치를 계산하는 공식을 가리킵니다. Solidity 컴파일러는 v0.8.20부터 이 주석을 AST에 포함하므로, 이 패턴을 쓸 때 최소 컴파일러 버전으로 권장됩니다. 컨트랙트 바깥에 선언된 구조체에 이 주석이 있더라도 소스 코드 안 어떤 컨트랙트의 네임스페이스로도 보지 않습니다.

erc7201 공식

식별자 `erc7201`이 가리키는 공식은 `erc7201(id: string) = keccak256(keccak256(id) - 1) & ~0xff`입니다. Solidity 표현식으로는 `keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff))`입니다. 이 공식을 쓸 때 주석은 `@custom:storage-location erc7201:<NAMESPACE_ID>`가 됩니다. 예를 들어 `@custom:storage-location erc7201:foobar`는 id가 "foobar"이고 `erc7201("foobar")`에 루트를 둔 네임스페이스를 뜻합니다. 앞으로 다른 EIP가 고유한 식별자로 새 공식을 정의할 수 있으며 이때 이 EIP의 관례를 따라 `erc1234` 형식의 식별자를 쓰도록 권장합니다.

공식이 충돌을 피하는 이유

Solidity·Vyper의 트리형 저장소는 루트 0에서 시작해 `root + n`, `keccak256(root)`, `keccak256(H(k) ⊕ root)`, `keccak256(root ⊕ H(k))` 형태로 위치를 만듭니다. 네임스페이스 루트는 이 기본 트리에도, 다른 네임스페이스에서 파생된 트리에도 속하지 않아야 합니다. 그래야 여러 네임스페이스를 기본 배치와 함께, 의도했든 아니든 충돌 없이 쓸 수 있습니다. `keccak256(id) - 1`은 Solidity가 쓰지 않는 위치지만 네임스페이스가 1슬롯보다 커지면 `keccak256(id) + n`까지 뻗어 Solidity가 쓸 수 있는 영역과 겹칠 수 있어 최종 위치로 쓰지 않습니다. 그래서 해시를 한 번 더 적용해, keccak256 충돌 저항과 배열이 비정상적으로 크지 않다는 가정 아래 기본 저장소와 완전히 분리되도록 합니다. 끝에 `& ~0xff`로 256 단위 정렬을 하는 것은 Verkle 상태 트리 전환 뒤 가스 스케줄이 바뀌어 256개 슬롯 묶음이 한꺼번에 warm 상태가 될 가능성을 내다본 최적화입니다.

참조 구현

참조 구현은 `pragma solidity ^0.8.20`을 쓰고, `uint256 x`와 `uint256 y`를 가진 `MainStorage` 구조체에 `/// @custom:storage-location erc7201:example.main` 주석을 답니다. `keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff))`로 계산한 값 `0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500`을 `MAIN_STORAGE_LOCATION` 상수에 둡니다. `_getMainStorage()` 함수는 어셈블리에서 반환할 저장소 포인터 `$`의 `.slot`에 이 상수를 대입해 구조체를 돌려주고 `_getXTimesY()`는 그 구조체의 `x * y`를 반환합니다.

이름과 하위 호환성

이 패턴은 때때로 다이아몬드 저장소(diamond storage)라고 불려 왔습니다. 그 이름 때문에 다이아몬드 프록시 패턴과 같은 것으로 오해되기 쉽지만 두 가지는 서로 독립적으로 쓸 수 있습니다. 이 EIP는 프록시 패턴과 분명히 구분하려고 네임스페이스 저장소 배치라는 다른 이름을 택했습니다. 하위 호환성 측면에서는 문제가 발견되지 않았다고 원문은 적습니다. 상태는 Final로 확정된 표준입니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs