智能合约

以太坊的智能合约是运行在 以太坊虚拟机(EVM, Ethereum Virtual Machine) 上的代码,EVM是智能合约的沙盒,合约存储在以太坊的区块链上,并被编译成 EVM字节码 。EVM字节码是一种低级的面向栈的语言,类似于汇编语言,EVM字节码可以通过以太坊虚拟机执行。

Solidity

Solidity 是一种面向合约的、为实现智能合约而创建的高级编程语言,它的语法类似于JavaScript和Python,可以编译成EVM字节码,然后在以太坊虚拟机上执行。

下面的合约是一个最简单的加密货币合约

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

contract Coin {
// 关键字“public”让这些变量可以从外部读取
address public minter;
mapping (address => uint) public balances;

// 轻客户端可以通过事件针对变化作出高效的反应
event Sent(address from, address to, uint amount);

// 这是构造函数,只有当合约创建时运行
function Coin() public {
minter = msg.sender;
}

function mint(address receiver, uint amount) public {
if (msg.sender != minter) return;
balances[receiver] += amount;
}

function send(address receiver, uint amount) public {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

接下来对合约里面的一些代码进行解读
1
address public minter;

这一行声明了一个可以被公开访问的 address 类型的状态变量。这个类型适合存储合约地址或外部人员的密钥对。

关键字 public 自动生成一个函数,可以让你在合约之外读取这个状态变量的当前值。由编译器生成的函数代码大致如下:

1
function minter() returns (address) { return minter; }

1
mapping (address => uint) public balances;

这一行创建一个公共状态变量,该类型将 address 映射为无符号整数。 Mappings 可以看作是一个哈希表,它会执行虚拟初始化,使所有可能存在的键都映射到一个字节表示全为零的值。

但是,这种类比并不太恰当,因为它既不能获得映射的所有键的列表,也不能获得所有值的列表。 因此,要么记住你添加到 mapping 中的数据(使用列表或更高级的数据类型会更好),要么在不需要键列表或值列表的上下文中使用它,就如本例。

public 关键字创建的 getter 函数 getter function 大致如下,通过该函数可以轻松地查询到账户的余额:

1
2
3
function balances(address _account) public view returns (uint) {
return balances[_account];
}

1
event Sent(address from, address to, uint amount);

这一行声明了一个 event ,它会在函数 send 中被触发。客户端可以通过监听这些事件针对变化作出高效的反应,一旦它被发出,监听该事件的 lister 都将收到通知。

可以使用如下代码监听该事件:

1
2
3
4
5
6
7
8
9
10
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));
}
})

1
2
3
function Coin() public {
minter = msg.sender;
}

特殊函数 Coin 是合约的构造函数,只有当合约创建时运行。构造函数会在合约创建时执行,且只执行一次。构造函数的函数名必须与合约名相同,且不能有返回值。

它永远存储创建合约的人的地址,全局变量 msg (以及 tx 和 **block**),包含一些允许访问区块链的属性。

msg.sender 始终指向当前函数的调用者的地址,而 tx.origin 始终指向事务的发起者。

可见性

Solidity有几种默认的变量或函数访问域关键字: privatepublicexternalprivate 。对合约实例方法来讲,默认可见状态为 public ,而合约实例变量的默认可见状态为 private

  • public :表示该函数或状态变量可以从任何地方调用,包括合同内部、外部合同、外部用户。如果不显式指定函数或状态变量的可见性,它们将被视为public。
  • private :表示函数或变量只能在本合约内部使用(代码层面)
  • external :表示函数只能从外部访问,不能被合约里的函数直接调用,但可以使用 this.func() 外部调用的方式调用该函数
  • internal :一般用在合约继承中,父合约中被标记成 internal 状态变量或函数可供子合约进行直接访问和调用(外部无法直接获取和调用)

底层调用方式

Solidity有两种用于与其他合同进行交互的低级函数: calldelegatecall

它们的区别如下图所示:

  • call 方式会调用外部合约B的 func() 函数,在外部合约上下文执行完后继续返回本合约A上下文执行。 call 返回一个bool值来表明外部调用成功与否
  • delegatecall 方式相当于将外部合约B的 func() 代码复制到本合约的上下文空间中执行。 delegatecall 返回一个bool值来表明外部调用成功与否

此外还有 callcode 函数,它是 delegatecall 之前的一个版本,现在已经弃用。它俩在 msg.sendermsg.value 的指向上有区别

回退函数 fallback()

fallback() 函数是一种特殊的函数,通常用于接收以太币或处理未知函数调用,具有以下特征:

  • 三无函数:一个合同只能有一个 fallback() 函数,且没有名字、没有参数、没有返回值
  • 替补函数:如果一个合同收到一个未知的函数调用,就会执行 fallback() 函数。这可用于实现一种默认行为或错误处理逻辑
  • 收币函数:当合同收到以太币时,会执行 fallback() 函数,以处理接收到的以太币
  • 如果合同需要接收以太币,且没有声明 receive() 函数,那么需要将 fallback() 函数声明为 payable ,否则会抛出异常
  • Solidity 0.6.0 及更高版本中,无数据的以太币转账将触发合约的 receive() 函数(如果定义了的话),而不是 fallback() 函数。如果没有定义 receive() 函数,则会触发 fallback() 函数

交易函数

发送Ether主要有以下几个函数: call.value()()send()transfer()

call.value()()

1
2
3
4
5
address.to.call.value(amountInWei)(data)

// `address.to` 是要接收以太币的目标地址
// `amountInWei` 是要发送的以太币数量,以wei为单位
// `data` 是要传递给目标地址的调用数据,如果不发送数据,可以省略

在合约中直接发起 TX 的函数之一,相当危险

send()

1
2
3
4
address.send(uint256 amount);

// `address` 是接收以太币的目标地址
// `amount` 是要发送的以太币数量,以wei为单位

通过该函数发送Ether失败时直接返回 false

send() 的目标如果是合约账户,则会尝试调用它的 fallback() 函数, fallback() 函数执行失败,会直接返回 false ,但只提供 2300 Gas 给 fallback() ,所以可以防止重入漏洞

transfer()

1
2
3
4
address.transfer(uint256 amount);

// `address` 是接收以太币的目标地址
// `amount` 是要发送的以太币数量,以wei为单位

transfer() 是一个较为安全的转币函数,该函数调用没有返回值,当发送失败时(例如,由于 gas 不足或接收函数抛出异常)会自动回滚状态,抛出异常。如果目标是一个合约账户,它会尝试执行合约的接收函数( fallback() 函数),并且只会传递 2300 Gas 用于 fallback() 函数执行,这可以防止因 gas 不足而导致的转账失败。

自定义修饰符

自定义修饰符(Modifiers)用于在函数执行前和/或执行后修改函数的行为

  • 定义修饰符:
    1
    2
    3
    4
    5
    modifier ModifierName(parameters) {
    // 在函数执行前的代码,如 `require(msg.sender == owner);`
    _; // 占位符
    // 在函数执行后的代码
    }

ModifierName 是修饰符的名称,可以自定义

parameters 可选,用于传递参数给修饰符,通常用于指定某些条件

_ 是占位符,表示函数的实际逻辑将在此执行。如果修饰符通过了所有条件检查,那么 _ 中的代码将在函数执行之前和之后执行

  • 在函数中应用修饰符
    1
    2
    3
    function functionName(parameters) public ModifierName(parameters) {
    // 函数的实际逻辑
    }

只需要在函数声明之前加上修饰符名称,就可以将其应用于该函数

Ethererum IDE

在线IDE:Remix

离线版Remix:Remix | github

TODO

  • 整理学习Solidity常见漏洞类型
  • 学习 Remix-ide