컨트랙트에서 저장되는 다양한 전역변수들은 Storage에 저장된다.
이 때 Slot(256비트) 단위로 주소를 가지어 저장한다.
뭔 개소리냐고? 예시를 보면 ㅈㄴ 쉽다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageLayoutExample {
uint256 public a = 42; // 슬롯 0
uint8 public b = 1; // 슬롯 1 (uint8 + uint16과 같은 작은 타입들은 같은 슬롯에 패킹될 수도 있음)
uint16 public c = 256; // 슬롯 1 (b와 함께 패킹될 가능성 있음)
uint256 public d; // 슬롯 2 (명시적 할당 없음, 기본값 0)
address public owner = msg.sender; // 슬롯 3
bool public flag = true; // 슬롯 3 (bool은 1바이트만 차지)
bytes32 public hash = keccak256("Hello"); // 슬롯 4
struct User {
uint256 id;
address userAddress;
}
User public user = User(123, msg.sender); // 슬롯 5,6(구조체는 여러 슬롯 차지)
mapping(uint256 => address) public users; // 슬롯 7 (매핑 자체는 슬롯을 차지하지 않음, 키 해시를 통해 위치 계산)
uint256[3] public fixedArray = [10, 20, 30]; // 슬롯 8,9,10(고정 배열)
string public myString = "Ethereum"; // 문자열은 동적 데이터로 별도 슬롯 사용 (슬롯 11에 길이 저장, 실제 값은 Keccak256(11) 위치)
bytes public myBytes = hex"123456"; // bytes도 동적 데이터 (슬롯 13에 길이 저장, 실제 값은 Keccak256(12) 위치)
}
EVM은 컨트랙트에 선언한 순서대로 슬롯에 저장한다.
즉 위부터 슬롯 0부터 차례대로 저장한다.
대충 감이 오는가?
먼저 데이터 타입을 나누어서 생각하자.
고정 길이 타입이면 최대한 모여잇~ 그것이 Packing
이들은 고정 길이 타입의 데이터다.
길이가 고정되어 있으니 32바이트 슬롯을 함께 사용할 수도 잇다.
b,c는 작은 정수형으로, 256비트 짜리 슬롯에, 8 + 16 합쳐도 24비트 짜리라 한 슬롯에 같이 들어갈 수 있다는 점이다.
아마 솔리디티를 배우면서, 작은 정수형들은 선언할 때 모아서 선언하라는 팁을 배웠을 것인데,
그 이유가 바로 이것이다.
마찬가지로 owner는 address 타입, 20바이트, bool은 1비트다. 한 슬롯에 함께 묶인다.
정적 길이 타입이면 최대한 안 겹치게 해시 사용해서 따로 저장해잇~
dynamic은 해당 슬롯에, "길이가 저장되고" 실제 값은 별도의 슬롯에 저장된다.
만약 0-31바이트라면, 한 개의 슬롯에 저장되고
32바이트 이상이라면 슬롯에는 길이 정보만 저장하고, 실제는 keccack256(slotNum)에 저장.
동적인 데이터에 특정 공간만 주면, 길어지면 남의 슬롯이랑 충돌이 발생할 것 아닌가?
가뜩이나 순서대로 슬롯을 배정하고 있는데? 맞잖아?
그래서 이런 길이가 동적인 데이터들은, 단순히 Slot 12가 아니라,
Slot keccak256(12) 위치에 저장이 된다.
즉 실제 슬롯 12는 비워두고, 해시를 한 슬롯에 실제 데이터를 저장하는 것이다.
구조체 각각 필드가 각각의 슬롯을 사용한다.
위를 보면, 매핑의 선언만 했지 값을 할당은 안했다.
앞으로 누군가 채워넣겠지? 그러므로 동적이다.
매핑 선언은 그 자체로 슬롯에 값이 들어가진 않지만, 슬롯 번호 8은 사용을 한다.
만약 누군가 값을 넣는다면,
그 값은 Keccak(key,8)인 Slot에 저장된다.
결론, 데이터 타입을 정적이냐, 동적이냐로 나눠서,
정적이면 최대한 모아서 한 슬롯에 넣고,
동적이면 최대한 안 겹치게, 해시를 사용해서 널리널리 뿌려서 저장한다. 너무 쉽죠?