简单DApp开发——NFTMarket合约开发
简单DApp开发——NFTMarket合约开发
什么是DApp?
DApp是一种利用智能合约处理数据,利用区块链网络存储数据的去中心化应用。与传统中心化应用需要用户将数据传输到中心化的服务器再进行处理和存储不同,DApp使用户与智能合约进行交互,并自动的生成不可篡改、价值唯一、公开透明、可溯源的账本数据,并存储在区块链网络中,这使数据的安全性得到保障。
同时DApp因为其匿名性,无需用户提供其自身信息,这对用户信息起到保护作用。
当今社会,应用提供服务时,往往要求我们提供许多完全多余的身份信息。例如,当我们来到一家饭店,想要使用某微信小程序自助点餐,我们往往需要关注其公众号,提供本人使用的手机号验证,才能得以点餐。这一系列过程繁琐而没有实质性的必要,并且有极大的泄露个人信息的危险(不排除是恶意服务商的有意为之)。这模糊了人的身份。在合理的逻辑下,当我进入饭店,坐下并意图点餐的时候,我便应该获取了顾客的身份;而在当前的逻辑下,我需要通过我的一系列真实的身份信息去申请一个顾客的身份,用永久使用的(或者说是长期使用的)身份信息去换取一个短暂使用的身份,这是完全是本末倒置的。
什么是ERC
ERC是Ethereum Request for Comments(以太坊征求意见提案)的缩写,代表以太坊已正式化的提案,它是由EIP(Ethereum Improvement Proposals以太坊升级提案)经过以太坊开发团队各种审议和测试后通过的一种提案,即对有用提案进行标准化,从而实现对开发者提供模版帮助以及标准限制。而其后的20\721\1155则代表提案号,ERC-20则代表第20号提案,其它提案号亦然。
ERC20——以太坊标准代币发行协议
什么是代币?
在以太坊中,代币可以表示几乎一切可以通过数值衡量的东西:
- 游戏角色的数值
- 账户资产
- 金融债券
- 某种货币
- ……
这些东西往往都代表着价值,而不同系统、不同的事物之间往往需要有价值的流动(例如,用蓝绿修改器去提升游戏角色属性数值),这就需要一个统一的价值界定标准。ERC-20提出了一种同质化货币(Fungible token)的标准,使得每种代币在类型与价值上与其他任何代币都完全相同。
ERC-20(以太坊意见征求 20)由 Fabian Vogelsteller 提出于 2015 年 11 月。这是一个能实现智能合约中代币的应用程序接口标准。
ERC-20 的功能示例包括:
- 将代币从一个帐户转到另一个帐户
- 获取帐户的当前代币余额
- 获取网络上可用代币的总供应量
- 批准一个帐户中一定的代币金额由第三方帐户使用
如果智能合约实施了下列方法和事件,它可以被称为 ERC-20 代币合约,一旦部署,将负责跟踪以太坊上创建的代币。
1 |
|
ERC20已经是一个相对成熟的标准,在开发的过程中,我们往往不再需要自己去实现(但在学习的过程中去亲手实现是非常重要的)。
OpenZeppelin框架
OpenZeppelin是一个开源的框架,提供可复用的智能合约模板来开发分布式的应用、协议和DAO(去中心化的自治组织),通过标准的、经过完整测试和整个社区检视的代码,减少产生漏洞的风险。
我们也将通过openzeppelin框架去实现我们的ERC20与ERC721合约
ERC721——非同质化货币标准
CryptoKitties是首款利用区块链开发的游戏,玩家们可以购买一只以太猫来进行喂养、繁殖与交易。与ERC20标准下的同质化代币不同,以太猫是一种不可再分的代币(我们可以把ERC20标准下的代币,类比的看作我们现实中使用的货币,一张十块钱可以兑换为两张五块使用,而以太猫就像是一只现实中的小猫,我们无法将再分了(不会有人想吃猫肉吧,太过分了!!!))。
CryptoKitties官网
CryptoKitties的成功,验证了不可细分的资产是如何产生的,以及如何再以太坊上交易。
由此诞生出了ERC721标准。 ERC721提出了如何开发非同质化货币(Non-Fungible Token,NFT)合约的标准。由于NFT不可分、唯一性的特性,使其与现实中的物品有共通的特点(你不可能找到两片完全相同的树叶;当我们讨论树叶时,树叶是一个不可再分的整体),区块链获得了全新的应用价值。某种程度上,我们可以将现实生活中的任何物品映射到区块链上。目前NFT的概念被数字藏品和电子游戏广泛应用。
ERC721接口实现
1 |
|
有趣的事: 一只以太猫的最高出售价格竟然能达到惊人的340万美元!!!人对于价值的衡量真是魔幻,如此昂贵的电子宠物你想要拥有吗?由于游戏基于以太坊区块链网络,游戏的每一步动作都需要消耗gas,请不要再说什么网游都太氪金了,区块链游戏才是真正的呼吸都需要付费。
与人相关的问题总是如此复杂,甚至充满矛盾。存在矛盾依然能够相对正常(其实什么是正常也很难去定义吧,可我找不到一个贴切的用语去形容了)的行事(机器遇上矛盾必然就宕掉了),这大概就是人与机器最本质的差别吧。所以我认为在人能够使机器认识矛盾之前(首先人类应当能够理解矛盾,可矛盾如何解释呢?我也无法想象,我们或许只能寄希望于世界出现超人),真正的人工智能大概永远也不会出现吧。(血肉苦弱,机械飞升,让人与机械互补?哈哈,太科幻了)
NFTMarket合约开发
开发工具:Remix IDE
ERC20代码实现:1
2
3
4
5
6
7
8
9
10// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract cUSDT is ERC20 {
constructor() ERC20("my USDT", "myUSDT") {
_mint(msg.sender, 1 * 10 ** 8 * 10 ** decimals());
}
}
ERC721代码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
constructor() ERC721("MyNFT", "myNFT") Ownable(msg.sender) {}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
// The following functions are overrides required by Solidity.
function _update(
address to,
uint256 tokenId,
address auth
) internal override(ERC721, ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}
function _increaseBalance(
address account,
uint128 value
) internal override(ERC721, ERC721Enumerable) {
super._increaseBalance(account, value);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function supportsInterface(
bytes4 interfaceId
)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
需求分析
- 购买NFT
- 出售NFT
- 查询在售的NFT(查询所有在售的NFT,查询我正在出售的NFT)
- 更改在售的NFT状态(更改价格或下架)
功能流程:
- 购买NFT:
查询指定NFT是否存在 -> 购买者将NFT出售价格的代币转移给出售地址 -> NFTMarket合约将NFT转给购买者地址 - 出售NFT:
售卖者调用safeTransferFrom()将NFT转给NFTMarket,并在data字段中指定价格,NFTMarket合约将自动上架NFT - 查询在售的NFT:
1)查询所有在售的NFT
直接返回在售NFT列表
2)查询调用者账户出售的NFT
遍历NFT列表,将属于调用者账户出售的NFT组成列表并返回 - 更改在售NFT状态:
1)更改价格
修改指定nft价格
2)将指定nft转回给卖家,并将其从在售列表中移除
NFTMarket代码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol";
import "@openzeppelin/contracts/interfaces/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC20.sol";
/**
* @title NFTMarket contract that allows atomic swaps of ERC20 and ERC721
*/
contract Market is IERC721Receiver {
//erc20与erc721的实体
IERC20 public erc20;
IERC721 public erc721;
bytes4 internal constant MAGIC_ON_ERC721_RECEIVED = 0x150b7a02;
//订单结构定义
struct Order {
address seller;
uint256 tokenId;
uint256 price;
}
//tokenID对订单的映射
mapping(uint256 => Order) public orderOfId;
//订单数组,用于给用户遍历访问在售的token
Order[] public orders;
//tokenID对token的订单指针的映射
mapping(uint256 => uint256) public idToOrderIndex;
//交易事件,完成交易时触发
event Deal(address buyer, address seller, uint256 tokenId, uint256 price);
//新建订单事件
event NewOrder(address seller, uint256 tokenId, uint256 price);
//取消订单事件
event CancelOrder(address seller, uint256 tokenId);
更改订单事件
event ChangePrice(
address seller,
uint256 tokenId,
uint256 previousPrice,
uint256 price
);
constructor(IERC20 _erc20, IERC721 _erc721) {
require(
address(_erc20) != address(0),
"Market: IERC20 contract address must be non-null"
);
require(
address(_erc721) != address(0),
"Market: IERC721 contract address must be non-null"
);
//此处直接将传入的地址封装为实体,方便后面的使用
erc20 = _erc20;
erc721 = _erc721;
}
function buy(uint256 _tokenId) external {
//查询指定token是否存在,否则回滚状态
require(isListed(_tokenId), "Market: Token ID is not listed");
address seller = orderOfId[_tokenId].seller;
address buyer = msg.sender;
uint256 price = orderOfId[_tokenId].price;
//必须在支付成功之后才能更改状态,否则可能遭到重入攻击
require(
erc20.transferFrom(buyer, seller, price),
"Market: ERC20 transfer not successful"
);
//支付成功后,合约将NFT转给买家
erc721.safeTransferFrom(address(this), buyer, _tokenId);
//移除订单
removeListing(_tokenId);
emit Deal(buyer, seller, _tokenId, price);
}
function cancelOrder(uint256 _tokenId) external {
require(isListed(_tokenId), "Market: Token ID is not listed");
address seller = orderOfId[_tokenId].seller;
//只能由卖家操作
require(seller == msg.sender, "Market: Sender is not seller");
//合约将NFT转给卖家
erc721.safeTransferFrom(address(this), seller, _tokenId);
removeListing(_tokenId);
emit CancelOrder(seller, _tokenId);
}
function changePrice(uint256 _tokenId, uint256 _price) external {
require(isListed(_tokenId), "Market: Token ID is not listed");
address seller = orderOfId[_tokenId].seller;
//只能由卖家操作
require(seller == msg.sender, "Market: Sender is not seller");
uint256 previousPrice = orderOfId[_tokenId].price;
orderOfId[_tokenId].price = _price;
Order storage order = orders[idToOrderIndex[_tokenId]];
order.price = _price;
emit ChangePrice(seller, _tokenId, previousPrice, _price);
}
//获取所有NFT信息
function getAllNFTs() public view returns (Order[] memory) {
return orders;
}
//获取所有调用账户下的NFT信息
function getMyNFTs() public view returns (Order[] memory) {
Order[] memory myOrders = new Order[](orders.length);
uint256 myOrdersCount = 0;
for (uint256 i = 0; i < orders.length; i++) {
if (orders[i].seller == msg.sender) {
myOrders[myOrdersCount] = orders[i];
myOrdersCount++;
}
}
Order[] memory myOrdersTrimmed = new Order[](myOrdersCount);
for (uint256 i = 0; i < myOrdersCount; i++) {
myOrdersTrimmed[i] = myOrders[i];
}
return myOrdersTrimmed;
}
function isListed(uint256 _tokenId) public view returns (bool) {
return orderOfId[_tokenId].seller != address(0);
}
function getOrderLength() public view returns (uint256) {
return orders.length;
}
/**
* @dev List a good using a ERC721 receiver hook
* @param _operator the caller of this function
* @param _seller the good seller
* @param _tokenId the good id to list
* @param _data contains the pricing data as the first 32 bytes
*/
//此方法是本合约中最重要,也最难以理解的。只要用户调用safeTransferFrom将token转移给NFTMarket,此方法将接收数据,并自动上架
/**
* 在ERC721中,当外部账户调用4个参数的safeTransferFrom时,
会自动检测_to的地址是否是一个合约地址(code size > 0),
若是合约地址,则会自动的调用合约地址的onERC721Received方法,并要求返回值为常量MAGIC_ON_ERC721_RECEIVED(若不是,则会抛出错误,并回滚状态).
在此次实现中,data代表价格,需要用bytes4编码的方式表示。合约拿到data后需要将其解析为uint256的形式
*/
function onERC721Received(
address _operator,
address _seller,
uint256 _tokenId,
bytes calldata _data
) public override returns (bytes4) {
require(_operator == _seller, "Market: Seller must be operator");
uint256 _price = toUint256(_data, 0);//将价格解析为uint256的形式
placeOrder(_seller, _tokenId, _price);
return MAGIC_ON_ERC721_RECEIVED;//此常量其实就是bytes4(keccak256("onERC721Received(address _operator,address _seller,uint256 _tokenId,bytes calldata _data)"))
}
// https://stackoverflow.com/questions/63252057/how-to-use-bytestouint-function-in-solidity-the-one-with-assembly
function toUint256(
bytes memory _bytes,
uint256 _start
) public pure returns (uint256) {
require(_start + 32 >= _start, "Market: toUint256_overflow");
require(_bytes.length >= _start + 32, "Market: toUint256_outOfBounds");
uint256 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _start))
}
return tempUint;
}
function placeOrder(//此方法用于新增订单
address _seller,
uint256 _tokenId,
uint256 _price
) internal {
require(_price > 0, "Market: Price must be greater than zero");
orderOfId[_tokenId].seller = _seller;
orderOfId[_tokenId].price = _price;
orderOfId[_tokenId].tokenId = _tokenId;
orders.push(orderOfId[_tokenId]);
idToOrderIndex[_tokenId] = orders.length - 1;
emit NewOrder(_seller, _tokenId, _price);
}
function removeListing(uint256 _tokenId) internal {//移除订单
delete orderOfId[_tokenId];
uint256 orderToRemoveIndex = idToOrderIndex[_tokenId];
uint256 lastOrderIndex = orders.length - 1;
if (lastOrderIndex != orderToRemoveIndex) {
Order memory lastOrder = orders[lastOrderIndex];
orders[orderToRemoveIndex] = lastOrder;
idToOrderIndex[lastOrder.tokenId] = orderToRemoveIndex;
}
orders.pop();
}
}
重入攻击
重入攻击是一种常见的智能合约漏洞,本质是合约内部调用的函数未能恰当地处理合约状态的更改。攻击者利用这个漏洞,将攻击代码插入到合约执行流程中,使得攻击者可以在合约还未完成之前再次调用某个函数(如: fallback, receive),从而让攻击者在合约中获得额外的资产或信息。
被重入攻击的重大事件
- 2016年,The DAO合约被重入攻击,被盗取3,600,000枚ETH。为了追回损失(尽管这违背了区块链精神,代码及法律),以太坊发起了一次版本更新,将区块链状态回退。有相当多的人拒绝此次更新,这导致了以太坊进行硬分叉,分叉成以太坊和以太坊经典(ETC带走了约30%的算力)。
- 2019年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 sETH。
- 2020年,借贷平台 http://Lendf.me 遭受重入攻击,被盗 $25,000,000。
- 2021年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。
- 2022年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。
对重入攻击的预防手段
目前主要通过两种方式修复和预防重入攻击,检查-生效-交互模式和重入锁
检查-生效-交互(checks-effect-interaction):
检查-生效-交互模式是指,在改变状态之前必须先判断条件是否满足。重入锁
在solidity合约开发中,重入锁是一种防止重入函数的修饰器(modifier)。它通过一个默认为0 的状态变量_status 来控制被修饰函数是否应该被顺利执行。被重入锁修饰的函数,在第一次调用时会检查_status是否为0,紧接着将_status的值设置为1,调用结束后再将_status改为0。这样,当攻击合约在调用结束前第二次的调用就会报错,重入攻击就失败了。