在本节中,我们将详细讲解官方文档中的 100% Yul 实现的 ERC20 合约。
在本节中,我们将讲解官方文档中的 100% Yul 实现的 ERC20 合约。为了方便理解,我们会逐步讲解,并在适当的位置附上代码。
首先,来看合约的构造函数部分:
code {
// Store the creator in slot zero.
sstore(0, caller())
// Deploy the contract
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
这里的构造函数与上节中的例子实现有所不同,主要是将合约创建者的地址存储在存储槽 0 中。这是为了记录合约的 owner。
接下来,在 switch
语句中实现了函数选择器的功能,它负责 ERC20 的各项操作逻辑:
// Protection against sending Ether
require(iszero(callvalue()))
// Dispatcher
switch selector()
case 0x70a08231 /* "balanceOf(address)" */ {
returnUint(balanceOf(decodeAsAddress(0)))
}
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
case 0xa9059cbb /* "transfer(address,uint256)" */ {
transfer(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
returnTrue()
}
case 0x095ea7b3 /* "approve(address,uint256)" */ {
approve(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0xdd62ed3e /* "allowance(address,address)" */ {
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
}
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
default {
revert(0, 0)
}
...
function selector() -> s {
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
}
...
以上代码展示了 ERC20 合约的各项功能,包括 balanceOf
、totalSupply
、transfer
、transferFrom
等常见的 ERC20 操作。这些函数都是 non-payable
的,这意味着它们不能接受主网币。因此,在调用这些函数之前,合约会检查交易是否附带了主网币。如果附带了主网币,交易将被拒绝。顺带一提,在 Solidity 中直接使用 payable
关键字修饰函数可以节省 gas 费用。
selector
函数用于从 calldata 中提取函数选择器。它通过 calldataload(0)
从 calldata 的起始位置(偏移量 0)读取 32 字节的数据。接着,将读取的数据右移 224 位,只保留最左侧的 4 字节(即函数选择器部分)。
再来看 runtime 对象中的代码:
object "runtime" {
code {
// Protection against sending Ether
require(iszero(callvalue()))
...
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
}
这里实现了一个简单的 require
函数。如果传入的 condition
为 0,合约将调用 revert 来回滚交易。require 并不是 Yul 的内置函数,而是此处自定义的。
接下来,我们来详细看各个 ERC20 函数的实现。
totalSupply()
...
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
...
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function totalSupply() -> supply {
supply := sload(totalSupplyPos())
}
function totalSupplyPos() -> p { p := 1 }
totalSupply()
的返回值通过 returnUint(v)
函数返回。
returnUint(v)
是把值 v
存储在内存槽 0 中,之后 return
内存槽 0 中的数据。回顾之前说的,return
是结束当前函数的调用并归还控制权和结果,和 leave
是不同的。
totalSupply()
函数从存储槽 totalSupplyPos()
读取数据,totalSupplyPos()
始终返回槽位 1。因此,总供应量存储在存储槽 1 中。由于 Yul 中不支持直接使用变量名来表示存储位置,所以通过定义函数 totalSupplyPos()
来返回指定的槽位,
顺便看下,returnTrue()
函数,就是把 1 放在内存中再返回,于是总为真。
function returnTrue() {
returnUint(1)
}
mint(address,uint256)
...
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
...
function mint(account, amount) {
require(calledByOwner())
mintTokens(amount)
addToBalance(account, amount)
emitTransfer(0, account, amount)
}
1. 检查调用者是否为合约所有者
首先,mint 函数通过 calledByOwner()
检查当前调用者是否是合约的所有者:
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
function calledByOwner() -> cbo {
cbo := eq(owner(), caller())
}
function owner() -> o {
o := sload(ownerPos())
}
function ownerPos() -> p { p := 0 }
owner()
函数从存储槽 0 中读取合约所有者的地址,而 calledByOwner()
会将读取到的地址与当前调用者 caller()
进行对比,确保只有所有者可以调用 mint
函数。
2. 增加总供应量
通过检查后,mintTokens 函数会增加代币的总供应量:
function mintTokens(amount) {
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
}
function safeAdd(a, b) -> r {
r := add(a, b)
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
}
mintTokens
函数首先读取当前的总供应量,将其与铸造的数量相加,然后将结果存储到存储槽 `totalSupp...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!