面向老程序员的Solidity摘要
开发以太坊DApp,Solidity是必经之路。然而,对于跟我一样的那些有多年开发经验的以太坊新人来讲,Solidity学习固然是一方面,但更重要的是快速了解它的惯例和套路,以及一些值得注意的事项。这,正是本文试图达到的目标。至于详尽的语法文档,还请各位自行查阅。
EVM和字节码
与Java代码类似,Solidity代码会先被编译成字节码,然后再由EVM负责执行。从逻辑上来讲,可以将以太坊视为一台计算机,其中的每个EVM节点类似在计算机中执行的进程,分布式账本则是这台计算机的存储。
一旦部署成功,其代码会被复制到以太坊上其他节点,并可以通过命令查看其源码。以Truffle开发环境为例:
- truffle develop
- 部署MyCoin合约,deploy
- MyCoin.at(地址),从其返回的json对象中的source属性即可看到合约代码。
合约部署之后就无法更新,这就给开发者带来了相当大的挑战:
- 如何开发出高质量的合约,尽可能的没有Bug?
- 如何设计可升级的合约?
执行代价
说到程序执行的代价,一般指的都是花多少内存、存储和CPU时间。但执行以太坊上的代码,除了这些通常意义的代价之外,还需要真金白银。这是因为以太坊上的交易确认都是需要花钱的!它们主要是那些改变以太坊状态的操作,如:
- 账户转账
- 部署合约
- 合约内的写操作
而且,与其他系统不同,这些操作的执行结果并不会立刻生效。它们会以交易的形式提交到交易池中等待矿工确认,这便是交易费的由来。并且,这个价格也不是一个固定值,它随着市场行情的波动上下浮动。如果你的交易长时间没有结果,那么可以看看是否是因为交易费过低。
关于交易费的行情,可以从最新的交易中了解。
这也给开发者带来了挑战:如何在实现功能的前提下尽可能的降低交易成本?
账户
要在以太坊上进行操作,必需要有以太坊账户。当前有两类账户类型:
- 外部账户,可简单认为是“人类用户”,有私钥和余额,交易发送前会用私钥先签名。
- 合约账户,合约部署之后,会随之对应有一个账户,由余额和相应的合约状态数据。它由外部消息来触发执行。触发源来自外部账户或其他合约账户。
这里也带来了一些关于安全性方面的概念转变:
- 私钥是终极秘密,一定是本地存储,否则都是不安全的。
- 由于合约是公开的,谁都可以发起执行。如果要实现“只有xxx才能执行本合约”,必需要在合约内部代码中进行控制。
合约语法
合约类似Java中的类,但与类不同之处在于,它的构造函数只会被调用一次,即部署合约的时候。
合约的状态变量相当于类的实例变量,但同样是持久化的。并且,mapping只能声明成状态变量但可在函数内引用。
变量类型同样也分值类型和引用类型,其中引用类型包括:数组和结构体,后者给自定义类型提供方案。
函数可以返回一个值或多个值,同时可以指定返回的变量。如:
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
函数修改器(Modifier)类似AOP中的拦截器,提供了修改函数执行流程的机会,一般用来做验证和检查。其中“_”用来将控制流返还给被修改的函数,如下例:
modifier onlySeller() { // Modifier
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
function abort() public onlySeller { // Modifier usage
// ...
}
几个重要的修改器:
- payable,接收以太的函数必需加上
- view或pure,表示函数不会改变以太坊状态
事件提供了让外部应用了解合约状态变化的途径,一般使用流程是:
- 合约内部发出事件
- 外部应用利用web3监听事件
可见性:
- external,仅适用于函数,表示其可被外部合约或交易调用,但不能被内部调用。
- public
- 函数缺省的可见性,可被内部调用和通过消息调用,
- 状态变量,EVM会为其自动产生getter
- internal,函数和状态变量可被当前合约和其子合约调用
- 状态变量的缺省可见性
- private,函数和状态变量仅被当前合约调用
合约支持多重继承。
EVM提供了4种数据位置用来存放数据:
- storage,持久化,存储于整个以太坊
- memory,函数的本地内存,非持久化
- calldata,函数入参,非持久化
- stack,EVM的调用栈
规则:
- 状态变量:storage
- external函数入参:calldata
- 函数入参:memory
- 函数局部变量:
- 引用类型,缺省为storage,但可被覆盖
- 值类型,memory,不可被覆盖
- mapping类型,指向外部的状态变量
- 状态变量之间赋值,将产生独立副本,即相互更改不受引用。
- storage和memory变量之间相互赋值,总产生独立副本。
- memory变量之间赋值
- 值类型,产生独立副本
- 引用类型,指向同一地址
由于合约执行是有成本的,需要警惕循环语句。
对于多重继承的合约,需要明确指明顺序,如:
contract X {}
contract A is X {}
contract C is A, X {}
fallback函数没有函数名,无法直接调用,但在两个情况下会被触发:
- 合约中无任何函数匹配调用者发过来的请求时
- 合约接收以太时,此时,fallback函数需使用payable
由于其无法被外部调用,EVM限制其只能最多消耗2300的gas,若超过,则fallback函数失败。因此,记得要测试合约的fallback函数是否会超过这个限制。
并且,fallback是安全事故的高发地,需要对其进行必要的安全相关的测试。
接口和抽象合约跟Java中的接口和抽象类差别不大,库(library)是一段可复用的代码,在调用它的合约上下文内执行:
- 它不能用状态变量
- 不能继承或被继承
- 不能接收以太
合约抛出异常之后,状态回滚,当前有3种方式:
- require(表达式),若表达式为false,则抛出异常,未使用的gas退回
- 适合验证函数的入参
- assert(表达式),同上,但未使用的gas不会退回,将全部被消耗
- 适合验证内部状态
- revert(),直接抛出异常
常见模式
鉴于以太坊应用的以下特点,编写solidity代码时需要非常小心:
- 执行消耗真金白银
- 合约公开可见,即使是private
- 合约不可篡改,一旦发布无法变更
常见的编码套路有:
- 对于支付,优先采用“取款”,而不是“转账”(即send或transfer),避免接收合约恶意fallback函数。
- 对于支付,采用CDI模式,避免重入问题。即:
- 检查 -> 更改本合约状态 ->支付。
- 善用Modifier进行权限控制。
- 使用mapping类型保存合约数据,甚至为了方便升级,单独分离出两类:
- 数据合约,仅包含mapping,保留操作mapping的函数,客观上类似数据表。
- 控制合约,仅包含逻辑控制,通过数据合约的接口操作数据。若逻辑有问题,只需升级本合约即可,数据仍然得以保留。
- 使用代理合约,参见这里。
最后,也是最省事的方式:使用成熟类库,如OpenZeppelin。关于Solidity的好东西,可以通过其Awesome List来了解。