《DApp设计与开发》Solidity语法与ERC标准模块整理

208 阅读22分钟

Solidity基础语法

solidity建议看这个网站:solidity.readthedocs.org/

◦ solidity数据存储位置

官方文档内容

solidty除了动态大小的数组和 映射mapping (见下文),数据的存储方式是从位置 0 开始连续放置在 存储storage 中。 对于每个变量,根据其类型确定字节大小。

存储大小少于 32 字节的多个变量会被打包到一个 存储插槽storage slot.

  • storage slot 的第一项会以低位对齐的方式储存。
  • 值类型仅使用存储它们所需的字节。
  • 如果 存储插槽storage slot 中的剩余空间不足以储存一个值类型,那么它会被存入下一个 存储插槽storage slot 。
  • 结构体(struct)和数组数据总是会开启一个新插槽(但结构体或数组中的各元素,则按规则紧密打包)。
  • 结构体和数组之后的数据也或开启一个新插槽

映射和动态数组

数组的元素会从 keccak256(p) 开始; 它的布局方式与静态大小的数组相同。一个元素接着一个元素,如果元素的长度不超过16字节,就有可能共享存储槽。
mapping 中的键 k 所对应的槽会位于 keccak256(h(k) . p) ,其中 . 是连接符, h 是一个函数,根据键的类型:

  • 值类型, h 与在内存中存储值的方式相同的方式将值填充为32字节。
  • 对于字符串和字节数组, h(k) 只是未填充的数据。

bytes和string

一般来说,编码与 bytes1[] 类似,即有一个槽用于存放数组本身同时还有一个数据区,数据区位置使用槽的 keccak256 hash计算。 然而,对于短字节数组(短于32字节),数组元素与长度一起存储在同一个槽中。

具体地说:如果数据长度小于等于 31 字节,则元素存储在高位字节(左对齐),最低位字节存储值 length * 2。 如果数据长度大于等于 32 字节,则在主插槽 p 存储 length * 2 + 1 ,数据照常存储在 keccak256(p) 中。 因此,可以通过检查是否设置了最低位:短(未设置最低位)和长(设置最低位)来区分短数组和长数组。

我的总结

静态数组: 如果总数据量小于32就可以放在一个插槽,大于32就依次往后存。若静态数组后面还有其他变量不共用最后一个插槽。
动态数组: keccak256(slot)
mapping: keccak256(bytes32(key)+bytes32(slot)) string: 如果数据小于31就会存储在按顺序的插槽上(剩下一字节存储长度),如果数据大于31就该插槽存储长度,按动态数组的方式存储数据
bytes: 与string类似
bytes1[] 与数组类似,在内存中每个字节数组元素会占一个插槽,而存储里会紧挨在一起

存储变量的位置:

Storage 变量是指永久存储在区块链中的变量。 
Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 calldata 用来保存函数参数的特殊数据位置,是一个只读位置。是不可修改的、非持久的函数参数存储区域,效果大多类似memory。

状态变量(在函数之外声明的变量)默认为“storage”形式,并永久写入区块链;而在函数内部声明的变量默认是“memory”型的,它们函数调用结束后消失。

内存与存储的不同:

uint8[4] a;

数组在存储中占用32字节(1个槽),但在内存中占用128字节(4个元素,每个32字节)

struct S {
    uint a;
    uint b;
    uint8 c;
    uint8 d;
}

下面的结构体在存储中占用 96 (1个槽,每个32字节) ,但在内存中占用 128 个字节(4 个元素每个 32 字节)。

简单记:内存很大方,一个变量就是一个槽

◦ solidity中的基础数据类型及其⻓度

int uint bytes string 都占一个插槽 address 占20字节

定长字节数组:bytes1, bytes2, bytes3, …, bytes32
bytes1[] 当作字节数组使用,但由于填充规则,每个元素会浪费 31 字节(storage存储除外)

◦ solidity数据类型转换

隐式转换

隐式转换情况: 在赋值, 参数传递给函数以及应用运算符时。 例如, uint8 可以转换成 uint16, int128 转换成 int256,但 int8 不能转换成 uint256 (因为 uint256 不能涵盖某些值,例如, -1

显式转换

int8 y = -3;
uint x = uint(y);

这段代码的最后, x 的值将是 0xfffff..fd (64 个 16 进制字符),因为这是 -3 的 256 位补码形式。
如果一个类型显式转换成更小的类型,相应的高位将被舍弃:

uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

如果将整数显式转换为更大的类型,则将填充左侧(即在更高阶的位置)。 转换结果依旧等于原来整数:

uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
assert(a == b);

定长字节数组转换则有所不同, 他们可以被认为是单个字节的序列和转换为较小的类型将切断序列:

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b 为 0x12

如果将定长字节数组显式转换为更大的类型,将按正确的方式填充。

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

如果整数和定长字节数组有相同的大小,则允许他们之间进行显式转换, 如果要在不同的大小的整数和定长字节数组之间进行转换 ,必须使用一个中间类型来明确进行所需截断和填充的规则:

bytes2 a = 0x1234;
uint32 b = uint16(a);           // b 为 0x00001234
uint32 c = uint32(bytes4(a));   // c 为 0x12340000
uint8 d = uint8(uint16(a));     // d 为 0x34
uint8 e = uint8(bytes1(a));     // e 为 0x12

bytes 数组和 bytes calldata 切片可以显示转换为固定长度的 bytes 类型 (bytes1/…/bytes32). 如果数组比固定长度的 bytes 类型,则在末尾处会发生截断。 如果数组比目标类型短,它将在末尾用零填充。

 bytes s = "abcdefgh";
b = bytes16(s);  // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"

字面常量与基本类型的转换

整型与字面常量转换

十进制和十六进制字面常量可以隐式转换为任何足以表示它而不会截断的整数类型 :

uint8 a = 12; //  可行
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 失败, 会截断为 0x3456

定长字节数组与字面常量转换

十进制字面常量不能隐式转换为定长字节数组。十六进制字面常量可以是,但仅当十六进制数字大小完全符合定长字节数组长度。 不过零值例外,零的十进制和十六进制字面常量都可以转换为任何定长字节数组类型:

bytes2 a = 54321; // 不可行
bytes2 b = 0x12; // 不可行
bytes2 c = 0x123; // 不可行
bytes2 d = 0x1234; // 可行
bytes2 e = 0x0012; // 可行
bytes4 f = 0; // 可行
bytes4 g = 0x0; // 可行

字符串字面常量和十六进制字符串字面常量可以隐式转换为定长字节数组,如果它们的字符数与字节类型的大小相匹配

bytes2 a = hex"1234"; // 可行
bytes2 b = "xy"; // 可行
bytes2 c = hex"12"; // 不可行
bytes2 d = hex"123"; // n不可行
bytes2 e = "x"; // 不可行
bytes2 f = "xyz"; // 不可行

地址类型

只有 bytes20 和 uint160 允许显式转换为 address 类型
从 bytes20 或其他整型显示转换为 address 类型时,都会作为 address payable 类型。

一个地址 address a 可以通过 payable(a) 转换为 address payable 类型.

◦ solidity构造函数

如果基构造函数有参数, 派生合约需要指定所有参数。

contract Base {
    uint x;
    constructor(uint x) { x = x; }
}

// 直接在继承列表中指定参数
contract Derived1 is Base(7) {
    constructor() {}
}

// 或通过派生的构造函数中用 修饰符 "modifier"
contract Derived2 is Base {
    constructor(uint y) Base(y * y) {}
}

contract DerivedFromDerived is Derived3 {
    constructor() Base(10 + 10) {}
}

构造函数将始终以线性化顺序执行,无论在继承合约的构造函数中提供其参数的顺序如何。

//  构造函数以以下顺序执行:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// 构造函数以以下顺序执行:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

◦ solidity继承

合约继承使用is关键字。

方法重写

方法重写的前提就是当前合约继承其他合约,并且其他合约中的某个需要被重写的方法标注为virtual

  function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }

在新合约中,我们可以直接继承上述合约并且重写balanceOf,注意新的函数需要增加override标识。

function balanceOf(address owner) public view override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return 0;
    }

注意:

  1. 被重写的函数必须标注为virtual,表示当前合约的继承者可以重写该函数。
  2. 重写别人的函数必须标注为override,表示我重写了父合约的函数。

使用 is 从另一个合约派生,派生合约可以访问所有非私有成员,包括内部(internal)函数和状态变量,但无法通过 this 来外部访问。
重写函数只能将覆盖函数的可见性从 external 更改为 public
可变性nonpayable 可以被 view 和 pure 覆盖。 view 可以被 pure 覆盖。 payable 是一个例外,不能更改为任何其他可变性。
private 的函数是不可以标记为 virtual 的。

contract Base
{
    function foo() virtual external view {}
}

contract Inherited is Middle
{
    function foo() override public pure {}
}

对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。

contract Base1
{
    function foo() virtual public {}
}

contract Base2
{
    function foo() virtual public {}
}

contract Inherited is Base1, Base2
{
    // 继承自两个基类合约定义的foo(), 必须显示的指定 override
    function foo() public override(Base1, Base2) {}

不过如果(重写的)函数继承自一个公共的父合约, override 是可以不用显示指定的

contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 不用显示  override
contract D is B, C {}
  • 除接口之外(因为接口会自动作为 virtual ),没有实现的函数必须标记为 virtual
  • 从 Solidity 0.8.8 开始, 在重写接口函数时不再要求 override 关键字,除非函数在多个父合约定义。

修改器重写

修改器重写也可以被重写,与函数类似

contract Base
{
    modifier foo() virtual {_;}
}

contract Inherited is Base
{
    modifier foo() override {_;}

◦ solidity函数修饰符

  • external

外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。

  • public

public 函数是合约接口的一部分,可以在内部或通过消息调用。

  • internal

内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。 由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。

  • private

private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

状态变量声明为 constant (常量)或者 immutable (不可变量)

  • view

可以将函数声明为 view 类型,这种情况下要保证不修改状态。 下面的语句被认为是修改状态:

  1. 修改状态变量。
  2. 产生事件
  3. 创建其它合约
  4. 使用 selfdestruct
  5. 通过调用发送以太币。
  6. 调用任何没有标记为 view 或者 pure 的函数。
  7. 使用低级调用。
  8. 使用包含特定操作码的内联汇编。
  • Pure 纯函数
    函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态变量。
    它仅处理输入参数和 msg.data ,对当前区块链状态没有任何了解。 这也意味着读取 immutable 变量也不是一个 pure 操作。
    除了上面解释的状态修改语句列表之外,以下被认为是读取状态:
  1. 读取状态变量。
  2. 访问 address(this).balance 或者 <address>.balance
  3. 访问 blocktx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
  4. 调用任何未标记为 pure 的函数。
  5. 使用包含某些操作码的内联汇编。

◦ solidity异常

if(msg.sender != owner) { 
revert(); 
} 
assert(msg.sender == owner);
require(msg.sender == owner);  

revert

错误必须与 revert 语句 一起使用。它会还原当前调用中的发生的所有变化,并将错误数据传回给调用者。
会退还剩下的gas

error InsufficientBalance(uint256 available, uint256 required);

contract TestToken {
    mapping(address => uint) balance;
    function transfer(address to, uint256 amount) public {
        if (amount > balance[msg.sender])
            revert InsufficientBalance({
                available: balance[msg.sender],
                required: amount
            });
        balance[msg.sender] -= amount;
        balance[to] += amount;
    }
   
}

错误产生的数据,会通过revert操作传递给调用者,可以交由链外组件处理或在 try/catch 语句 中捕获它。 注意,只有外部调用的错误才能被捕获。发生在内部调用或同一函数内的 revert 不能被捕获。

assert

assert 函数只能用于测试内部错误,检查不变量。正常的函数代码永远不会产生 Panic , 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug

assert会烧掉所有的gas

require

require()语句的失败报错应该被看作一个正常的判断语句流程不通过的事件
require会退回剩下的gas
Require对比Revert,复杂时候用Revert

◦ solidity事件

事件是能方便地调用以太坊虚拟机日志功能的接口,事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。日志和事件在合约内不可直接被访问

contract TinyAuction {
    event HighestBidIncreased(address bidder, uint amount); // 事件

    function bid() public payable {
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
    }
}

常⻅ERC标准

◦ ERC20/ERC777

ERC20的问题

(1)使用 ERC20 标准没办法在合约里记录是谁发过来多少币,从而没法计算利息(因为接收者合约并不知道自己接收到ERC20代币)。
同样由于ERC20 标准没有一个转账通知机制,很多ERC20代币误转到合约之后,再也没有办法把币转移出来,已经有大量的ERC20 因为这个原因被锁死
(2)ERC20 转账时,无法携带额外的信息,会导致线下沟通成本增加

ERC777

ERC777 在 ERC20的基础上定义了 send(dest, value, data) 来转移代币, send函数额外的参数用来携带其他的信息,send函数会检查持有者和接收者是否实现了相应的钩子函数,如果有实现(不管是普通用户地址还是合约地址都可以实现钩子函数),则调用相应的钩子函数。

ERC1820 接口注册表合约
ERC1820合约提过了两个主要接口:

  • setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer)
    用来设置地址(_addr)的接口(_interfaceHash 接口名称的 keccak256 )由哪个合约实现(_implementer)。
  • getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address)
    这个函数用来查询地址(_addr)的接口由哪个合约实现。

ERC777 使用 send转账时会分别在持有者和接收者地址上使用ERC1820 的getInterfaceImplementer函数进行查询,查看是否有对应的实现合约,ERC777 标准规范里预定了接口及函数名称,如果有实现则进行相应的调用。

操作员

ERC777 定义了一个新的操作员角色,操作员被作为移动代币的地址。 每个地址直观地移动自己的代币,将持有人和操作员的概念分开可以提供更大的灵活性。
此外,ERC777还可以定义默认操作员(默认操作员列表只能在代币创建时定义的,并且不能更改),默认操作员是被所有持有人授权的操作员,这可以为项目方管理代币带来方便,当然认何持有人仍然有权撤销默认操作员。

操作员相关列表:

// 保存默认操作者列表 address[] private _defaultOperatorsArray;
// 保存授权的操作者 mapping(address => mapping(address => bool)) private _operators;
// 保存取消授权的默认操作者 mapping(address => mapping(address => bool)) private _revokedDefaultOperators;
// 操作员 相关的操作(操作员是可以代表持有者发送和销毁代币的账号地址)

function defaultOperators() external view returns (address[] memory); 
function isOperatorFor( address operator, address holder ) external view returns (bool); // 是否是操作员
【操作者是持有者或授权了的操作员才会返回truefunction authorizeOperator(address operator) external;  //授权操作员
【基本逻辑是如果授权的操作员地址在默认操作员里,就把_revokedDefaultOperators里的该地址删除;如果是自己添加的操作员,就保存到_operators里】
function revokeOperator(address operator) external;  //撤销操作员
【基本逻辑是如果撤销操作员地址在默认操作员里,就把_revokedDefaultOperators里的该地址设为true;如果是自己添加的操作员,就删除_operators里的对应地址】

粒度

granularity() 用来定义代币最小的划分粒度(>=1),要求必须在创建时设定,之后不可以更改,不管是在铸币、发送还是销毁操作的代币数量,必须是粒度的整数倍。

// 默认粒度为1,粒度表示代币最小的分隔单位 
function granularity() public view returns (uint256) { 
return 1; 
}

发送代币

send和operatorSend
operatorSend 可以通过参数operatorData携带操作者的信息,发送代币除了执行对应账户的余额加减和触发事件之外,还有额外的规定

image.png

// ERC777 定义的转账函数, 同时触发 ERC20的 `Transfer` 事件 
function send(address recipient, uint256 amount, bytes calldata data) external { 
_send(msg.sender, msg.sender, recipient, amount, data, "", true); 
}

// 转移代币,需要有操作者权限,触发 `Sent` 和 `Transfer` 事件
function operatorSend( address sender, address recipient, uint256 amount, bytes calldata data, bytes calldata operatorData ) external {   
require(isOperatorFor(msg.sender, sender), "ERC777: caller is not an operator for holder");   
_send(msg.sender, sender, recipient, amount, data, operatorData, true); 
}

// 转移 token
// 最后一个参数 requireReceptionAck 表示是否必须实现
ERC777TokensRecipient function _send( address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData, bool requireReceptionAck ) private { 
require(from != address(0), "ERC777: send from the zero address");
require(to != address(0), "ERC777: send to the zero address"); 
_callTokensToSend(operator, from, to, amount, userData, operatorData); 
_move(operator, from, to, amount, userData, operatorData); 
_callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck); 
}

// 尝试调用持有者的 tokensToSend() 函数 
function _callTokensToSend( address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData ) private {
address implementer = _erc1820.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH); 
if (implementer != address(0)) { 
IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData); } 
} 

// 尝试调用接收者的 tokensReceived() function _callTokensReceived( address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData, bool requireReceptionAck ) private {
address implementer = _erc1820.getInterfaceImplementer(to, TOKENS_RECIPIENT_INTERFACE_HASH); 
if (implementer != address(0)) { 
IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData); 
} else if (requireReceptionAck) { 
require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
} 
} 
}

总结一下: 1.外部地址和合约地址先去ERC1820里注册一个ERC777TokensSender /ERC777TokensRecipient实现接口
2.后面转账就会在之前/之后调用tokensToSend/tokensReceived

铸币与销毁

ERC777 则定义了代币从铸币、转移到销毁的整个生命周期。ERC777 没有定义铸币的方法名,只定义了 Minted事件,因为很多代币,是在创建的时候就确定好代币的数量。
如果有需要合约可以自己定义铸币函数,铸币函数在实现时要求:

  1. 必须触发Minted事件
  2. 发行量需要加上铸币量, 接收者是不为 0 ,且接收者余额加上铸币量。
  3. 如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。

ERC777 定义了两个函数用于销毁代币 (burn 和 operatorBurn),可以方便钱包和dapps有统一的接口交互。burn 和 operatorBurn 的实现要求:

  1. 必须触发Burned事件。
  2. 总供应量必须减少代币销毁量, 持有者的余额必须减少代币销毁的数量。
  3. 如果持有者通过ERC1820注册ERC777TokensSender 实现,必须调用持有者的tokensToSend钩子函数。
// 销毁代币实现 function _burn( address operator, address from, uint256 amount, bytes memory data, bytes memory operatorData ) private {
require(from != address(0), "ERC777: burn from the zero address"); 
_callTokensToSend(operator, from, address(0), amount, data, operatorData); 
// Update state variables
_totalSupply = _totalSupply.sub(amount);   
_balances[from] = _balances[from].sub(amount); 
emit Burned(operator, from, amount, data, operatorData); 
emit Transfer(from, address(0), amount);
}

// 铸币函数(即常说的挖矿)
function _mint( address operator, address account, uint256 amount, bytes memory userData, bytes memory operatorData ) internal { 
require(account != address(0), "ERC777: mint to the zero address");
// Update state variables 
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount); 
_callTokensReceived(operator, address(0), account, amount, userData, operatorData, true);
emit Minted(operator, account, amount, userData, operatorData);
emit Transfer(address(0), account, amount);
}

◦ ERC721

有一个_checkOnERC721Received方法即接收者必须实现了onERC721Received这个函数。目的是防止死锁,转到错误地址。只有加了safe前缀的方法才会调用。

◦ ERC1155

ERC1155结合了ERC20和ERC721的能力,这是一个标准接口,支持开发同质化的、半同质化的、非同质化的代币和其他配置的通用智能合约。

区分ERC1155中的某类代币是同质化还是非同质化代币,如果某个id对应的代币总量为1,那么它就是非同质化代币,类似ERC721;如果某个id对应的代币总量大于1,那么他就是同质化代币,因为这些代币都分享同一个id,类似ERC20

IERC1155事件

  • TransferSingle事件:单类代币转账事件,在单币种转账时释放。
  • TransferBatch事件:批量代币转账事件,在多币种转账时释放。
  • ApprovalForAll事件:批量授权事件,在批量授权时释放。
  • URI事件:元数据地址变更事件,在uri变化时释放。

IERC1155函数

  • balanceOf():单币种余额查询,返回account拥有的id种类的代币的持仓量。
  • balanceOfBatch():多币种余额查询,查询的地址accounts数组和代币种类ids数组的长度要相等。
  • setApprovalForAll():批量授权,将调用者的代币授权给operator地址。。
  • isApprovedForAll():查询批量授权信息,如果授权地址operatoraccount授权,则返回true
  • safeTransferFrom():安全单币转账,将amount单位id种类的代币从from地址转账给to地址。如果to地址是合约,则会验证是否实现了onERC1155Received()接收函数。
  • safeBatchTransferFrom():安全多币转账,与单币转账类似,只不过转账数量amounts和代币种类ids变为数组,且长度相等。如果to地址是合约,则会验证是否实现了onERC1155BatchReceived()接收函数。

ERC1155接收合约

ERC721标准类似,为了避免代币被转入黑洞合约,ERC1155要求代币接收合约继承IERC1155Receiver并实现两个接收函数:

  • onERC1155Received():单币转账接收函数,接受ERC1155安全转账safeTransferFrom 需要实现并返回自己的选择器0xf23a6e61
  • onERC1155BatchReceived():多币转账接收函数,接受ERC1155安全多币转账safeBatchTransferFrom 需要实现并返回自己的选择器0xbc197c81

ERC1155主合约

//代币持仓映射,记录代币种类id下某地址account的持仓量balances
mapping(uint256 => mapping(address => uint256)) private _balances;
//批量授权映射,记录持有地址给另一个地址的授权情况。
mapping(address => mapping(address => bool)) private _operatorApprovals;

ERC1155函数

  • supportsInterface():实现ERC165标准,声明它支持的接口,供其他合约检查
  • 查询余额与ERC721标准不同,这里需要输入查询的持仓地址account以及币种id
function balanceOf(address account, uint256 id) public view virtual override returns (uint256){
 require(account != address(0), "ERC1155: address zero is not a valid owner"); 
 return _balances[id][account];                     
 }

批量查询
image.png

  • setApprovalForAll():实现IERC1155setApprovalForAll(),批量授权,释放ApprovalForAll事件。

  • isApprovedForAll():实现IERC1155isApprovedForAll(),查询批量授权信息。

  • safeTransferFrom():实现IERC1155safeTransferFrom(),单币种安全转账,释放TransferSingle事件。与ERC721不同,这里不仅需要填发出方from,接收方to,代币种类id,还需要填转账数额amount

image.png

_beforeTokenTransfer和_afterTokenTransfer都需要自己设计。 _doSafeTransferAcceptanceCheck检查接收者是否实现了onERC1155Received方法。

  • safeBatchTransferFrom():实现IERC1155safeBatchTransferFrom(),多币种安全转账,释放

image.png

  • _mint():单币种铸造函数。

image.png

  • _mintBatch():多币种铸造函数。

image.png

  • _burn():单币种销毁函数。

image.png

  • _burnBatch():多币种销毁函数。

image.png

  • _doSafeTransferAcceptanceCheck:单币种转账的安全检查,被safeTransferFrom()调用,确保接收方为合约的情况下,实现了onERC1155Received()函数。
  • _doSafeBatchTransferAcceptanceCheck:多币种转账的安全检查,,被safeBatchTransferFrom调用,确保接收方为合约的情况下,实现了onERC1155BatchReceived()函数。