特点
- EVM出于所谓运算速度和效率方面考虑,采用了非主流的256bit整数。
- 不支持浮点数
- 缺乏标准库支持,例如字符串拼接、切割、查找等等都需要开发者自己实现
- 给合约打补丁或是部分升级合约代码在EVM中是完全不可能的
存储
Code
code 部署合约时储存 data 字段也就是合约内容的空间,即专门存储智能合约的二进制源码的空间
Storage
Storage 是一个可以读写修改的持久存储的空间,也是每个合约持久化存储数据的地方。Storage 是一个巨大的 map,一共 2 256 2^{256} 2256 个插槽 (slot),每个插糟有 32 bytes,合约中的“状态变量”会根据其具体类型分别保存到这些插槽中。
Stack
stack 即所谓的“运行栈",用来保存 EVM 指令的输入和输出数据。可以免费使用,没有 gas 消耗,用来保存函数的局部变量,数量被限制在 16 个。stack 的最大深度为 1024 ,其中每个单元是 32 byte。
Args
args 也叫 calldata,是一段只读的可寻址的保存函数调用参数的空间,与栈不同的地方的是,如果要使用 calldata 里面的数据,必须手动指定偏移量和读取的字节数。
Memory
Memory 一个简单的字节数组,主要是在运行期间存储数据,将参数传递给内部函数。基于 32 byte 进行寻址和扩展。
安全
溢出攻击
EVM 的 safeMath 库不是默认使用,例如开发者对 solidity 的 uint256 做计算的时候,如果最终结果大于 uint256 的最大值,就会产生溢出变为一个很小的数,这样就产生了溢出漏洞。诸如 BEC、SMT 等相关币种都遭受过溢出攻击,带来了极度严重都后果
对策: 利用一些第三方的safemath库来保证这个数字操作的安全
重入攻击
solidity 一大特性是可以调用外部其他合约,但在将 eth 发送给外部地址或者调用外部合约的时候, 需要合约提交外部调用。如果外部地址是恶意合约,攻击者可以在 Fallback 函数中加入恶意代码,当发生转账的时候,就会调用 Fallback 函数执行恶意代码,恶意代码会执行调用合约的有漏洞函数,导致转账重新提交。最严重的重入攻击发生在以太坊早期,即知名的 DAO 漏洞 - (先转账再扣钱,结果转账到一个恶意合约地址)。
对策: 在金额转移之前,一定要先扣钱
非预期函数执行
EVM 没有严格检查函数调用,如果合约地址作为传入参数可控,可能导致非预期行为发生。
权限问题,如果构造函数(0.4.20之前的版本)名和合约名写的不一致,就会被外部或者其他合约所调用,恶意的攻击者可能获取了我们当前智能合约的所有权
对策: 注意编码规范,保证合约名与构造函数名相同。如果现在使用构造函数,我们建议使用constractor来创建
时间戳依赖
时间戳就可以被矿工修改的
可预测的随机数
使用区块变量生成随机数,所有的区块变量都可以被矿工操纵.因为这些区块变量在同一区块上是共用的。攻击者通过其恶意合约调用受害者合约,那么此交易打包在同一区块中,其区块变量是一样的。
对策: 通过Oracle获取随机数
服务攻击
withdraw时,不要直接把币转到用户指定的某个地址(可能是个恶意合约), 而是让用户主动提取自己的押金
不要空投,而是让人主动来领取.
私有数据
不要以为声明为private的变量外部就无法访问到
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract DCA {
uint counter;
uint private age;
constructor(uint _c) {
counter = _c;
age = _c + 2;
}
}
通过api可直接按内存地址访问到private的数据
import web3
url = ""
w3 = web3.Web3(web3.HTTPProvider(url))
w3.eth.get_storage_at(address, 0x0)
#一个uint占256bit, 即32字节 #HexBytes('0x0000000000000000000000000000000000000000000000000000000000000005')
w3.eth.get_storage_at(address, 0x1)
# HexBytes('0x0000000000000000000000000000000000000000000000000000000000000007')
注意
tx.origin和msg.sender区别
-
msg.sender
(address
): 消息发送方 (当前调用) -
tx.origin
(address
): 交易发送方(完整调用链上的原始发送方)
用户A -> 合约1 -> 合约2
若在合约2中使用 msg.sender,会得到合约1的地址
若在合约2中使用 tx.origin,会得到用户A,即整个调用链的起点
合约间的调用方式call、delegatecall
调用方式 | 修改storage | 调用者的msg.sender | 被调用者的msg.sender | 执行上下文件 |
---|---|---|---|---|
call | 修改被调用者的合约storage | 交易的发起者地址 | 调用者地址 | 在被调用者里 |
delegatecall | 修改调用者的合约storage | 交易的发起者地址 | 调用者地址 | 在调用者里 |
receive 和 fallback 调用流程
接收以太功能函数
solidity 接收函数 receive 没有参数、没有返回值。
solidity 向合约转账,发送 Eth,就会执行 receive
函数。
如果没有定义接收函数 receive,就会执行 fallback
函数。
fallback函数,即 回退函数,没有名字,没有参数,没有返回值
function(){
}