ERC20 with some Yul
มาอ่าน code เรื่อยๆกันครับ
มาอ่าน code จาก repo นี้กัน
โดย code นี้จะเป็น code ของ ERC20 ที่แทรก Yul ไปด้วยบางส่วนเพื่อ optimize gas
เอาละ มาเริ่มกัน
ก่อนอื่น constructor มี code ดังนี้
constructor(string memory name_, string memory symbol_) {
/// @dev constructor in solidity bc cannot handle immutables with inline assembly
/// also, constructor gas optimization not really important (one time cost)
// get string lengths
bytes memory nameB = bytes(name_);
bytes memory symbolB = bytes(symbol_);
uint256 nameLen = nameB.length;
uint256 symbolLen = symbolB.length;
// check strings are <=32 bytes
assembly {
if or(lt(0x20, nameLen), lt(0x20, symbolLen)) {
mstore(0x00, _STRING_TOO_LONG_SELECTOR)
revert(0x00, 0x04)
}
}
// compute domain separator
bytes32 initialDomainSeparator = _computeDomainSeparator(
keccak256(nameB)
);
// set immutables
_name = bytes32(nameB);
_symbol = bytes32(symbolB);
_nameLen = nameLen;
_symbolLen = symbolLen;
_initialChainId = block.chainid;
_initialDomainSeparator = initialDomainSeparator;
}
มาดูทีละส่วนกัน
constructor(string memory name_, string memory symbol_) {
/// @dev constructor in solidity bc cannot handle immutables with inline assembly
/// also, constructor gas optimization not really important (one time cost)
// get string lengths
bytes memory nameB = bytes(name_);
bytes memory symbolB = bytes(symbol_);
uint256 nameLen = nameB.length;
uint256 symbolLen = symbolB.length;
// check strings are <=32 bytes
assembly {
if or(lt(0x20, nameLen), lt(0x20, symbolLen)) {
mstore(0x00, _STRING_TOO_LONG_SELECTOR)
revert(0x00, 0x04)
}
}
ตรงส่วนนี้เป็นการ validate argument ใน version solidity + yul
// get string lengths
bytes memory nameB = bytes(name_);
bytes memory symbolB = bytes(symbol_);
uint256 nameLen = nameB.length;
uint256 symbolLen = symbolB.length;
// check strings are <=32 bytes
assembly {
if or(lt(0x20, nameLen), lt(0x20, symbolLen)) {
mstore(0x00, _STRING_TOO_LONG_SELECTOR)
revert(0x00, 0x04)
}
}
โดยในส่วนที่เขียนด้วย yul ถ้าเขียนเป็น solidity เพียวๆ จะได้เป็นแบบนี้แทน
require(0x20 >= nameLen && 0x20 >= symbolLen, "StringTooLong()");
โอเค มา breakdown ส่วนที่เป็น yul กัน
เริ่มจากบรรทัดแรก จะเป็นการ validate ว่า argument อันใดอันหนึ่ง ยาวเกินมั้ย
if or(lt(0x20, nameLen), lt(0x20, symbolLen))
โดยถ้าใช่ แสดงว่า validate ไม่ผ่าน ดังนั้นสิ่งที่ต้องทำต่อไปคือการ revert ด้วย error reason “StringTooLong()”
ซึ่งตัว syntax ในการทำ คือ revert(ช่วงใน memory ที่เก็บ error reason)
โอเค ก่อนจะไปใช้ revert
- มาเก็บ error reason ใน memory กันก่อน
// first 4 bytes of keccak256("StringTooLong()") right padded with 0s
bytes32 internal constant _STRING_TOO_LONG_SELECTOR =
0xb11b2ad800000000000000000000000000000000000000000000000000000000;
// code snippet
mstore(0x00, _STRING_TOO_LONG_SELECTOR)
โดย mstore จะเป็นการ store ลงไปใน memory ทีละ 32 bytes ดังนั้นตอนนี้ที่ 0x00–0x1f จะได้เป็น
2. จากนั้นระบุตำแหน่ง error reason ให้กับ revert
โดยจริงๆเราเก็บ error reason ไว้แค่ช่วง 0x00–0x04 ที่เหลือเป็น 0 หมด ตามรูป ดังนั้น จะได้ว่า
revert(0x00, 0x04)
โอเค ลองมารันกัน
เราจะเจอว่าสามารถทำการ revert ได้ตามที่เราตั้งเป้าไว้ แต่ตัว ide ไม่เข้าใจ error reason ที่เราส่งมา (เพราะ code ไม่ได้เก็บ error reason ไว้ แต่เก็บ hash ของ error reason ไว้ละนะ)
โอเค มาแก้ code เค้ากัน
โดยเราจะเริ่มจาก
- เก็บ hash ของ
function Error(string memory reason) external;
ไว้ใน memory
โดยตัว 4 byte แรก ของ hash จะได้เป็น
0x08c379a000000000000000000000000000000000000000000000000000000000000000
เอาละ รอบนี้เราจะไม่เอาไปเก็บที่ 0x00 เหมือนเดิมละ เพราะเราจะใช้ slot memory ช่วงที่ต่อเนื่องกันยาวๆ แต่ตรง 0x00 มีว่างให้เราใช้แค่ 0x00–0x3f
รอบนี้เราจะใช้ free memory pointer เข้ามาช่วย
โดยปกติแล้วที่ 0x40–0x5f เนี่ย จะเอาไว้เก็บตำแหน่งใน memory ที่ว่างอยู่
ดังนั้นเราจะ load ค่าตำแหน่งนั้นมาเป็นจุดเริ่มกัน
let ptr := mload(0x40) // Get free memory pointer
โดยตอนนี้ค่า ptr จะเป็น 0x380
โดยตั้งแต่ 0x380 เป็นต้นไป คือช่วงว่างที่ยังไม่ได้ใช้
เอาละ มาเก็บ hash ลงไปกัน
mstore(ptr, 0x08c379a0000000000000000000000000000000000000000000000000000000000) // Selector for method Error(string)
จะได้
โอเค หลังจากเก็บ hash ของ
function Error(string memory reason) external;
ไปแล้ว ขั้นต่อไปคือ
2. เก็บค่า offset
โดยเราจะเก็บค่า 0x20 ไว้ในตำแหน่งหลังจาก 4 byte แรกของ hash
mstore(add(ptr, 0x04), 0x20)
จะได้
3. เก็บความยาวของ error reason
โดย error reason เราคือ
ดังนั้น code จะเป็น
mstore(add(ptr, 0x24), 15) // Revert reason length
รันแล้วได้เป็น
4. เก็บ error reason ลง memory
mstore(add(ptr, 0x44), "StringTooLong()")
จะได้
5. สั่ง revert โดยให้เอาช่วงทั้งหมดที่เราเขียนมาเป็น error reason
โดยจะได้ว่า
เราเริ่มเขียนไปตั้งแต่ ค่าแรกของของ free memory pointer (ptr)
เขียนไป 4 + 32 + 32 + 32 = 100 bytes
= 0x64
revert(ptr, 0x64) // Revert data length is 4 bytes for selector and 3 slots of 0x20 bytes
โอเค ตอนนี้ ide จะยังแสดง error reason แบบ unknown อยู่
แต่เราก็จะ debug จาก ค่าใน memory ได้ละ
โดย constructor ที่เหลือจะเป็น solidity ละ
// compute domain separator
bytes32 initialDomainSeparator = _computeDomainSeparator(
keccak256(nameB)
);
// set immutables
_name = bytes32(nameB);
_symbol = bytes32(symbolB);
_nameLen = nameLen;
_symbolLen = symbolLen;
_initialChainId = block.chainid;
_initialDomainSeparator = initialDomainSeparator;
โอเค จบ constructor ไปละ มาดู mint() แถมไปดีกว่า
contract MyToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
// Mint 100 tokens to msg.sender
// Similar to how
// 1 dollar = 100 cents
// 1 token = 1 * (10 ** decimals)
_mint(msg.sender, 100 * 10 ** uint(decimals()));
}
}
โดย code _mint เริ่มจากการเช็คว่า to != address(0)
function _mint(address dst, uint256 amount) internal virtual {
assembly {
// require(dst != address(0), "Address Zero");
if iszero(dst) {
mstore(0x00, _RECIPIENT_ZERO_SELECTOR)
revert(0x00, 0x04)
}
จากนั้นจะเป็นการเอาค่า _supply มา + amount แล้วเช็คว่าเกิน totalSupply มั้ย
// _supply += amount;
let newSupply := add(amount, sload(0x02))
if lt(newSupply, amount) {
mstore(0x00, _OVERFLOW_SELECTOR)
revert(0x00, 0x04)
}
โอเคมา recheck code ตรงนี้กัน
โดยเริ่มจากการดึงค่า uint256 state variable ซึ่งปกติจะมี syntax เป็น
function getSlot() external pure returns(uint256 slot) {
assembly {
slot := a.slot
}
}
function getValueBySlotIndex(uint256 slotIndex) external view returns(bytes32 ret) {
assembly {
ret := sload(slotIndex)
}
}
เอาละ เราจะเขียน recheck ว่า _supply.slot = 0x20 มั้ย
ได้เป็น
ok ส่วนเช็คค่า newSupply ผ่านละ
ต่อมาจะเป็นการเอาค่า newSupply ไปเขียนทับ _supply
sstore(0x02, newSupply)
จากนั้นจะเป็นการ update ค่า _balances[dst] ให้เท่ากับ _balances[dst] += amount;
// unchecked { _balances[dst] += amount; }
mstore(0x00, dst)
mstore(0x20, 0x00)
let dstSlot := keccak256(0x00, 0x40)
sstore(dstSlot, add(sload(dstSlot), amount))
เอาละมา recheck code ตรงนี้กัน
โดย code ตรงนี้จะประกอบด้วย 3 ส่วนคือ
- หาตำแหน่งของ _balances[dst] ใน storage ได้เป็น dstSlot
- ดึงค่าของ _balances[dst] มาจาก storage ด้วย sload
- update ค่าของ _balances[dst] ด้วย sstore
มาเริ่มจาก 2 ส่วนแรก = หาตำแหน่ง และดึงค่าของ _balances กัน
โดยเราจะเอา code อีกชุด ที่อ่านค่า mapping(address => uint256) ได้เหมือนกัน คือ
/* Mapping Functions */
function getMappingValue(uint256 key) external view returns(uint256 value) {
uint256 slot;
assembly {
slot := simpleMapping.slot
}
bytes32 dataLocation = keccak256(abi.encode(key, slot));
assembly {
value := sload(dataLocation)
}
}
จริงๆ code ส่วนนี้มีการใช้ solidity ช่วยในส่วน abi.encode อยู่ แต่เดี๋ยวเราจะเริ่มเขียนเป็น yul เพียวกัน
โอเคมาเริ่มกัน
เริ่มจาก code ในการอ่าน _balances[to] จะได้เป็นแบบนี้
uint256 slot;
assembly {
slot := _balances.slot
}
bytes32 dataLocation = keccak256(abi.encode(to, slot));
assembly {
value := sload(dataLocation)
}
มาเริ่มจากการเช็ค _balances.slot กัน
จะได้
ต่อมาจะเป็นการ sload(dataLocation)
แต่ code นี้ยังใช้ solidity ช่วยหา dataLocation ในส่วน abi.encode อยู่
การมาแปลง abi.encode เป็น yul จะได้เป็น step คือ
- เก็บความยาวของ argument ไว้ใน memory
โดย เราจะ abi.encode(to, slot ของ _balances)
ดังนั้น argument เราน่าจะยาว = 32 + 32 = 64 bytes = 0x40
เอาละเราจะเก็บ ความยาวของ argument ไว้ใน memory ส่วน free memory pointer เหมือนเดิม
จะได้ code เป็น
let ptr := mload(0x40)
mstore(ptr, 0x40)
ต่อมาจะเป็น
2. เก็บ argument ของ abi.encode ไว้ใน memory
โดยจาก argument key และ _balances.slot จะได้ code เป็น
mstore(add(ptr, 0x20), dst)
mstore(add(ptr, 0x40), _balances.slot)
โดยเรา proff ไปแล้วว่า _balances.slot = 0 ดังนั้นจะได้
mstore(add(ptr, 0x20), dst)
mstore(add(ptr, 0x40), 0x00)
สรุปแล้วได้ code คือ
let ptr := mload(0x40)
mstore(ptr, 0x40)
mstore(add(ptr, 0x20), dst)
mstore(add(ptr, 0x40), 0x00)
โอเค อันนี้เป็น code ที่ equivalent กับการใช้ abi.encode ใน solidity
แต่ตัว dataLocation เนี่ย เราจะเอาแค่ค่า dst กับ _balances.slot ไป hash
ไม่ได้เอา length ไป hash ด้วย
ดังนั้นเราจะใช้ memory แค่ 2 slot คือ
- slot สำหรับ dst
2. slot สำหรับ _balances.slot
แต่ในเมื่อเราใช้แค่ 2 slot แปลว่าจริงๆแล้วเราไปใช้ช่วง 0x00–0x3f ในการเก็บข้อมูลก็ได้เหมือนกัน ไม่จำเป็นต้องเป็น free memory pointer
ถ้าเราต้องเก็บข้อมูลยาวกว่านี้ ถึงจะค่อยต้องใช้
ดังนั้นจาก
let ptr := mload(0x40)
mstore(ptr, 0x40)
mstore(add(ptr, 0x20), dst)
mstore(add(ptr, 0x40), 0x00)
เราจะแปลงได้เป็น
let ptr := mload(0x40) => เอาออก
mstore(ptr, 0x40) => เอาออก
mstore(add(ptr, 0x20), dst) => mstore(0x00, dst)
mstore(add(ptr, 0x40), 0x00) => mstore(0x20, 0x00)
จากนั้น เราก็เอาช่วงใน memory ที่เก็บค่าของการ abi.encode ไป hash จะได้เป็น
mstore(0x00, dst)
mstore(0x20, 0x00)
let dstSlot := keccak256(0x00, 0x40)
โอเค เรา proof ตัว location ของ _balances[dst] ใน storage ได้ละ
จากนั้นก็ทำการอ่านค่าออกมาจาก storage
let balanceValue := sload(dstSlot)
และทำการ update เป็นค่าใหม่กลับลงไปใน storage
sstore(dstSlot, add(balanceValue, amount))
จะได้ code เป็น
// unchecked { _balances[dst] += amount; }
mstore(0x00, dst)
mstore(0x20, 0x00)
let dstSlot := keccak256(0x00, 0x40)
let balanceValue := sload(dstSlot)
sstore(dstSlot, add(balanceValue, amount))
ซึ่งเขียนสั้นๆได้เป็น
// unchecked { _balances[dst] += amount; }
mstore(0x00, dst)
mstore(0x20, 0x00)
let dstSlot := keccak256(0x00, 0x40)
sstore(dstSlot, add(sload(dstSlot), amount))
โอเค ส่วนสุดท้ายจะเป็นการ emit event
// emit Transfer(address(0), dst, amount);
mstore(0x00, amount)
log3(0x00, 0x20, _TRANSFER_HASH, 0x00, dst)
โดยมี
// keccak256("Transfer(address,address,uint256)")
bytes32 internal constant _TRANSFER_HASH =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
โดยค่า amount เราจะเอาไปเก็บไว้ใน 0x00–0x1f ก่อน ตาม syntax ของการ emit event ด้วย log3
log3(p, s, t1, t2, t3)
- emits an event with three topics t1
, t2
, t3
and data of size s
starting at memory slot p
โอเค เอาประมาณนี้ก่อนละกัน