《深入掌握以太坊核心技术》--10-简单代币合约
简单的Coin子货币案例
来源:Solidity官网文档
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
// "public" 关键字使变量可以从其他合约访问
address public minter;
// 映射(mapping)用于存储地址和余额的映射关系
//类型指定address到uint,地址指定用户,不用列表来存储用户
mapping(address => uint) public balances;
// 事件允许客户端对您声明的特定合约更改做出反应
event Sent(address from, address to, uint amount);
// 构造函数只在合约创建时运行,初始化合约
constructor() {
// 将合约创建者设置为铸币者,告诉谁是铸币者
minter = msg.sender;
}
// 向地址发送一定数量的新创建的代币
// 只能由合约创建者调用
function mint(address receiver, uint amount) public {
// 确保调用者是铸币者
require(msg.sender == minter);
// 增加接收者的余额
balances[receiver] += amount;
}
// 错误允许您提供有关操作失败原因的信息。它们会返回给函数的调用者。
error InsufficientBalance(uint requested, uint available);
// 从任何调用者向地址发送一定数量的现有代币
function send(address receiver, uint amount) public {
// 如果请求的数量大于调用者的余额,则抛出错误
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount, //表示用户请求提取的金额
available: balances[msg.sender] //表示用户账户中实际可用的余额
});
//加减运算需检验溢出
// 减少调用者的余额
balances[msg.sender] -= amount;
// 增加接收者的余额
balances[receiver] += amount;
// 触发 Sent 事件,可跟踪,监听Sent内容
emit Sent(msg.sender, receiver, amount);
}
}
ERC20 代币标准
ERC-20 是以太坊上智能合约的一个通用标准,用于代表可互换的代币。ERC-20 标准定义了一组规则和函数,使得代币合约在以太坊网络上具有一致性和互操作性。这意味着符合 ERC-20 标准的代币可以在相同的钱包、交易所和其他以太坊智能合约中使用,而不需要针对每种代币编写特定的代码,即任何一个代币都完全等同于任何其他代币;没有其他意义的不同。(同质化代币)
方法
来源:同质化代币
// 返回代币的名称,以太坊中的Ethereum,BTC中的Bitcoin
function name() public view returns (string)
// 返回代币的符号,以太坊中的ETH,比特币中的BTC
function symbol() public view returns (string)
// 返回代币的小数位数,即这个代币的金额不可再拆分的最小单位,例如以太坊的wei,一般是18,也就是精确度。
function decimals() public view returns (uint8)
// 返回代币的总供应量
function totalSupply() public view returns (uint256)
// 查询指定账户的代币余额
function balanceOf(address _owner) public view returns (uint256 balance)
// 将指定数量的代币从调用者账户转移到目标账户(余额可为0),同时需触发Transfer事件,如果转账余额不足,需要throw
function transfer(address _to, uint256 _value) public returns (bool success)
// 从一个账户转移代币到另一个账户,前提是调用者已被授权进行这样的转移,这通常被称为提币,但需要依赖approve方法授权
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
// 允许一个账户(_spender)以特定的数量(_value)从调用者账户转移代币,最多总数不超过_value金额。并需要触发Approval事件,也就是授权。
//允许授权的额度_value值为0. 且为了系统安全,在进行授权时,需要分2步操作:先为该地址授权0额度,再将额度设置为需要的值。
function approve(address _spender, uint256 _value) public returns (bool success)
// 查询指定账户授权给另一个账户可以转移的代币数量
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
事件
// 调用transfer时(包括转账金额为0)需要触发该事件
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// 成功调用approve方法时需要触发该事件
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
ERC20合约
来源:ERC-20
pragma solidity ^0.4.24;
import "./IERC20.sol";
import "../../math/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Originally based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract ERC20 is IERC20 {
using SafeMath for uint256;
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowed;
uint256 private _totalSupply;
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
/**
* @dev Gets the balance of the specified address.
* @param owner The address to query the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address owner) public view returns (uint256) {
return _balances[owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param owner address The address which owns the funds.
* @param spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address owner,
address spender
)
public
view
returns (uint256)
{
return _allowed[owner][spender];
}
/**
* @dev Transfer token for a specified address
* @param to The address to transfer to.
* @param value The amount to be transferred.
*/
function transfer(address to, uint256 value) public returns (bool) {
require(value <= _balances[msg.sender]);
require(to != address(0));
_balances[msg.sender] = _balances[msg.sender].sub(value);
_balances[to] = _balances[to].add(value);
emit Transfer(msg.sender, to, value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
*/
function approve(address spender, uint256 value) public returns (bool) {
require(spender != address(0));
_allowed[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param from address The address which you want to send tokens from
* @param to address The address which you want to transfer to
* @param value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address from,
address to,
uint256 value
)
public
returns (bool)
{
require(value <= _balances[from]);
require(value <= _allowed[from][msg.sender]);
require(to != address(0));
_balances[from] = _balances[from].sub(value);
_balances[to] = _balances[to].add(value);
_allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
emit Transfer(from, to, value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed_[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param spender The address which will spend the funds.
* @param addedValue The amount of tokens to increase the allowance by.
*/
function increaseAllowance(
address spender,
uint256 addedValue
)
public
returns (bool)
{
require(spender != address(0));
_allowed[msg.sender][spender] = (
_allowed[msg.sender][spender].add(addedValue));
emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed_[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param spender The address which will spend the funds.
* @param subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseAllowance(
address spender,
uint256 subtractedValue
)
public
returns (bool)
{
require(spender != address(0));
_allowed[msg.sender][spender] = (
_allowed[msg.sender][spender].sub(subtractedValue));
emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param account The account that will receive the created tokens.
* @param amount The amount that will be created.
*/
function _mint(address account, uint256 amount) internal {
require(account != 0);
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param account The account whose tokens will be burnt.
* @param amount The amount that will be burnt.
*/
function _burn(address account, uint256 amount) internal {
require(account != 0);
require(amount <= _balances[account]);
_totalSupply = _totalSupply.sub(amount);
_balances[account] = _balances[account].sub(amount);
emit Transfer(account, address(0), amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal burn function.
* @param account The account whose tokens will be burnt.
* @param amount The amount that will be burnt.
*/
function _burnFrom(address account, uint256 amount) internal {
require(amount <= _allowed[account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
_allowed[account][msg.sender] = _allowed[account][msg.sender].sub(
amount);
_burn(account, amount);
}
}
event事件
事件(Event)是一种特殊的 Solidity 语言结构,用于在智能合约执行期间发出通知。事件允许智能合约与外部应用程序进行通信,以便它们可以监视合约中的活动并采取适当的行动。用户可以在区块链上实时监听正在发送的事件,而无需支付过多成本。一旦事件被触发,所有监听该事件的监听器都将收到通知。
事件的定义
event EventName(type1 indexed arg1, type2 arg2, ...);
案例
event Sent(address from, address to, uint amount);
- 声明了一个“事件”(event),它会在send函数的最后一行触发
- 用户可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知
- 所有的事件都包含了from,to和amount三个参数,可方便追踪事务
- emit触发Sent事件,并将参数传入
emit Sent(msg.sender, receiver, amount);
在前端使用web3.js创建Coin合约对象,监听事件:
// 监听Coin合约的Sent事件代码片段
Coin.Sent().watch({}, ", function(error, result) {
if (!error) {
console.log("Coin transfer:" + result.args.amount +
"coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender:" + Coin.balances.call(result.args.from)+
"Receiver:" + Coin.balances.call(result.args.to));
}
});
简单的投票合约Ballot
- 电子投票的主要问题是如何将投票权分配给正确的人员以及如何防止被操纵。这个合约展示了如何进行委托投票,同时,计票又是自动和完全透明的
- 为每个(投票)表决创建一份合约,然后作为合约的创造者–即主席,将给予每个独立的地址以投票权
- 地址后面的人可以选择自己投票,或者委托给他们信任的人来投票
- 在投票时间结束时,winningProposal()将返回获得最多投票的提案
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title 使用委托进行投票。
contract Ballot {
// 这声明了一个新的复杂类型,稍后将用于变量。
// 它将表示单个选民。
struct Voter { //投票人结构体
uint weight; // 投票权重,只有0,1
bool voted; // 如果为true,则该人已经投票
address delegate; // 委托的人,即代理地址
uint vote; // 所投提案的索引,即赞同的方案在数组中的位置,每个提案用数字来表示
}
// 这是单个提案的类型。
struct Proposal { // 提案结构体
bytes32 name; // 提案名称(最多32字节)
uint voteCount; // 提案得到的票数
}
address public chairperson; // 主席(合约创建者)
// 这声明了一个状态变量,
// 为每个可能的地址存储一个`Voter`结构。
mapping(address => Voter) public voters; // 所有的投票人,地址到投票者的映射,一个adress对应一个结构体Voter的映射
// 一个动态大小的`Proposal`结构数组。
Proposal[] public proposals; // 所有的提案,数组,元素是proposals结构体,下标做提案
/// 构造函数,将合约创建者设置为主席、投票人,另外需要传入所有提案的简称
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1; //主席权重+1
// 对于提供的每个提案名称,
// 创建一个新的提案对象并将其添加到数组的末尾。
for (uint i = 0; i < proposalNames.length; i++) {
// `Proposal({...})` 创建一个临时
// Proposal对象,并`proposals.push(...)`将其附加到`proposals`的末尾。
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
}
// 将选民赋予在此选票上投票的权利,即主席为他人账户授予投票权
// 只能由 `chairperson` 调用。
function giveRightToVote(address voter) external {
// 如果 `require` 的第一个参数为 `false`,则终止执行,并且所有对状态和以太币余额的更改都将被撤销。
// 这在旧的 EVM 版本中会消耗所有的 gas,但现在不会了。
// 使用 `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) external {
// 分配引用
// storage:声明该变量存储在storage存储空间中
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "You have no right to vote");
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
// 只要 `to` 也进行了委托,就转发委托。
// 一般来说,这样的循环非常危险,
// 因为如果它们运行时间过长,可能
// 需要的 gas 超过了一个块中可用的 gas。
// 在这种情况下,委托将不会执行,
// 但在其他情况下,这样的循环可能
// 导致合约完全“卡住”。
//a->b ,b->c a->c,即A调用委托方法要委托给B,但是B委托给了C,将A的委托人设置成C
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// 发现委托中的循环,不允许。
//防止死循环,a->b,b->c c->a
require(to != msg.sender, "Found loop in delegation.");
}
Voter storage delegate_ = voters[to];
// 选民不能委托给无法投票的账户。
require(delegate_.weight >= 1);
// 由于 `sender` 是一个引用,这
// 修改了 `voters[msg.sender]`。
sender.voted = true;
sender.delegate = to;
if (delegate_.voted) {
// 如果代表已经投票,
// 直接将自己的票投给委托人投的提案
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// 如果代表尚未投票,
// 则将自己的投票权重加给委托人
delegate_.weight += sender.weight;
}
}
/// 将您的投票(包括委托给您的投票)投给提案 `proposals[proposal].name`。
function vote(uint proposal) external {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
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() 函数以获取包含在 proposals 数组中的获胜者的索引,然后
// 返回获胜者的名称
function winnerName() external view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}