Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在以太坊虚拟机(EVM)上运行。
Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。
注:静态类型语言(Statically Typed Language)是指在编译阶段即要求变量、函数参数以及对象的属性具有固定数据类型的语言。在静态类型语言中,一旦一个变量被声明为某种类型,它在其生命周期内就必须保持这个类型,不能随意改变。程序员在编写代码时必须显式地为变量指定类型,并且编译器会在编译时期对类型进行严格检查,确保类型安全。
编译环境:Remix
英文版:https://remix.ethereum.org/
Remix 是一个基于 Web 浏览器的 IDE,它可以让你编写 Solidity 智能合约,然后部署并运行该智能合约。
下一篇主要介绍Remix 的使用
重要:安全考量
在 Solidity 中,安全考量尤为重要,因为智能合约可以用来处理token,甚至有可能是更有价值的东西。 除此之外,智能合约的每一次执行都是公开的,而且源代码也通常是容易获得的。
陷阱
私有信息
(1)在智能合约中你所用的一切都是公开可见的,即便是局部变量和被标记成 private 的状态变量也是如此。
随机性
(1)如果不想让矿工作弊的话,在智能合约中使用随机数会很棘手 (注:在智能合约中使用随机数很难保证节点不作弊, 这是因为智能合约中的随机数一般要依赖计算节点的本地时间得到, 而本地时间是可以被恶意节点伪造的,因此这种方法并不安全。 通行的做法是采用 链外 的第三方服务,比如 Oraclize 来获取随机数)。
重入
任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 以太币 的转移 ,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。 举个例子,下面的代码中有一个 bug(这只是一个代码段,不是完整的合约):
bash
pragma solidity ^0.4.0;
// 不要使用这个合约,其中包含一个 bug。
contract Fund {
/// 合约中 |ether| 分成的映射。
mapping(address => uint) shares;
/// 提取你的分成。
function withdraw() public {
if (msg.sender.send(shares[msg.sender]))
//向msg.sender转入shares[msg.sender]个ETH
shares[msg.sender] = 0;
}
}
这里的问题不是很严重,因为有限的 gas 也作为 send 的一部分,但仍然暴露了一个缺陷: 以太币 的传输过程中总是可以包含代码执行,所以接收者可以是一个回调进入 withdraw 的合约。 这就会使其多次得到退款,从而将合约中的全部 以太币 提取。
bash
pragma solidity ^0.4.0;
// 不要使用这个合约,其中包含一个 bug。
contract Fund {
/// 合约中 |ether| 分成的映射。
mapping(address => uint) shares;
/// 提取你的分成。
function withdraw() public {
if (msg.sender.call.value(shares[msg.sender])())
shares[msg.sender] = 0;
}
}
这个合约将允许一个攻击者多次得到退款,因为它使用了 call ,默认发送所有剩余的 gas。
为了避免重入,你可以使用下面撰写的"检查-生效-交互"(Checks-Effects-Interactions)模式:
pragma solidity ^0.4.11;
bash
contract Fund {
/// 合约中 |ether| 分成的映射。
mapping(address => uint) shares;
/// 提取你的分成。
function withdraw() public {
var share = shares[msg.sender];//检查
shares[msg.sender] = 0;//生效
msg.sender.transfer(share);//交互
}
}
gas 限制和循环
必须谨慎使用没有固定迭代次数的循环,例如依赖于 存储 值的循环: 由于区块 gas 有限,交易只能消耗一定数量的 gas。 无论是明确指出的还是正常运行过程中的,循环中的数次迭代操作所消耗的 gas 都有可能超出区块的 gas 限制,从而导致整个合约在某个时刻骤然停止。 这可能不适用于只被用来从区块链中读取数据的 view 函数。 尽管如此,这些函数仍然可能会被其它合约当作 链上 操作的一部分来调用,并使那些操作骤然停止。 请在合约代码的说明文档中明确说明这些情况。
发送和接收以太币
(1)目前无论是合约还是"外部账户"都不能阻止有人给它们发送 以太币。 合约可以对一个正常的转账做出反应并拒绝它,但还有些方法可以不通过创建消息来发送 以太币。 其中一种方法就是单纯地向合约地址"挖矿",另一种方法就是使用 selfdestruct(x) 。
(2)如果一个合约收到了 以太币 (且没有函数被调用),就会执行 fallback 函数。 如果没有 fallback 函数,那么 以太币 会被拒收(同时会抛出异常)。 在 fallback 函数执行过程中,合约只能依靠此时可用的"gas 津贴"(2300 gas)来执行。 这笔津贴并不足以用来完成任何方式的 存储 访问。 为了确保你的合约可以通过这种方式收到 以太币,请你核对 fallback 函数所需的 gas 数量
(3)有一种方法可以通过使用 addr.call.value(x)() 向接收合约发送更多的 gas。 这本质上跟 addr.transfer(x) 是一样的, 只不过前者发送所有剩余的 gas,并且使得接收者有能力执行更加昂贵的操作 (它只会返回一个错误代码,而且也不会自动传播这个错误)。 这可能包括回调发送合约或者你想不到的其它状态改变的情况。 因此这种方法无论是给诚实用户还是恶意行为者都提供了极大的灵活性。
(4)如果你想要使用 address.transfer 发送 以太币 ,你需要注意以下几个细节:
如果接收者是一个合约,它会执行自己的 fallback 函数(**内部有回调函数**),从而可以回调发送 以太币 的合约。
如果调用的深度超过 1024,发送 以太币 也会失败。由于调用者对调用深度有完全的控制权,他们可以强制使这次发送失败; 请考虑这种可能性,或者使用 send 并且确保每次都核对它的返回值。 更好的方法是使用一种接收者可以取回 以太币 的方式编写你的合约。
发送 以太币 也可能因为接收方合约的执行所需的 gas 多于分配的 gas 数量而失败 (确切地说,是使用了 require , assert, revert , throw 或者因为这个操作过于昂贵) - "gas 不够用了"。 如果你使用 transfer 或者 send 的同时带有返回值检查,这就为接收者提供了在发送合约中阻断进程的方法。 再次说明,最佳实践是使用 "取回"模式而不是"发送"模式。
调用栈深度
外部函数调用随时会失败,因为它们超过了调用栈的上限 1024。 在这种情况下,Solidity 会抛出一个异常。 恶意行为者也许能够在与你的合约交互之前强制将调用栈设置成一个比较高的值。请注意,使用 .send() 时如果超出调用栈 并不会 抛出异常,而是会返回 false。 低级的函数比如 .call(),.callcode() 和 .delegatecall() 也都是这样的。
注意:
在 for (var i = 0; i < arrayName.length; i++) { ... } 中, i 的类型会变为 uint8 , 因为这是保存 0 值所需的最小类型。如果数组超过 255 个元素,则循环不会终止。
不占用完整 32 字节的类型可能包含"脏高位"。这在当你访问 msg.data 的时候尤为重要 ------ 它带来了延展性风险: 你既可以用原始字节 0xff000001 也可以用 0x00000001 作为参数来调用函数 f(uint8 x) 以构造交易。 这两个参数都会被正常提供给合约,并且 x 的值看起来都像是数字 1, 但 msg.data 会不一样,所以如果你无论怎么使用 keccak256(msg.data),你都会得到不同的结果。