【Solidity Yul Assembly】4.2 | 100% Yul ERC20 Example

  • 0xE
  • 发布于 2024-09-10 09:59
  • 阅读 894

在本节中,我们将详细讲解官方文档中的 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 合约的各项功能,包括 balanceOftotalSupplytransfertransferFrom 等常见的 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 函数的实现。

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

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

剩余50%的内容订阅专栏后可查看

点赞 1
收藏 0
分享

0 条评论

请先 登录 后评论
0xE
0xE
0x59f6...a17e
17年进入币圈,Web3 开发者。刨根问底探链上真相,品味坎坷悟 Web3 人生。有工作机会可加v:__0xE__