今天介绍一下ERC721A的源码.在弄懂源码之前,首先要明白二进制的左移右移运算以及加减法操作.这是ERC721的基础.
二进制加法
需要说明的是,在计算机中做二进制数运算时,一定要明确是在多少位的整型前提下进行的,这样才能够正确处理位数溢出的问题
我们以32位,计算5+3为例,5的二进制表示为 0000… 0101,3的二进制表示为0000…0011,然后逐位二进制加法,0000…0101+0000…0011=0000…1000,然后0000…转为10进制为2^3=8.
二进制减法
减法和加法一样,只过不是把A-B变成A+(-B).-B是通过取反然后加1(补码)得到.例如计算5-3,5的二进制表示为 0000… 0101.将3取反(0变成1,1变为0),得到1111…1100,然后加1,得到1111…1101,最后逐位进行计算0000… 0101+1111…1101=10000…0010,最高位溢出的将被舍弃.所以最终果是0000…0010,转为十进制是2.
位移操作
- 左移
所有位向左移,低位补0.左移n位,相当于乘以2的n次方(只有在不溢出的情况下成立).
x << n = x *(2^n)
- 右移
右移 n 位,相当于除以 2 的 n 次方,向下取整(保留符号)
x >> n ≡ floor(x / 2^n)
// 以正数为例
0000 1010 (十进制 10)
>> 1
= 0000 0101 (十进制 5)
有了上述的基本概念,我们解释一下ERC721A中合约的源码.例如:
uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225;
_BITMASK_NEXT_INITIALIZED意思是左移225位(2^225),主要用于掩码操作.
uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1;
_BITMASK_ADDRESS,左移160位,为1000..000(160个0),然后减1,-1表示为1111...1111,_BITMASK_ADDRESS实际可表示为111...111(161个1).
我们再来看看ERC721A是怎样把多个变量拼接到同一个uint256中.如下代码:
// Mapping from token ID to ownership details
// An empty struct value does not necessarily mean the token is unowned.
// See {_packedOwnershipOf} implementation for details.
//
// Bits Layout:
// - [0..159] `addr`
// - [160..223] `startTimestamp`
// - [224] `burned`
// - [225] `nextInitialized`
// - [232..255] `extraData`
mapping(uint256 => uint256) private _packedOwnerships;
_packedOwnerships是一个mapping,key是token ID,value是对应的owner详情,包含了address、startTimestam等多个信息,从注释可以看出,每个信息都有对应的区间位置.源码中是通过_packOwnershipData方法对数据进行处理:
function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) {
assembly {
// Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean.
owner := and(owner, _BITMASK_ADDRESS)
// `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`.
result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags))
}
}
and、or、sh1是solidity中内联汇编的语法,具体请查看https://solidity-cn.readthedocs.io/zh/develop/assembly.html#id4
owner := and(owner, _BITMASK_ADDRESS)
是一个按位与运算,它的作用是将变量 owner 和 _BITMASK_ADDRESS 进行按位与操作,**清洗掉 owner 中无关的高位,只保留低 160 位地址部分。shl(pos,x)**等价于执行 x << pos,shl(_BITPOS_START_TIMESTAMP, timestamp()的意思是将timestamp()左移160位.在看flags的实际处理方式:
function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) {
// For branchless setting of the `nextInitialized` flag.
assembly {
// `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`.
result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1))
}
}
eq是内联汇编的操作码,相当于(quantity == 1) ? 1 : 0,这个位移操作,相当于:
uint256 private constant _BITPOS_NEXT_INITIALIZED = 225;
(1 << _BITPOS_NEXT_INITIALIZED) // 如果 quantity == 1
(0 << _BITPOS_NEXT_INITIALIZED) // 如果 quantity != 1
224位没有设置,默认是0
所以_packedOwnerships的数据拼接结构为:
225 | 224 | 160-223 | 0-159 |
---|---|---|---|
nextInitialized | burned | startTimestamp | address |
同理我们查看_packedAddressData
// Bits Layout:
// - [0..63] `balance`
// - [64..127] `numberMinted`
// - [128..191] `numberBurned`
// - [192..255] `aux`
mapping(address => uint256) private _packedAddressData;
_packedAddressData也是分区间存储多个信息.
_packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);
// quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1) 相当于 quantity << _BITPOS_NUMBER_MINTED + quantity
// 这里_packedAddressData存储了numberMinted与balance