ERC1155 是 ERC721 之外的另一种 NFT 标准,可以理解为 ERC20 与 ERC721 的融合体。简单举几个例子理解一下:
ERC20 就好像银行发行的一百元纸币,每张纸币的价值相同
ERC721 就好像艺术馆里面的艺术品,每件艺术品都是独一无二的
ERC1155 类似于游戏中的道具,比如一些普通道具可能有成千上万件,而一些稀有道具只有几十件。也类似于银行发行的不同面额纸币,相同面额价值相同,可以互换。不同面额价值不同,不可互换
对应到代码实现中:
ERC20 合约中每个 token 是同一种类型,它们的价值都是一样的
ERC721 合约中每个 tokenId 都是不同类型,各自拥有不同的属性。每个 tokenId 有且只有一个
ERC1155 合约中根据 id 有不同的分类,每一个 id 是一种类型。当这个 id 下有多个 token 时,就是 fungible token(类似 ERC20);当这个 id 下只有一个 token 时,就是 non-fungible token(类似ERC721)
我们直接来看一下 OpenZeppelin 官方文档给出的一个例子:
// contracts/GameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
// 继承ERC1155标准
contract GameItems is ERC1155 {
// 发行5种道具,id分别为0~5
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant THORS_HAMMER = 2;
uint256 public constant SWORD = 3;
uint256 public constant SHIELD = 4;
constructor() ERC1155("https://game.example/api/item/{id}.json") {
// id不同,mint的数量不同
_mint(msg.sender, GOLD, 10**18, "");
_mint(msg.sender, SILVER, 10**27, "");
// THORS_HAMMER只mint一个,即non-fungible
_mint(msg.sender, THORS_HAMMER, 1, "");
_mint(msg.sender, SWORD, 10**9, "");
_mint(msg.sender, SHIELD, 10**9, "");
}
}
接下来看看 OpenZeppelin ERC1155 的代码。
数据结构:
// id -> 用户地址 -> 数量,即用户拥有的某id token的数量
mapping(uint256 => mapping(address => uint256)) private _balances;
// 授权地址信息
// owner -> 授权人 -> 是否授权
mapping(address => mapping(address => bool)) private _operatorApprovals;
// uri,根据id区分,格式应该为 https://token-cdn-domain/{id}.json
string private _uri;
这里需要注意一下 uri 的格式,根据 OpenZeppelin 文档:
The
urican include the string{id}which clients must replace with the actual token ID, in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters.For token ID
2and urihttps://game.example/api/item/{id}.jsonclients would replace{id}with0000000000000000000000000000000000000000000000000000000000000002to retrieve JSON athttps://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json
所有的 id,前后缀都相同,只是在 id 部分区分,需要将其补足为 64 位。同时,这个 uri 打开后的格式应该为 Json 文本,例如:
{
"name": "Thor's hammer",
"description": "Mjölnir, the legendary hammer of the Norse god of thunder.",
"image": "https://game.example/item-id-8u5h2m.png",
"strength": 20
}
在代码中看到,ERC1155 只能对所有的 token 进行授权,而不能像 ERC721 那样只对某一个 tokenId 进行授权(ERC721 也包括全部授权的方法)。
查询用户某一个 id 类型下的数量:
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: balance query for the zero address");
return _balances[id][account];
}
转账:
function _safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = _msgSender();
_beforeTokenTransfer(operator, from, to, _asSingletonArray(id), _asSingletonArray(amount), data);
// 更新转出与转入者的数量
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
emit TransferSingle(operator, from, to, id, amount);
_doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}
其他的方法例如 mint, burn 等其实都大同小异,没有什么难度。
在转账时,如果接收地址是合约,需要验证其实现了钩子函数接口,防止 token 永久锁死在合约中:
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (to.isContract()) {
// 校验合约是否实现了 onERC1155Received 方法
try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}
ERC1155 概念与代码实现都很简单。我个人觉得 ERC1155 更适合的场景应该是在游戏中,比如对应不同数量的道具这类场景。而 ERC721 则更适合艺术品这个场景。目前在 OpenSea 上的艺术品还是以 ERC721 居多。
ERC1155 是 ERC721 之外的另一种 NFT 标准,可以理解为 ERC20 与 ERC721 的融合体。简单举几个例子理解一下:
ERC20 就好像银行发行的一百元纸币,每张纸币的价值相同
ERC721 就好像艺术馆里面的艺术品,每件艺术品都是独一无二的
ERC1155 类似于游戏中的道具,比如一些普通道具可能有成千上万件,而一些稀有道具只有几十件。也类似于银行发行的不同面额纸币,相同面额价值相同,可以互换。不同面额价值不同,不可互换
对应到代码实现中:
ERC20 合约中每个 token 是同一种类型,它们的价值都是一样的
ERC721 合约中每个 tokenId 都是不同类型,各自拥有不同的属性。每个 tokenId 有且只有一个
ERC1155 合约中根据 id 有不同的分类,每一个 id 是一种类型。当这个 id 下有多个 token 时,就是 fungible token(类似 ERC20);当这个 id 下只有一个 token 时,就是 non-fungible token(类似ERC721)
我们直接来看一下 OpenZeppelin 官方文档给出的一个例子:
// contracts/GameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
// 继承ERC1155标准
contract GameItems is ERC1155 {
// 发行5种道具,id分别为0~5
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant THORS_HAMMER = 2;
uint256 public constant SWORD = 3;
uint256 public constant SHIELD = 4;
constructor() ERC1155("https://game.example/api/item/{id}.json") {
// id不同,mint的数量不同
_mint(msg.sender, GOLD, 10**18, "");
_mint(msg.sender, SILVER, 10**27, "");
// THORS_HAMMER只mint一个,即non-fungible
_mint(msg.sender, THORS_HAMMER, 1, "");
_mint(msg.sender, SWORD, 10**9, "");
_mint(msg.sender, SHIELD, 10**9, "");
}
}
接下来看看 OpenZeppelin ERC1155 的代码。
数据结构:
// id -> 用户地址 -> 数量,即用户拥有的某id token的数量
mapping(uint256 => mapping(address => uint256)) private _balances;
// 授权地址信息
// owner -> 授权人 -> 是否授权
mapping(address => mapping(address => bool)) private _operatorApprovals;
// uri,根据id区分,格式应该为 https://token-cdn-domain/{id}.json
string private _uri;
这里需要注意一下 uri 的格式,根据 OpenZeppelin 文档:
The
urican include the string{id}which clients must replace with the actual token ID, in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters.For token ID
2and urihttps://game.example/api/item/{id}.jsonclients would replace{id}with0000000000000000000000000000000000000000000000000000000000000002to retrieve JSON athttps://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json
所有的 id,前后缀都相同,只是在 id 部分区分,需要将其补足为 64 位。同时,这个 uri 打开后的格式应该为 Json 文本,例如:
{
"name": "Thor's hammer",
"description": "Mjölnir, the legendary hammer of the Norse god of thunder.",
"image": "https://game.example/item-id-8u5h2m.png",
"strength": 20
}
在代码中看到,ERC1155 只能对所有的 token 进行授权,而不能像 ERC721 那样只对某一个 tokenId 进行授权(ERC721 也包括全部授权的方法)。
查询用户某一个 id 类型下的数量:
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: balance query for the zero address");
return _balances[id][account];
}
转账:
function _safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = _msgSender();
_beforeTokenTransfer(operator, from, to, _asSingletonArray(id), _asSingletonArray(amount), data);
// 更新转出与转入者的数量
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
emit TransferSingle(operator, from, to, id, amount);
_doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}
其他的方法例如 mint, burn 等其实都大同小异,没有什么难度。
在转账时,如果接收地址是合约,需要验证其实现了钩子函数接口,防止 token 永久锁死在合约中:
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (to.isContract()) {
// 校验合约是否实现了 onERC1155Received 方法
try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}
ERC1155 概念与代码实现都很简单。我个人觉得 ERC1155 更适合的场景应该是在游戏中,比如对应不同数量的道具这类场景。而 ERC721 则更适合艺术品这个场景。目前在 OpenSea 上的艺术品还是以 ERC721 居多。
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer
Smart Contract Developer
Share Dialog
Share Dialog

Subscribe to xyyme.eth

Subscribe to xyyme.eth
<100 subscribers
<100 subscribers
No activity yet