ERC20 with some Yul

Nattawat Songsom
7 min readDec 23, 2023

--

มาอ่าน 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

  1. มาเก็บ 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 เค้ากัน

โดยเราจะเริ่มจาก

  1. เก็บ 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 ส่วนคือ

  1. หาตำแหน่งของ _balances[dst] ใน storage ได้เป็น dstSlot
  2. ดึงค่าของ _balances[dst] มาจาก storage ด้วย sload
  3. 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 คือ

  1. เก็บความยาวของ 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 คือ

  1. 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

โอเค เอาประมาณนี้ก่อนละกัน

References

--

--

No responses yet