티스토리 뷰
전체 코드
solidity 8.23 버전 사용
https://github.com/piatoss3612/dig-solidity/blob/main/yul-erc20/src/ERC20A.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import {IERC20} from "./interfaces/IERC20.sol";
import {IERC20Metadata} from "./interfaces/IERC20Metadata.sol";
import {IERC20Errors} from "./interfaces/IERC20Errors.sol";
contract ERC20A is IERC20, IERC20Metadata, IERC20Errors {
/**
* @dev custom error and its selector
*/
error StringLengthOver31();
error ArithmeticOverflow();
bytes4 private constant STRING_LENGTH_OVER_31 = 0xc1755612;
bytes4 private constant ARITHMETIC_OVERFLOW = 0xe47ec074;
/**
* @dev custom error selectors for ERC-6093
*/
bytes4 private constant ERC20_INSUFFICIENT_BALANCE = 0xe450d38c;
bytes4 private constant ERC20_INVALID_SENDER = 0x96c6fd1e;
bytes4 private constant ERC20_INVALID_RECEIVER = 0xec442f05;
bytes4 private constant ERC20_INSUFFICIENT_ALLOWANCE = 0xfb8f41b2;
bytes4 private constant ERC20_INVALID_APPROVER = 0xe602df05;
bytes4 private constant ERC20_INVALID_SPENDER = 0x94280d62;
/**
* @dev ERC-20 event selectors
*/
bytes32 private constant TRANSFER = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
bytes32 private constant APPROVAL = 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925;
/**
* @dev ERC-20 state variables
*/
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
/**
* @dev ERC-20 metadata
*/
bytes32 private _nameBytes;
bytes32 private _symbolBytes;
uint256 private _nameLength;
uint256 private _symbolLength;
/**
* @dev contract constructor
* @param name_ the name of the token
* @param symbol_ the symbol of the token
* @notice the name and symbol must be less than or equal to 31 characters
*/
constructor(string memory name_, string memory symbol_) {
bytes memory nameBytes = bytes(name_);
bytes memory symbolBytes = bytes(symbol_);
uint256 nameLength = nameBytes.length;
uint256 symbolLength = symbolBytes.length;
assembly {
if or(lt(31, nameLength), lt(31, symbolLength)) { // check if the name or symbol length is greater than 31
mstore(0, STRING_LENGTH_OVER_31)
revert(0, 4) // revert with the reason at offset 0 to 4 bytes in memory
}
}
// set the name and symbol metadata
_nameBytes = bytes32(nameBytes);
_symbolBytes = bytes32(symbolBytes);
_nameLength = nameLength;
_symbolLength = symbolLength;
}
/**
* @dev returns the name of the token
*/
function name() public view returns (string memory name_) {
bytes32 nameBytes = _nameBytes;
uint256 nameLength = _nameLength;
assembly {
name_ := mload(0x40)
mstore(0x40, add(name_, 0x40))
mstore(name_, nameLength)
mstore(add(name_, 0x20), nameBytes)
}
}
/**
* @dev returns the symbol of the token
*/
function symbol() public view returns (string memory symbol_) {
bytes32 symbolBytes = _symbolBytes;
uint256 symbolLength = _symbolLength;
assembly {
symbol_ := mload(0x40)
mstore(0x40, add(symbol_, 0x40))
mstore(symbol_, symbolLength)
mstore(add(symbol_, 0x20), symbolBytes)
}
}
/**
* @dev returns the number of decimals used to get its user representation
*/
function decimals() public pure returns (uint8) {
assembly {
mstore(0x60, 18)
return(0x60, 32)
}
}
/**
* @dev returns the total supply of the token
*/
function totalSupply() public view returns (uint256 totalSupply_) {
assembly {
totalSupply_ := sload(_totalSupply.slot)
}
}
/**
* @dev returns the balance of the account
* @param account the address of the account
*/
function balanceOf(address account) external view returns (uint256 balance_) {
assembly {
mstore(0x00, account)
mstore(0x20, _balances.slot)
balance_ := sload(keccak256(0x00, 0x40))
}
}
/**
* @dev transfers `value` amount of tokens to address `to`
* @param to the address of the recipient
* @param value the amount of tokens to transfer
* @return success whether the transfer was successful
* @notice the caller must have a balance of at least `value`
* @notice the recipient must be a valid address and not the zero address
*/
function transfer(address to, uint256 value) external returns (bool success) {
// get the caller's address
address from;
assembly {
from := caller()
}
// transfer the tokens
_transfer(from, to, value);
// return true
assembly {
success := 1
}
}
/**
* @dev returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through `transferFrom`
* @param owner_ the address of the owner
* @param spender_ the address of the spender
* @return allowance_ the amount of tokens that `spender` will be allowed to spend on behalf of `owner`
*/
function allowance(address owner_, address spender_) public view returns (uint256 allowance_) {
assembly {
mstore(0x00, owner_)
mstore(0x20, _allowances.slot)
mstore(0x20, keccak256(0x00, 0x40))
mstore(0x00, spender_)
allowance_ := sload(keccak256(0x00, 0x40))
}
}
/**
* @dev sets `value` as the allowance of `spender` over the caller's tokens
* @param spender the address of the spender
* @param value the amount of tokens to allow
* @return success whether the approval was successful
*/
function approve(address spender, uint256 value) external returns (bool success) {
address owner_;
assembly {
owner_ := caller()
}
_approve(owner_, spender, value);
assembly {
success := 1
}
}
/**
* @dev transfers `value` amount of tokens from address `from` to address `to`
* @param from the address to transfer from
* @param to the address to transfer to
* @param value the amount of tokens to transfer
* @return success whether the transfer was successful
* @notice the caller must have an allowance of at least `value` for the `from` address
* @notice the `from` address must have a balance of at least `value`
* @notice the `to` address must be a valid address and not the zero address
*/
function transferFrom(address from, address to, uint256 value) external returns (bool success) {
address spender;
assembly {
spender := caller()
}
_spendAllowance(from, spender, value);
_transfer(from, to, value);
assembly {
success := 1
}
}
/**
* @dev move 'value' tokens from 'from' to 'to'
* @param from address to transfer from
* @param to address to transfer to
* @param value amount of tokens to transfer
* @notice the caller must have a balance of at least `value`
* @notice the recipient must be a valid address and not the zero address
*/
function _transfer(address from, address to, uint256 value) internal virtual {
// check if the sender is the zero address
assembly {
if iszero(from) {
mstore(0x00, ERC20_INVALID_SENDER)
mstore(0x04, from)
revert(0x00, 0x24)
}
}
// check if the recipient is the zero address
assembly {
if iszero(to) {
mstore(0x00, ERC20_INVALID_RECEIVER)
mstore(0x04, to)
revert(0x00, 0x24)
}
}
// update the balances
_update(from, to, value);
}
/**
* @dev updates the balances and total supply
* @param from the address to transfer from
* @param to the address to transfer to
* @param value the amount of tokens to transfer
*/
function _update(address from, address to, uint256 value) internal virtual {
// if the sender is the zero address, update the total supply (mint)
// else, subtract the value from the sender's balance
assembly {
switch iszero(from)
case 1 {
// check for arithmetic overflow
let totalSupply_ := sload(_totalSupply.slot)
let newTotalSupply := add(totalSupply_, value)
if lt(newTotalSupply, totalSupply_) {
mstore(0, ARITHMETIC_OVERFLOW)
revert(0, 4)
}
// update the total supply
sstore(_totalSupply.slot, newTotalSupply)
}
default {
// check if the sender has enough balance
mstore(0x00, from)
mstore(0x20, _balances.slot)
let fromBalanceSlot := keccak256(0x00, 0x40)
let fromBalance := sload(fromBalanceSlot)
if lt(fromBalance, value) {
mstore(0x00, ERC20_INSUFFICIENT_BALANCE)
mstore(0x04, from)
mstore(0x24, fromBalance)
mstore(0x44, value)
revert(0x00, 0x64)
}
// update the sender's balance
sstore(fromBalanceSlot, sub(fromBalance, value))
}
}
// if the recipient is the zero address, update the total supply (burn)
// else, add the value to the recipient's balance
assembly {
switch iszero(to)
case 1 {
// update the total supply
sstore(_totalSupply.slot, sub(sload(_totalSupply.slot), value))
}
default {
mstore(0x00, to)
mstore(0x20, _balances.slot)
let toBalanceSlot := keccak256(0x00, 0x40)
let toBalance := sload(toBalanceSlot)
// update the recipient's balance
sstore(toBalanceSlot, add(toBalance, value))
}
}
// emit Transfer(from, to, value)
assembly {
mstore(0x00, value)
log3(0x00, 0x20, TRANSFER, from, to)
}
}
/**
* @dev creates `value` tokens and assigns them to `account`, increasing the total supply
* @param account the address to which the tokens will be assigned
* @param value the amount of tokens to be created
* @notice the recipient must be a valid address and not the zero address
*/
function _mint(address account, uint256 value) internal {
// check if the recipient is the zero address
assembly {
if iszero(account) {
mstore(0x00, ERC20_INVALID_RECEIVER)
mstore(0x04, account)
revert(0x00, 0x24)
}
}
// update the balances
_update(address(0), account, value);
}
/**
* @dev destroys `value` tokens from `account`, reducing the total supply
* @param account the address from which the tokens will be destroyed
* @param value the amount of tokens to be destroyed
* @notice the caller must have a balance of at least `value`
*/
function _burn(address account, uint256 value) internal {
// check if the sender is the zero address
assembly {
if iszero(account) {
mstore(0x00, ERC20_INVALID_SENDER)
mstore(0x04, account)
revert(0x00, 0x24)
}
}
// update the balances
_update(account, address(0), value);
}
/**
* @dev sets `value` as the allowance of `spender` over the `owner`'s tokens
* @param owner_ the address of the owner
* @param spender_ the address of the spender
* @param value the amount of tokens to allow
*/
function _approve(address owner_, address spender_, uint256 value) internal {
_approve(owner_, spender_, value, true);
}
/**
* @dev sets `value` as the allowance of `spender` over the `owner`'s tokens
* @param owner_ the address of the owner
* @param spender_ the address of the spender
* @param value the amount of tokens to allow
* @param emitEvent whether to emit the Approval event
* @notice the owner must be a valid address and not the zero address
* @notice the spender must be a valid address and not the zero address
*/
function _approve(address owner_, address spender_, uint256 value, bool emitEvent) internal virtual {
// check if the owner is the zero address
assembly {
if iszero(owner_) {
mstore(0x00, ERC20_INVALID_APPROVER)
mstore(0x04, owner_)
revert(0x00, 0x24)
}
}
// check if the spender is the zero address
assembly {
if iszero(spender_) {
mstore(0x00, ERC20_INVALID_SPENDER)
mstore(0x04, spender_)
revert(0x00, 0x24)
}
}
// set the allowance
assembly {
mstore(0x00, owner_)
mstore(0x20, _allowances.slot)
mstore(0x20, keccak256(0x00, 0x40))
mstore(0x00, spender_)
let allowanceSlot := keccak256(0x00, 0x40)
sstore(allowanceSlot, value)
}
// emit Approval(owner_, spender_, value)
assembly {
if emitEvent {
mstore(0x00, value)
log3(0x00, 0x20, APPROVAL, owner_, spender_)
}
}
}
/**
* @dev reduces the allowance of `spender` over the `owner`'s tokens by `value`
* @param owner_ the address of the owner
* @param spender_ the address of the spender
* @param value the amount of tokens to reduce the allowance by
* @notice the owner must be a valid address and not the zero address
* @notice the spender must be a valid address and not the zero address
* @notice the caller must have an allowance of at least `value` for the `spender`
*/
function _spendAllowance(address owner_, address spender_, uint256 value) internal {
uint256 currentAllowance = allowance(owner_, spender_);
// check if the spender has enough allowance to spend from the owner
if (currentAllowance != type(uint256). max) {
assembly {
if lt(currentAllowance, value) {
mstore(0x00, ERC20_INSUFFICIENT_ALLOWANCE)
mstore(0x04, spender_)
mstore(0x24, currentAllowance)
mstore(0x44, value)
revert(0x00, 0x64)
}
}
// reduce the allowance
unchecked {
_approve(owner_, spender_, currentAllowance - value, false);
}
}
// max allowance does not need to be reduced and is not checked
}
}
테스트 및 커버리지
$ forge test --mc ERC20OTest
[⠒] Compiling...
No files changed, compilation skipped
Running 23 tests for test/ERC20O.t.sol:ERC20OTest
[PASS] test_Allowance() (gas: 12309)
[PASS] test_Approve() (gas: 42638)
[PASS] test_BalanceOf() (gas: 9975)
[PASS] test_Burn() (gas: 17919)
[PASS] test_Constructor() (gas: 564691)
[PASS] test_Decimals() (gas: 5606)
[PASS] test_Mint() (gas: 46626)
[PASS] test_Name() (gas: 9598)
[PASS] test_RevertApproveWithERC20InvalidApprover() (gas: 14231)
[PASS] test_RevertApproveWithERC20InvalidSpender() (gas: 11970)
[PASS] test_RevertBurnWithERC20InsufficientBalance() (gas: 16088)
[PASS] test_RevertBurnWithERC20InvalidSender() (gas: 11995)
[PASS] test_RevertMintWithArithmeticOverflow() (gas: 15467)
[PASS] test_RevertMintWithERC20InvalidReceiver() (gas: 11974)
[PASS] test_RevertTransferFromWithInsufficientAllowance() (gas: 20359)
[PASS] test_RevertTransferWithERC20InsufficientBalance() (gas: 18168)
[PASS] test_RevertTransferWithERC20InvalidReceiver() (gas: 12212)
[PASS] test_RevertTransferWithInvalidSender() (gas: 14150)
[PASS] test_Symbol() (gas: 9621)
[PASS] test_TotalSupply() (gas: 7625)
[PASS] test_Transfer() (gas: 48635)
[PASS] test_TransferFrom() (gas: 61311)
[PASS] test_TransferWithMaxAllowance() (gas: 80760)
Test result: ok. 23 passed; 0 failed; 0 skipped; finished in 12.34ms
Ran 1 test suites: 23 tests passed, 0 failed, 0 skipped (23 total tests)
$ forge coverage
[⠒] Compiling...
[⠘] Compiling 30 files with 0.8.24
[⠒] Solc 0.8.24 finished in 6.57s
Compiler run successful!
Analysing contracts...
Running tests...
| File | % Lines | % Statements | % Branches | % Funcs |
|-------------------------|-----------------|------------------|-----------------|-----------------|
| src/ERC20A.sol | 100.00% (37/37) | 100.00% (30/30) | 100.00% (10/10) | 100.00% (16/16) |
| src/ERC20O.sol | 100.00% (50/50) | 100.00% (61/61) | 100.00% (24/24) | 100.00% (16/16) |
| src/test/TestERC20A.sol | 100.00% (4/4) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (4/4) |
| src/test/TestERC20O.sol | 100.00% (4/4) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (4/4) |
| src/utils/Context.sol | 33.33% (1/3) | 33.33% (1/3) | 100.00% (0/0) | 33.33% (1/3) |
| Total | 97.96% (96/98) | 98.04% (100/102) | 100.00% (34/34) | 95.35% (41/43) |
가스비 비교
평균 비용을 비교한 결과, name과 symbol 함수를 제외한 대부분의 함수를 호출하는 비용이 inline assembly를 사용하는 경우가 더 저렴하다.
함수 이름 | ERC20A (Inline Assembly) | ERC20 (OpenZeppelin 구현체) |
allowance | 1330 | 1363 |
approve(address,uint256) | 18027 | 18165 |
balanceOf | 1420 | 1438 |
burn | 4540 | 4688 |
decimals | 245 | 266 |
mint | 10925 | 11033 |
name | 2677 | 2178 |
symbol | 2732 | 2222 |
totalSupply | 826 | 826 |
transfer(address,uint256) | 14196 | 14398 |
transferFrom | 19841 | 20081 |
Inline Assembly 사용법: string 또는 bytes를 저장하고 불러오기
1. 길이가 32바이트 미만인 경우
스토리지에 저장되는 형식
string greeting = "hello, world";
function getStringAsWordShorterThan32() public view returns(bytes32 word) {
assembly {
word := sload(greeting.slot)
}
}
greeting 상태 변수가 저장된 스토리지 슬롯에는 다음과 같은 데이터가 저장되어 있습니다.
0x68656c6c6f2c20776f726c640000000000000000000000000000000000000018
0x68656c6c6f2c20776f726c64 : "hello, world"
0x18 : 문자열의 길이 * 2
문자열의 길이는 12
문자열이 왼쪽으로 시프팅된 길이는 32 - 12 = 20
상태 변수의 타입이 bytes로 선언되어 있을 때도 마찬가지입니다. bytes 타입과 string 타입이 상호변환이 가능한 것을 보면 당연한 것이겠지요.
bytes greetingBytes = "hello, world";
function getBytesAsWordShorterThan32() public view returns(bytes32 word) {
assembly {
word := sload(greetingBytes.slot)
}
}
0x68656c6c6f2c20776f726c640000000000000000000000000000000000000018
스토리지에 저장된 문자열 반환
function getGreeting() public view returns (string memory _greeting) {
assembly {
let word := sload(greeting.slot)
let length := div(and(word, 0xff), 2)
let data := xor(word, and(word, 0xff))
_greeting := mload(0x40)
mstore(0x40, add(_greeting, 0x40))
mstore(_greeting, length)
mstore(add(_greeting, 0x20), data)
}
}
우선 문자열의 길이와 32byte 길이의 word에서 길이를 나타내는 마지막 바이트를 제거한 data를 구합니다.
let word := sload(greeting.slot) // 0x68656c6c6f2c20776f726c640000000000000000000000000000000000000018
let length := div(and(word, 0xff), 2) // 0x0c
let data := xor(word, and(word, 0xff)) // 0x68656c6c6f2c20776f726c640000000000000000000000000000000000000000
그리고 mload(0x40)을 사용해 free memory의 주소를 _greeting에 할당합니다. 일반적으로 free memory를 가리키는 값은 0x80입니다.
free memory가 0x80부터 시작하는 이유는 0x00-0x7f는 solidity에 의해 특정 용도로 예약되어 있는 공간이기 때문입니다. 그렇다고해서 아예 사용할 수 없는 것은 아니지만 가능하면 free memory를 사용하는 것이 권장됩니다.
_greeting := mload(0x40)
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "0000000000000000000000000000000000000000000000000000000000000080\t????????????????????????????????"
}
마지막으로 문자열의 길이와 문자열 자체를 메모리에 저장합니다.
mstore(0x40, add(_greeting, 0x40)) // renew free memory pointer
mstore(_greeting, length) // length
mstore(add(_greeting, 0x20), data) // data
- free memory의 시작 위치를 갱신합니다. _greeting 0x80과 0x40을 더한 결과인 0xc0을 메모리의 offset이 0x40인 위치에 저장합니다.
mstore(0x40, add(_greeting, 0x40)) // renew free memory pointer
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "00000000000000000000000000000000000000000000000000000000000000c0\t????????????????????????????????"
}
- length: 데이터의 길이를 가리킵니다. 메모리의 offset이 0x80(_greeting)인 위치에 length 0x0c를 저장합니다.
mstore(_greeting, length) // length
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "00000000000000000000000000000000000000000000000000000000000000c0\t????????????????????????????????",
"0x60": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x80": "000000000000000000000000000000000000000000000000000000000000000c\t???????????????????????????????\f"
}
- data: 메모리의 offset이 0xa0인 위치에 data를 저장합니다.
mstore(add(_greeting, 0x20), data) // data
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "00000000000000000000000000000000000000000000000000000000000000c0\t????????????????????????????????",
"0x60": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x80": "000000000000000000000000000000000000000000000000000000000000000c\t???????????????????????????????\f",
"0xa0": "68656c6c6f2c20776f726c640000000000000000000000000000000000000000\thello? world????????????????????"
}
명시적으로 반환되는 _greeting이 가리키는 메모리 위치에는 문자열의 길이가 저장되어 있습니다. 이 정보를 바탕으로 컨트랙트에서는 추가적인 연산을 거쳐 문자열을 반환합니다.
스토리지에 저장된 문자열 반환2
앞선 방법은 직관성이 떨어지기 때문에 다른 방법을 사용할 수도 있습니다. 직접 ABI 인코딩 형식으로 문자열을 반환하는 것입니다.
function getGreeting() public view returns (string memory) {
assembly {
let word := sload(greeting.slot)
let length := div(and(word, 0xff), 2)
let data := xor(word, and(word, 0xff))
let ptr := mload(0x40)
mstore(ptr, 0x20)
mstore(add(ptr, 0x20), length)
mstore(add(ptr, 0x40), data)
return(ptr, 0x60)
}
}
string 및 bytes 타입의 ABI 인코딩은 다음과 같은 구조로 되어 있습니다.
offset을 가리키는 32바이트 + 길이를 나타내는 32바이트 + 문자열 (32바이트의 배수 길이)
이하의 inline assembly는 ABI 형식의 문자열을 구성하여 반환합니다.
let ptr := mload(0x40)
mstore(ptr, 0x20)
mstore(add(ptr, 0x20), length)
mstore(add(ptr, 0x40), data)
return(ptr, 0x60)
- 메모리의 offset이 0x80인 위치에 문자열의 offset 0x20을 저장합니다.
mstore(ptr, 0x20)
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "0000000000000000000000000000000000000000000000000000000000000080\t????????????????????????????????",
"0x60": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x80": "0000000000000000000000000000000000000000000000000000000000000020\t??????????????????????????????? "
}
- 메모리의 offset이 0x0a인 위치에 문자열의 길이를 저장합니다.
mstore(add(ptr, 0x20), length)
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "0000000000000000000000000000000000000000000000000000000000000080\t????????????????????????????????",
"0x60": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x80": "0000000000000000000000000000000000000000000000000000000000000020\t??????????????????????????????? ",
"0xa0": "000000000000000000000000000000000000000000000000000000000000000c\t???????????????????????????????\f"
}
- 메모리의 offset이 0x0c인 위치에 문자열을 저장합니다.
mstore(add(ptr, 0x40), data)
{
"0x0": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x20": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x40": "0000000000000000000000000000000000000000000000000000000000000080\t????????????????????????????????",
"0x60": "0000000000000000000000000000000000000000000000000000000000000000\t????????????????????????????????",
"0x80": "0000000000000000000000000000000000000000000000000000000000000020\t??????????????????????????????? ",
"0xa0": "000000000000000000000000000000000000000000000000000000000000000c\t???????????????????????????????\f",
"0xc0": "68656c6c6f2c20776f726c640000000000000000000000000000000000000000\thello? world????????????????????"
}
- 메모리의 offset이 0x80(ptr)인 위치에서부터 길이가 0x60이 되는 바이트열을 반환합니다.
return(ptr, 0x60)
2. 길이가 32바이트 이상인 경우
스토리지에 저장되는 형식
// 44 bytes string
string greeting2 = "Hello, World! Hello, Earth! Hello, Universe!";
function getStringAsWordsLongerThan31() public view returns (bytes32 word1, bytes32 word2, bytes32 word3) {
assembly {
let slot := greeting2.slot
word1 := sload(slot)
mstore(0x00, slot)
let dataLocation := keccak256(0x00, 0x20)
word2 := sload(dataLocation)
word3 := sload(add(dataLocation, 1))
}
}
함수 호출 결과는 다음과 같습니다.
- greeting2가 가리키는 슬롯 p : 문자열의 길이 *2 + 1가 저장되어 있습니다.
- keccack256(abi.encode(p))에 해당하는 슬롯~ : 문자열이 저장되어 있습니다.
word1: 0x0000000000000000000000000000000000000000000000000000000000000059
word2: 0x48656c6c6f2c20576f726c64212048656c6c6f2c204561727468212048656c6c
word3: 0x6f2c20556e697665727365210000000000000000000000000000000000000000
스토리지에 저장된 문자열 반환
길이가 31 이하인 문자열과 동일한 방법을 사용하면 됩니다. 자세한 내용은 글이 너무 길어지기 때문에 스킵하도록 하겠습니다.
Inline Assembly 사용법: 이벤트 로그 생성
로그 생성 함수
- log0(p, s) : 메모리의 p부터 s만큼의 바이트를 사용해 로그를 생성합니다.
- log1(p, s, t1) : t1 토픽을 지정하여 로그를 생성합니다.
- log2(p, s, t1, t2) : t1, t2 토픽을 지정하여 로그를 생성합니다.
- log3(p, s, t1, t2, t3) : t1, t2, t3 토픽을 지정하여 로그를 생성합니다.
- log4(p, s, t1, t2, t3, t4) : t1, t2, t3, t4 토픽을 지정하여 로그를 생성합니다.
ERC-20 Transfer 예시
- 첫 번째 토픽은 반드시 이벤트 선택자가 들어가야 합니다. 이벤트 선택자는 32바이트 크기입니다.
- 두 번째 토픽부터는 이벤트에서 indexed가 명시된 토픽들이 차례로 들어갑니다.
- indexed가 명시되지 않은 파라미터들은 data로 인코딩하여 사용해야 합니다.
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address from, address to, uint256 value) public {
bytes32 selector = Transfer.selector;
// emit Transfer(from, to, value);
assembly {
mstore(0x00, value)
log3(0x00, 0x20, selector, from, to)
}
}
from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
value: 100000000000000000
[
{
"from": "0xB9e2A2008d3A58adD8CC1cE9c15BF6D4bB9C6d72",
"topic": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"event": "Transfer",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
"2": "100000000000000000",
"from": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"to": "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
"value": "100000000000000000"
}
}
]
Inline Assembly 사용법: 커스텀 에러 반환
파라미터가 없는 커스텀 에러
커스텀 에러의 선택자 4바이트만을 반환합니다.
error MyCustomError();
function revertError1() public {
bytes4 selector = MyCustomError.selector;
// revert MyCustomError();
assembly {
mstore(0x40, selector)
revert(0x40, 4)
}
}
파라미터가 있는 커스텀 에러
커스텀 에러의 선택자 4바이트와 파라미터를 ABI 인코딩한 결과를 함께 반환합니다.
error MyCustomError2(address from, uint256 balance);
function revertError2() public {
bytes4 selector = MyCustomError2.selector;
address from = msg.sender;
uint256 _balance = from.balance;
assembly {
mstore(0x40, selector)
mstore(0x44, from)
mstore(0x64, _balance)
revert(0x40, 0x44)
}
}
파라미터가 있는 커스텀 에러 (동적 타입)
string이나 bytes 타입이 파라미터로 필요한 경우, offset + length + data 형식으로 인코딩이 필요합니다.
error MyCustomError3(string reason);
function reverError3() public {
bytes4 selector = MyCustomError3.selector;
string memory _greeting = "Hello, World!";
assembly {
mstore(0x40, selector)
mstore(0x44, 0x20) // offset
mstore(0x64, mload(_greeting)) // length
mstore(0x84, mload(add(_greeting, 0x20))) // data
revert(0x40, 0x64)
}
}
Inline Assembly 활용 전체 코드
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
contract YulPractice {
string greeting = "hello, world";
function getStringAsWordShorterThan32() public view returns(bytes32 word) {
assembly {
word := sload(greeting.slot)
}
}
function getGreeting() public view returns (string memory) {
assembly {
let word := sload(greeting.slot)
let length := div(and(word, 0xff), 2)
let data := xor(word, and(word, 0xff))
let ptr := mload(0x40)
mstore(ptr, 0x20)
mstore(add(ptr, 0x20), length)
mstore(add(ptr, 0x40), data)
return(ptr, 0x60)
}
}
// 44 bytes string
string greeting2 = "Hello, World! Hello, Earth! Hello, Universe!";
function getStringAsWordsLongerThan31() public view returns (bytes32 word1, bytes32 word2, bytes32 word3) {
assembly {
let slot := greeting2.slot
word1 := sload(slot)
mstore(0x00, slot)
let dataLocation := keccak256(0x00, 0x20)
word2 := sload(dataLocation)
word3 := sload(add(dataLocation, 1))
}
}
event Transfer(address indexed from, address to, uint256 value);
function transfer(address from, address to, uint256 value) public {
bytes32 selector = Transfer.selector;
// emit Transfer(from, to, value);
assembly {
mstore(0x00, value)
log3(0x00, 0x20, selector, from, to)
}
}
error MyCustomError();
function revertError1() public {
bytes4 selector = MyCustomError.selector;
// revert MyCustomError();
assembly {
mstore(0x40, selector)
revert(0x40, 4)
}
}
error MyCustomError2(address from, uint256 balance);
function revertError2() public {
bytes4 selector = MyCustomError2.selector;
address from = msg.sender;
uint256 _balance = from.balance;
assembly {
mstore(0x40, selector)
mstore(0x44, from)
mstore(0x64, _balance)
revert(0x40, 0x44)
}
}
error MyCustomError3(string reason);
function reverError3() public {
bytes4 selector = MyCustomError3.selector;
string memory _greeting = "Hello, World!";
assembly {
mstore(0x40, selector)
mstore(0x44, 0x20)
mstore(0x64, mload(_greeting))
mstore(0x84, mload(add(_greeting, 0x20)))
revert(0x40, 0x64)
}
}
}
참고
'Solidity > Foundry' 카테고리의 다른 글
Foundry 프로젝트에서 Hardhat 같이 사용하기 (0) | 2024.04.21 |
---|---|
[Foundry] .env 파일 사용하지 마세요 (0) | 2024.02.23 |
Foundry 테스트 작성하기 (0) | 2023.10.28 |
Foundry 프로젝트 살펴보기 (1) | 2023.10.27 |
Foundry 설치 (0) | 2023.10.22 |