Task4 合约编写实战实例


合约编写实战实例

1、简单代币合约

pragma solidity > 0.4.22;

contract Coin{
    address public minter;
    
    mapping(address=>uint) balances;
    event Sent(address from, address to, uint amount);
    // 合约初始化,同样是为了之后的判断,当前交易者是否是创建合约的人
    // msg.sender == minter
    constructor(){
        minter = msg.sender;
    }
    
    function mint(address receiver, uint amount) public{
        // require msg.sender == minter
        require(msg.sender == minter);
        balances[receiver] += amount;
    }
    
    function send(address receiver, uint amount) public{
        //require balance > amount
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
    
}

1.1 创建一个基础合约

contract Coin{
    address public minter;
    mapping(address=>uint) balances;
}
  • 定义了一个 address 作为key, uint 做为 value 的 hashTable balances;这个类型将地址映射到无符号整型。

1.2 添加一个构造函数

constructor(){
	minter = msg.sender;
}
  • minter = msg.sender; 代表创建这个合约的账户地址,被赋值给变量minter.

1.3 添加一个挖矿合约

function mint(address receiver, uint amount) public{
    // require msg.sender == minter
    require(msg.sender == minter);
    balances[receiver] += amount;
}
  • mint function 关键点在于,如果调用这个方法的账户不是 minter (注意这里的 msg.sender 和 构造函数的 msg.sender 并一定相同,取决于是谁调用这个方法。例如创建这个合约的账户是A,那么minter=A,如果有一个新的账户 B 去调用了这个方法,此时 msg.sender=B,不满足msg.sender == minter这个条件),也就是创建合约的账户的话,这个 mint()将不能被执行。

1.4 添加一个转账合约

function send(address receiver, uint amount) public{
    //require balance > amount
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    balances[receiver] += amount;
}
  • 先判断 msg.sender 余额是否充足,然后 msg.sender 减少一定代币,接受者 receiver 增加一定代币

1.5 定义一个事件

event Sent(address from, address to, uint amount);
function send(address receiver, uint amount) public{
    //require balance > amount
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    balances[receiver] += amount;
    emit Sent(msg.sender, receiver, amount);
}
  • event Sent(address from, address to, uint amount);声明了一个所谓的事件,它在send函数最后一行被发出。用户界面可以监听区块链上正在发送的事件,且不会花费太多成本,一旦它被发出,监听该事件的listener都将收到通知,而所有的事件都包含了from,to和amount三个参数,可方便追踪事务。

2、水龙头合约

pragma solidity ^0.7.0;

contract faucet{
    function withdraw(uint amount) public{
        require(amount <= 1e18);
        msg.sender.transfer(amount);
    }
    
    receive () external payable{}
}
  • msg.sender.transfer (amount) 就是实际的提款操作了。msg 是 Solidity 中内置的对象,所有合约都可以访问,它代表触发此合约的交易。也就是说当我们调用 withdraw 函数的时候实际上触发了一笔交易,并用 msg 来表示它。sender 是交易 msg 的属性,表示了交易的发件人地址。函数 transfer 是一个内置函数,它接收一个参数作为以太币的数量,并将该数量的以太币从合约账户发送到调用合约的用户的地址中。

  • 最后一行是一个特殊的函数 receive ,这是所谓的 fallbackdefault 函数。当合约中的其他函数无法处理发送到合约中的交易信息时,就会执行该函数。在这里,我们将该函数声明为 externalpayableexternal 意味着该函数可以接收来自外部账户的调用,payable 意味着该函数可以接收来自外部账户发送的以太币。

3、投票合约的实现

该智能合约实现了一个自动化的、透明的投票应用。投票发起人可以发起投票,将投票权赋予投票人;投票人可以自己投票,或将自己的票委托给其他投票人;任何人都可以公开查询投票的结果。

实现上述功能的合约代码如下所示,并不复杂,语法跟 JavaScript 十分类似。

pragma solidity >=0.4.22 <0.7.0;

/// @title 委托投票
contract Ballot {
    // 这里声明了一个新的复合类型用于稍后的变量
    // 它用来表示一个选民
    struct Voter {
        uint weight; // 计票的权重
        bool voted;  // 若为真,代表该人已投票
        address delegate; // 被委托人
        uint vote;   // 投票提案的索引
    }

    // 提案的类型
    struct Proposal {
        bytes32 name;   // 简称(最长32个字节)
        uint voteCount; // 得票数
    }

    address public chairperson;

    // 这声明了一个状态变量,为每个可能的地址存储一个 `Voter`。
    mapping(address => Voter) public voters;

    // 一个 `Proposal` 结构类型的动态数组
    Proposal[] public proposals;

    /// 为 `proposalNames` 中的每个提案,创建一个新的(投票)表决
    constructor(bytes32[] memory proposalNames) public {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;
        //对于提供的每个提案名称,
        //创建一个新的 Proposal 对象并把它添加到数组的末尾。
        for (uint i = 0; i < proposalNames.length; i++) {
            // `Proposal({...})` 创建一个临时 Proposal 对象,
            // `proposals.push(...)` 将其添加到 `proposals` 的末尾
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // 授权 `voter` 对这个(投票)表决进行投票
    // 只有 `chairperson` 可以调用该函数。
    function giveRightToVote(address voter) public {
        // 若 `require` 的第一个参数的计算结果为 `false`,
        // 则终止执行,撤销所有对状态和以太币余额的改动。
        // 在旧版的 EVM 中这曾经会消耗所有 gas,但现在不会了。
        // 使用 require 来检查函数是否被正确地调用,是一个好习惯。
        // 你也可以在 require 的第二个参数中提供一个对错误情况的解释。
        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;
    }

    /// 把你的投票委托到投票者 `to`。
    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.");

        // 委托是可以传递的,只要被委托者 `to` 也设置了委托。
        // 一般来说,这种循环委托是危险的。因为,如果传递的链条太长,
        // 则可能需消耗的gas要多于区块中剩余的(大于区块设置的gasLimit),
        // 这种情况下,委托不会被执行。
        // 而在另一些情况下,如果形成闭环,则会让合约完全卡住。
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            // 不允许闭环委托
            require(to != msg.sender, "Found loop in delegation.");
        }

        // `sender` 是一个引用, 相当于对 `voters[msg.sender].voted` 进行修改
        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;
        }
    }

    /// 把你的票(包括委托给你的票),
    /// 投给提案 `proposals[proposal].name`.
    function vote(uint proposal) public {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // 如果 `proposal` 超过了数组的范围,则会自动抛出异常,并恢复所有的改动
        proposals[proposal].voteCount += sender.weight;
    }

    /// @dev 结合之前所有的投票,计算出最终胜出的提案
    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;
    }
}

代码解析

指定版本

在第一行,pragma 关键字指定了和该合约兼容的编译器版本。

pragma solidity ^0.4.11;

该合约指定,不兼容比 0.4.11 更旧的编译器版本,且 ^ 符号表示也不兼容从 0.5.0 起的新编译器版本。即兼容版本范围是 0.4.11 <= version < 0.5.0。该语法与 npm 的版本描述语法一致。

结构体类型

Solidity 中的合约(contract)类似面向对象编程语言中的类。每个合约可以包含状态变量、函数、事件、结构体类型和枚举类型等。一个合约也可以继承另一个合约。

在本例命名为 Ballot 的合约中,声明了 2 个结构体类型:VoterProposal

  • struct Voter:投票人,其属性包括 uint weight(该投票人的权重)、bool voted(是否已投票)、address delegate(如果该投票人将投票委托给他人,则记录受委托人的账户地址)和 uint vote(投票做出的选择,即相应提案的索引号)。
  • struct Proposal:提案,其属性包括 bytes32 name(名称)和 uint voteCount(已获得的票数)。

需要注意,address 类型记录了一个以太坊账户的地址。address 可看作一个数值类型,但也包括一些与以太币相关的方法,如查询余额 <address>.balance、向该地址转账 <address>.transfer(uint256 amount) 等。

状态变量

合约中的状态变量会长期保存在区块链中。通过调用合约中的函数,这些状态变量可以被读取和改写。

本例中定义了 3 个状态变量:chairpersonvotersproposals

  • address public chairperson:投票发起人,类型为 address
  • mapping(address => Voter) public voters:所有投票人,类型为 addressVoter 的映射。
  • Proposal[] public proposals:所有提案,类型为动态大小的 Proposal 数组。

3 个状态变量都使用了 public 关键字,使得变量可以被外部访问(即通过消息调用)。事实上,编译器会自动为 public 的变量创建同名的 getter 函数,供外部直接读取。

状态变量还可设置为 internalprivateinternal 的状态变量只能被该合约和继承该合约的子合约访问,private 的状态变量只能被该合约访问。状态变量默认为 internal

将上述关键状态信息设置为 public 能够增加投票的公平性和透明性。

函数

合约中的函数用于处理业务逻辑。函数的可见性默认为 public,即可以从内部或外部调用,是合约的对外接口。函数可见性也可设置为 externalinternalprivate

本例实现了 6 个 public 函数,可看作 6 个对外接口,功能分别如下。

创建投票

函数 function Ballot(bytes32[] proposalNames) 用于创建一个新的投票。

所有提案的名称通过参数 bytes32[] proposalNames 传入,逐个记录到状态变量 proposals 中。同时用 msg.sender 获取当前调用消息的发送者的地址,记录为投票发起人 chairperson,该发起人投票权重设为 1。

赋予投票权

函数 function giveRightToVote(address voter) 实现给投票人赋予投票权。

该函数给 address voter 赋予投票权,即将 voter 的投票权重设为 1,存入 voters 状态变量。

这个函数只有投票发起人 chairperson 可以调用。这里用到了 require((msg.sender == chairperson) && !voters[voter].voted) 函数。如果 require 中表达式结果为 false,这次调用会中止,且回滚所有状态和以太币余额的改变到调用前。但已消耗的 Gas 不会返还。

委托投票权

函数 function delegate(address to) 把投票委托给其他投票人。

其中,用 voters[msg.sender] 获取委托人,即此次调用的发起人。用 require 确保发起人没有投过票,且不是委托给自己。由于被委托人也可能已将投票委托出去,所以接下来,用 while 循环查找最终的投票代表。找到后,如果投票代表已投票,则将委托人的权重加到所投的提案上;如果投票代表还未投票,则将委托人的权重加到代表的权重上。

该函数使用了 while 循环,这里合约编写者需要十分谨慎,防止调用者消耗过多 Gas,甚至出现死循环。

进行投票

函数 function vote(uint proposal) 实现投票过程。

其中,用 voters[msg.sender] 获取投票人,即此次调用的发起人。接下来检查是否是重复投票,如果不是,进行投票后相关状态变量的更新。

查询获胜提案

函数 function winningProposal() constant returns (uint winningProposal) 将返回获胜提案的索引号。

这里,returns (uint winningProposal) 指定了函数的返回值类型,constant 表示该函数不会改变合约状态变量的值。

函数通过遍历所有提案进行记票,得到获胜提案。

查询获胜者名称

函数 function winnerName() constant returns (bytes32 winnerName) 实现返回获胜者的名称。

这里采用内部调用 winningProposal() 函数的方式获得获胜提案。如果需要采用外部调用,则需要写为 this.winningProposal()

参考资料


文章作者: Terence Cai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Terence Cai !
  目录