读《以太坊技术详解与实战》笔记(下)

读《以太坊技术详解与实战》笔记(下)

第六章

投票(代码与原版有所差异)

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

contract Ballot {
//投票者Voter的数据结构
struct Voter {
uint weight; //该投票者的投票所占权重
bool voted; //是否投过票
address delegate; //投票对应的提案编号
uint vote; //该投票者投票权的委托对象
}

struct Proposal {
bytes32 name; //提案名称
uint voteCount; //提案目前票数
}

address public chairperson;//投票主持人

mapping(address => Voter) public voters;//投票者地址与状态对应关系

Proposal[] public proposals;//提案的列表
//初始化合约时,给定一个提案的列表
constructor(bytes32[] memory proposalNames) {//构造函数
chairperson = msg.sender;
voters[chairperson].weight = 1;

for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
//只有投票主持者有给予Voter地址投票的权力
function giveRightToVote(address voter) public {
require(
msg.sender == chairperson,
"Only chairperson can give right to vote."
);//是否为主持人
require(
!voters[voter].voted,
"The voter already voted."
);//检测是否投票
require(voters[voter].weight == 0);//投票权重是否为零
voters[voter].weight = 1;
}

//投票者将自己的投票机会授权另一个地址
function delegate(address to) public {
Voter storage sender = voters[msg.sender];
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");

while (voters[to].delegate != address(0)) {
to = voters[to].delegate;

require(to != msg.sender, "Found loop in delegation.");
}
sender.voted = true;
sender.delegate = to;
Voter storage delegate_ = voters[to];
if (delegate_.voted) {

proposals[delegate_.vote].voteCount += sender.weight;
} else {

delegate_.weight += sender.weight;
}
}
//投票者根据提案列表编号(proposal)进行投票
function vote(uint proposal) public {
Voter storage sender = voters[msg.sender];//修改合约中的存储状态
//未加storagege修饰词,会导致sender是局部变量,调用结束会清除
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;

proposals[proposal].voteCount += sender.weight;
}
//根据proposals里的票数统计(voteCount)计算出票数最多的提案编号
function winningProposal() public view
returns (uint winningProposal_)
{
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
//获取票数最多的提案名称,其中调用winningProposal()函数
function winnerName() public view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}

1.revert()、assert()、require()三者的区别

1
2
if(msg.sender != owner) { throw; } <^0.4.13
之后throw关键字被弃用
  • 函数 assertrequire 可用于检查条件并在条件不满足时抛出异常。
  • assert 函数只能用于测试内部错误,并检查非变量。 require 函数用于确认条件有效性,并提供一个字符串消息
  • revert 函数可以用来标记错误并恢复当前的调用。调用包中有关错误的详细信息返回给调用者

image-20210320220120589

Solidity 对一个 require 式的异常执行回退操作(指令 0xfd )并执行一个无效操作(指令 0xfe )来引发 assert 式异常。想要保留交易原子性,最安全的做法是回退所有更改并使整个交易(或至少是调用)不产生效果

2.memory与storage区别

storage memory
储存的变量 函数外部声明的变量,即状态变量 函数内部声明的变量,即局部变量
存储的位置 区块链上,永久存在 内存中,运行完之后销毁
运行的位置 区块链网络上 单个节点
传递属性 指针传递 值传递

两者使用成本与calldata

  • storage

    • 存储中的数据是永久存在的。存储是一个key/value库

    • 存储中的数据写入区块链,因此会修改状态,这也是存储使用成本高的原因。

    • 占用一个256位的槽需要消耗20000 gas

    • 修改一个已经使用的存储槽的值,需要消耗5000 gas

    • 当清零一个存储槽时,会返还一定数量的gas

    • 存储按256位的槽位分配,即使没有完全使用一个槽位,也需要支付其开销

  • memory

    • 内存是一个字节数组,槽大小位256位(32字节)
  • 数据仅在函数执行期间存在,执行完毕后就被销毁

    • 读或写一个内存槽都会消耗3gas
    • 为了避免矿工的工作量过大,22个操作之后的单操作成本会上涨
  • calldata/调用数据

    • 调用数据是不可修改、非持久化的区域,用来保存函数参数,其行为类似于内存
    • 外部函数的参数必须使用calldata,但是也可用于其他变量
    • 调用数据避免了数据拷贝,并确保数据不被修改
    • 函数也可以返回使用calldata声明的数组和结果,但是不可能分配这些类型

    但是本合约中的多数函数需要使用storage来防止一些危险的因数,防止投票者的一些状态丢失

3.函数权限关键字与修饰关键字

函数关键字

  • public:只有 public 类型的函数才可以供外部访问,当一个状态变量的权限为 public 类型时,它就会自动生成一个可供外部调用的 get 函数。当函数声明时,它默认为是 public 类型,而状态变量声明时,默认为 internal 类型。
  • private:只能在当前类中进行访问,子类无法继承,也无法调用或访问。
  • internal:子类继承父类,子类可以访问父类的 internal 函数,同时,使用 using for 关键字后,本类可以使用被调用类的 internal 函数。
  • external:被声明的函数只能在合约外部调用。

修饰关键字

  • modifier:被 modifier 关键字声明的关键字所修饰的函数只能在满足 modifier 关键字声明的关键字的要求后才会被执行。
  • constant:被声明为 constant 的状态变量只能使用那些在编译时有确定值的表达式来给它们赋值。任何通过访问 内存、链数据(例如 now,this.balance 或 block.number)或执行数据(msg.gas)或对外部合约的调用来给它们赋值都是不允许的。不是所有类型的状态变量都支持用 constant 来修饰,当前支持的仅有值类型和字符串。
  • view:被该关键字修饰的状态变量只能读取其值,不能对该状态变量的值进行修改。
  • pure:被该关键字修饰的状态变量既不能读取变量,也不能修改该变量。

4.变长字节数组

  • 一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]
  • 数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反 ,T[0]代表最后一个元素
  • .lenth表示当前数组的长度。储存在storage的动态数组可以通过修改.lenth修改数组大小,memory的数组长度是固定的
  • 变长的 存储(storage) 数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度

5.映射键值对

映射或字典类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)。键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

简单来说,映射就是一个哈希表,每一个key与一个value互相对应,通过知道键值可以快速地定位到value

但是我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找值时使用。

6.tips

solidity 调用栈最深为1024,尽量用循环

拍卖

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

contract SimpleAuction {
// 最终受益者
address public beneficiary;
// 时间是unix的绝对时间戳(自1970-01-01以来的秒数)以秒为单位
uint public auctionEnd;

// 拍卖的当前状态
address public highestBidder;//最高出价者
uint public highestBid;//最高出价

//可以取回的之前的出价
mapping(address => uint) pendingReturns;

// 拍卖结束后设为 true,将禁止所有的变更
bool ended;

// 变更触发的事件
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);

// 以下是所谓的 natspec 注释,可以通过三个斜杠来识别。
// 当用户被要求确认交易时将显示。

/// 以受益者地址 `_beneficiary` 的名义,
/// 创建一个简单的拍卖,拍卖时间为 `_biddingTime` 秒。
constructor(
uint _biddingTime,
address _beneficiary
) public {
beneficiary = _beneficiary;
auctionEnd = now + _biddingTime;
}

/// 对拍卖进行出价,具体的出价随交易一起发送。
/// 如果没有在拍卖中胜出,则返还出价。
function bid() public payable {
// 参数不是必要的。因为所有的信息已经包含在了交易中。
// 对于能接收以太币的函数,关键字 payable 是必须的。

// 如果拍卖已结束,撤销函数的调用。
require(
now <= auctionEnd,
"Auction already ended."
);

// 如果出价不够高,返还你的钱
require(
msg.value > highestBid,
"There already is a higher bid."
);

if (highestBid != 0) {
// 返还出价时,简单地直接调用 highestBidder.send(highestBid) 函数,
// 是有安全风险的,因为它有可能执行一个非信任合约。
// 更为安全的做法是让接收方自己提取金钱。
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}

/// 取回出价(当该出价已被超越)
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里很重要,首先要设零值。
// 因为,作为接收调用的一部分,
// 接收者可以在 `send` 返回之前,重新调用该函数。
pendingReturns[msg.sender] = 0;

if (!msg.sender.send(amount)) {
// 这里不需抛出异常,只需重置未付款
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}

/// 结束拍卖,并把最高的出价发送给受益人
function auctionEnd() public {
// 对于可与其他合约交互的函数(意味着它会调用其他函数或发送以太币),
// 一个好的指导方针是将其结构分为三个阶段:
// 1. 检查条件
// 2. 执行动作 (可能会改变条件)
// 3. 与其他合约交互
// 如果这些阶段相混合,其他的合约可能会回调当前合约并修改状态,
// 或者导致某些效果(比如支付以太币)多次生效。
// 如果合约内调用的函数包含了与外部合约的交互,
// 则它也会被认为是与外部合约有交互的。

// 1. 条件
require(now >= auctionEnd, "Auction not yet ended.");
require(!ended, "auctionEnd has already been called.");

// 2. 生效
ended = true;
emit AuctionEnded(highestBidder, highestBid);

// 3. 交互
beneficiary.transfer(highestBid);
}
}

1.事件

事件是能方便地调用以太坊虚拟机日志功能的接口。

  • 定义事件event EventName(address bidder, uint amount);
  • 触发事件emit EventName(msg.sender, msg.value);
  • 本合约使用日志功能记录不同地址的不同出价,记录后我们可以搜索事件
  • 其中有一个参数修饰是 indexed ,用来表示这个参数用作索引,查询日志时就可以根据这个索引进行过滤
  • 事件搜索

2.函数返回值

  • 使用返回变量名

    1
    2
    3
    4
    5
    6
    function arithmetic(uint _a, uint _b) public pure
    returns (uint o_sum, uint o_product)
    {
    o_sum = _a + _b;
    o_product = _a * _b;
    }
  • 直接在return语句中提供返回值

    1
    2
    3
    4
    5
    function arithmetic(uint _a, uint _b) public pure
    returns (uint o_sum, uint o_product)
    {
    return (_a + _b, _a * _b);
    }
  • Getter 函数

    所有定义为public的状态变量都有getter函数,由编译器自动创建。该函数与变量具有相同的名称,并且具有外部可见性

3.payable

如果在函数中涉及到以太币的转移,需要使用到payable关键词。意味着可以在调用这笔函数的消息中附带以太币。

4.重入攻击-Re-Entrancy

以太坊上的智能合约彼此之间可以相互调用。假设在一个合约A执行过程中发生了一次外部的合约B调用,并且合约B是由黑客所控制的,合约B的调用过程中可以重新进入合约A的调用。如果合约A在执行外部合约调用之前并未完成自己的内部状态更新,则有可能会被合约B利用从而盗取资产。

先分析DAO攻击中的问题代码

1
2
3
4
5
6
function withDraw(){
uint amount = userBalannce[msg.sender];
if(amount > 0){
msg.sender.call.value(amount)();
userBalannce[msg.sender] = 0;
}

该函数的功能是实现提款操作。逻辑顺序是,先执行退款操作,再将账户的余额进行相应扣除。

首先由于Gas的限制,不需要担心死循环。但是以太币转账会触发代码执行,如果接收方是智能合约,那么就能在接受的过程中再次调用withdraw()函数。

  • 回退函数(fallback function)

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。

如果构造一个 fallback 函数,函数里面也调用对方的 withdraw 函数的话,那将会产生一个循环调用转账功能,存在漏洞的合约会不断向攻击者合约转账,终止循环结束(以太坊 gas 有上限)

1
2
3
4
function() payable{//定义payable修饰使得fallback函数具备转账功能
test++;//记录尝试次数
Victim(msg.sender).withDraw();//调用目标合约的转账函数
}

详细复现

本合约中的withdraw函数编写符合退款逻辑,避免了重入攻击

1
2
3
4
5
6
7
8
9
10
11
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)) {
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}

把写操作放在外部函数调用之前:先扣除在进行转账

修复建议
  • 变成“先记录,后转账”的模式-(Checks-effects-interactions)
  • 采用transfer()函数进行转账,或采用to.call.gas(2300).value(amount)(); 函数对gas进行限制。
  • 采用锁机制
1
2
3
4
5
6
7
8
9
10
11
 modifier onlyUnlocked{
require(unlocked,"contract is already locked");
unlocked = false;
_;
unlocked = true;
}
function withdraw() onlyUnlocked{
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}
  • modifer

  • ```java
    //定义修饰器

    modifier modifierfun(uint value){
        require(value >= 10); 
        _;  //代表修饰器所修饰函数中的代码。
    }
    // 修饰器修饰函数。 (先执行修饰器中的代码,再执行函数中的代码)
    function setValue(uint num) modifierfun(num){
        a = num;
    }
    
    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

    ### 盲拍

    ```java
    pragma solidity >=0.5.0 <0.7.0;

    contract BlindAuction {
    struct Bid {
    bytes32 blindedBid;
    uint deposit;
    }

    address payable public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // 可以取回的之前的出价
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    /// 使用 modifier 可以更便捷的校验函数的入参。
    /// onlyBefore 会被用于后面的 bid 函数:
    /// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。
    modifier onlyBefore(uint _time) { require(now < _time); _; }
    modifier onlyAfter(uint _time) { require(now > _time); _; }

    constructor(
    uint _biddingTime,
    uint _revealTime,
    address payable _beneficiary
    ) public {
    beneficiary = _beneficiary;
    biddingEnd = now + _biddingTime;
    revealEnd = biddingEnd + _revealTime;
    }

    /// 可以通过 _blindedBid = keccak256(value, fake, secret)
    /// 设置一个秘密竞拍。
    /// 只有在出价披露阶段被正确披露,已发送的以太币才会被退还。
    /// 如果与出价一起发送的以太币至少为 “value” 且 “fake” 不为真,则出价有效。
    /// 将 “fake” 设置为 true ,然后发送满足订金金额但又不与出价相同的金额是隐藏实际出价的方法。
    /// 同一个地址可以放置多个出价。
    function bid(bytes32 _blindedBid)
    public
    payable
    onlyBefore(biddingEnd)
    {
    bids[msg.sender].push(Bid({
    blindedBid: _blindedBid,
    deposit: msg.value
    }));
    }

    /// 披露你的秘密竞拍出价。
    /// 对于所有正确披露的无效出价以及除最高出价以外的所有出价,你都将获得退款。
    function reveal(
    uint[] _values,
    bool[] _fake,
    bytes32[] _secret
    )
    public
    onlyAfter(biddingEnd)
    onlyBefore(revealEnd)
    {
    uint length = bids[msg.sender].length;
    require(_values.length == length);
    require(_fake.length == length);
    require(_secret.length == length);

    uint refund;
    for (uint i = 0; i < length; i++) {
    Bid storage bid = bids[msg.sender][i];
    (uint value, bool fake, bytes32 secret) =
    (_values[i], _fake[i], _secret[i]);
    if (bid.blindedBid != keccak256(value, fake, secret)) {
    // 出价未能正确披露
    // 不返还订金
    continue;
    }
    refund += bid.deposit;
    if (!fake && bid.deposit >= value) {
    if (placeBid(msg.sender, value))
    refund -= value;
    }
    // 使发送者不可能再次认领同一笔订金
    bid.blindedBid = bytes32(0);
    }
    msg.sender.transfer(refund);
    }

    // 这是一个 "internal" 函数, 意味着它只能在本合约(或继承合约)内被调用
    function placeBid(address bidder, uint value) internal
    returns (bool success)
    {
    if (value <= highestBid) {
    return false;
    }
    if (highestBidder != address(0)) {
    // 返还之前的最高出价
    pendingReturns[highestBidder] += highestBid;
    }
    highestBid = value;
    highestBidder = bidder;
    return true;
    }

    /// 取回出价(当该出价已被超越)
    function withdraw() public {
    uint amount = pendingReturns[msg.sender];
    if (amount > 0) {
    // 这里很重要,首先要设零值。
    // 因为,作为接收调用的一部分,
    // 接收者可以在 `transfer` 返回之前重新调用该函数。(可查看上面关于‘条件 -> 影响 -> 交互’的标注)
    pendingReturns[msg.sender] = 0;

    msg.sender.transfer(amount);
    }
    }

    /// 结束拍卖,并把最高的出价发送给受益人
    function auctionEnd()
    public
    onlyAfter(revealEnd)
    {
    require(!ended);
    emit AuctionEnded(highestBidder, highestBid);
    ended = true;
    beneficiary.transfer(highestBid);
    }
    }

代码分析

由于区块链上面的交易都是公开透明的,前面的拍卖合约可以通过查询每一笔交易轻易得知目前的最高价。要实现盲拍确实困难。上述合约通过引入伪出价,使得真实的出价被隐藏在众多交易中,并且通过keccak256校验防止竞拍者修改自己的出价记录。实现一定程度上的盲拍。

1.转账函数

\
.transfer()

address.transfer()方法相当于 require(address.send()), 使用transfer方法也需要注意两点,第一点,跟send方法一样,transfer也只提供了2300 Energy。 第二点,不同于send方法,transfer方法提供了一种更安全的机制,失败的时候会抛出异常,所有已经完成的操作都会回滚。

\
.send()

使用address.send()方法需要注意两点,第一点,如上所述,它只提供了2300 Energy。 第二点,对于执行失败的send方法,send函数仅仅返回false,不会抛出任何异常。因此调用send方法的时候需要配合require使用,否则可能会出现交易上链,用户支付了fee,但是所有的状态改动没有生效。

\
.call.value()

相对于前两种方法,address.call.value(amount)( )使用起来更加灵活,适用的范围也更加广泛。因为这种方式提供了指定energy数量的接口,使用的时候不再受2300 Energy的限制,可以允许接收函数执行更复杂的操作。使用这种方法也需要注意两点问题。第一个,同send()一样,执行失败的时候此函数不会抛出异常,只返回false,需要用户手动处理返回结果,使用的时候建议配合require一起使用。第二点,如果不显示指定Energy数量,默认的Energy数量是用户所有可用的Energy。Energy数量可以通过修饰器 .gas(energyLimit)来设定。

2.构造函数

solidity 的内置变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。在这里,以秒为单位,因此 _biddingTime, _revealTime 都是从当前开始经过XXX秒后

1
2
3
4
5
6
7
8
9
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}

3. constructor

构造函数是使用 constructor 关键字声明的一个可选函数, 它在创建合约时执行, 可以在其中运行合约初始化代码。如果没有构造函数, 合约将假定采用默认构造函数, 它等效于 constructor() {}

在执行构造函数代码之前, 如果状态变量可以初始化为指定值; 如果不初始化, 则为零。

在 0.7.0 版本之前, 你需要通过 internalpublic 指定构造函数的可见性。

状态机

此处代码未采用书上的例子,转自一篇文章

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
uint public value;//定义商品的价值
address payable public seller;//定义一个payable类型的卖方
address payable public buyer;//定义一个payable类型的买方

enum State { Created, Locked, Release, Inactive }
//定义一个枚举类型包含四个状态变量
State public state;//定义枚举变量

modifier condition(bool _condition){
require(_condition);
_;
}//判断是否bool型变量condition是否为true

modifier onlyBuyer(){
require(
msg.sender == buyer,
"Only buyer can call this."
);
_;
}//判断调用地址是否为买方地址

modifier onlySeller(){
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}//判断调用地址是否为卖方地址

modifier inState(State _state) {
require(
state == _state,
"Invalid state."
);
_;
}//判断是否在对应的交易阶段调用对应的函数
//以下是四个阶段通知的声明,方便后续可以通过log日志获取详细信息
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
event SellerRefunded();
//撤回函数
function abort()
public
onlySeller//调用地址为卖方地址才可运行该函数
inState(State.Created)//当状态处于合约产生状态时可调用
{
emit Aborted();//调用log日志显示
state = State.Inactive;//状态转为不工作状态
seller.transfer(address(this).balance);
}
//买家确认购买函数
function confirmPurchase()
public
inState(State.Created)//处于合约产生阶段可调用
condition(msg.value == ( 2 * value))//购买人的输入value必须持有2*value
payable
{
emit PurchaseConfirmed();
buyer = msg.sender;//对买方地址进行赋值
state = State.Locked;//状态切换为锁定订单
}
//买家确认收获函数
function confirmReceived()
public
onlyBuyer//限制买家进行调用
inState(State.Locked)//处于合约锁定阶段可以调用
{
emit ItemReceived();
state = State.Release;//转换为合约发布阶段
buyer.transfer(value);//买方退回value
}
//卖方收回资金函数
function refundSeller()
public
onlySeller//只有卖方能够调用
inState(State.Release)//状态是合约锁定解除可调用
{
emit SellerRefunded();
state = State.Inactive;//状态切换为合约不工作
seller.transfer(3 * value);//返回3*value给卖家
}

debug调试

对state的初始值有所疑问,所以进行了调试

1
2
enum State { Created, Locked, Release, Inactive }
State public state;

采取remix在线部署后调试

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;
contract tAest {
enum State { Created, Locked, Release, Inactive }
State public state;
function a() {
state=State.Locked;
}
}

image-20210321220444930

点击Debug进入调试,

image-20210321220545250

简单说明

image-20210321220715904

执行到最后也不会执行a函数,这是因为我们没有调用

回到部署的界面,点击一下a,会出现新的debug按钮,再点进去

image-20210321220931862

继续单步执行到一个pop指令后,state 的值会改变为Locked

image-20210321221038692

如果在外面直接改变state会报错。这是因为solidity语法在变量声明时只能进行一次赋值或者初始化为默认值

image-20210321221346793

权限控制

本块内容其实在上面的合约中有所体现,采取白名单策略,同时编写设置白名单的函数,可以实现对用户的权限控制。

第七章

ERC20

它诞生于2015年,到2017年9月被正式标准化。协议规定了具有可互换性(fungible)代币的一组基本接口,包括代币符号、发行量、转账、授权等

image-20210322185706847

接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
contract ERC20 {
function name() constant returns (string name)
function symbol() constant returns (string symbol)
function decimals() constant returns (uint8 decimals)
function totalSupply() constant returns (uint totalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}
  • name:代币名字
  • symbol:代币简称
  • decimals:token使用小数点,
  • totalSupply:token供应总量
  • balanceOf:某个地址(账户)的余额
  • transfer:从代币合约的调用者地址上转移_value的数量token到的地址_to,并且必须触发Transfer事件
  • transferFrom:从地址_from发送数量为_value的token到地址_to,必须触发Transfer事件。transferFrom方法用于允许合同代理某人转移token。条件是from账户必须经过了approve。
  • approve:允许_spender多次取回您的帐户,最高达_value金额。 如果再次调用此函数,它将以_value覆盖当前的余量。
  • allowance:返回_spender仍然被允许从_owner提取的金额。
  • Transfer:当成功转移token时,一定要触发Transfer事件
  • Approval:当调用approval函数成功时,一定要触发Approval事件

理解后三个函数:如果账户A有1000个ETH,想允许B账户随意调用他的100个ETH

1
2
3
1. A账户按照以下形式调用approve函数approve(B,100)  
2. B账户想用这100个ETH中的10个ETH给C账户,调用transferFrom(A, C, 10)
3. 调用allowance(A, B)可以查看B账户还能够调用A账户多少个token

部署自己的ERC20代币

ERC721

ERC 721 合约标准规定了一种不可替代的代币 Non-fungible Token, NFT )的合约接 此类代币的最小单位为个,即在 ERC 20 标准中对应小数点位的 decimal 值为零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract ERC721 { 
// Required method
function totalSupply() constant returns (uint256 totalSupply);
function balanceOf(address owner) constant returns (uint256 balance);
function owner0f(uint256 tokenid) constant returns (address owner);
function approve(address _to, uint256 _tokenid);
function takeOwnership(uint256 tokenid);
function transfer(address to, uint256 tokenid);
// Optional method
function name() constant returns (string name);
function symbol() constant returns (string symbol);
function tokenOfOwnerByindex(address owner, uint256 index) constant returns
(uint tokenid);
function tokenMetadata(uint256 tokenid) constant returns (string infoUrl);
//Events
event Transfer(address indexed _from, address indexed _to, uint256 _tokenid);
event Approval(address indexed owner, address indexed _approved, uint256 tokenid);
}

可以看出ERC721继承了ERC20标准的一些基本功能接口。同时加入一些新的功能函数

  • owner0f:根据代币ID查询该代币持有者
  • tokenOfOwnerByindex:根据持有者及其索引查询所持有的代币ID
  • takeOwnership:与ERC20中的transferFrom一样
  • tokenMetadata:用于查看代币的元数据

ERC721代表作CryptoKitties以太坊养猫

第八章

本章主要是工具介绍和以太坊浏览器的使用

第九章

以太坊性能优化

第十章

以太坊隐私保护