智能合约既可以实现代币,又可以基于智能合约代码运行不可变的逻辑。虽然智能合约已经形成了一个充满活力和创造力的互联生态系统,但它也吸引着攻击者利用智能合约中的漏洞和TRON网络中的意外行为而从中牟利。由于智能合约代码不支持更改,也就无法修改安全漏洞,并且从智能合约被盗的资产是不可恢复的,而且很难追踪。
因此,在向主网发布任何代码之前,一定要做好充足的预防措施,来保护智能合约中任何有价值的东西。下面将介绍不同类型的攻击和最佳实践,以确保您的合约正确、安全地运行。
智能合约开发注意事项
安全性始于适当的设计和开发过程。有关智能合约开发流程中的注意事项有很多,但至少要保证如下几点:
- 所有的代码都存储在一个版本控制系统中,比如git
- 所有的代码修改通过Pull Requests
- 所有Pull Requests都至少有一个审阅者
- 使用TRON智能合约开发工具,如Tronbox,一键编译、部署和运行测试代码
- 在合并每个pull request之前,应该通过基本的代码分析工具(如Mythril和Slither)运行了你的代码
- 代码不抛出任何编译器警告
- 代码有良好的文档记录
攻击和漏洞
下面介绍一些常见的漏洞
重入
可重入性是开发智能合约时需要考虑的最大、最重要的安全问题之一。TVM不能同时运行多个合约,当一个合约调用其它合约时,TVM会暂停调用合约的执行和内存状态,直到调用返回,然后再继续执行。这种暂停和重新启动可能会出现被称为“重新进入”的漏洞。
下面是一个存在重入漏洞的合约示例:
// THIS CONTRACT HAS INTENTIONAL VULNERABILITY, DO NOT COPY
contract Victim {
mapping (address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
balances[msg.sender] = 0;
}
}
withdraw函数允许用户提取他的余额,它将依次进行以下操作:
- 获取用户的余额
- 发送余额给用户
- 设置用户余额为0
如果该函数调用来自一个普通外部帐户(比如你自己的Tronlink帐户),它的功能和预期是一样的:msg.sender.call.value()只是给你的帐户发送TRX。然而,其它智能合约也可以调用该合约的函数。如果一个自定义的恶意合约调用了withdraw()函数, msg.sender.call.value()不仅会发送TRX,它还会隐式的执行该合约中的代码,例如如下这个恶意的合约:
contract Attacker {
uint count;
function beginAttack() external payable {
count = 5;
Victim(VICTIM_ADDRESS).deposit.value(1 trx)();
Victim(VICTIM_ADDRESS).withdraw();
}
function() external payable {
if(count>0)
{
count -=1;
Victim(VICTIM_ADDRESS).withdraw();
}
}
}
调用 Attacker.beginAttack()将开始一个如下的循环:
0.) 攻击者通过外部账户触发合约调用:Attacker.beginAttack() 并存入1 TRX。
0.) Attacker.beginAttack() 将 1 TRX 存入 Victim合约: Victim.deposit.value(1 trx)();
1.) Attacker 调用Victim的提现函数 : Victim.withdraw()
1.) Victim 合约获取调用者的余额为1TRX: balances[msg.sender]
1.) Victim 发送 TRX 给 Attacker ,并触发调用Attacker的默认的回调函数
2.) Attacker回调函数内执行 -> Victim.withdraw()
2.) Victim 读取余额: balances[msg.sender]
2.) Victim 发送 TRX 给 Attacker 并触发调用Attacker的默认的回调函数
3.) Attacker回调函数内执行 -> Victim.withdraw()
3.) Victim 读取余额: balances[msg.sender]
3.) Victim 发送 TRX 给 Attacker 并触发调用Attacker的默认的回调函数
4.) Attacker 为了不超过合约允许执行的最大时间, 执行若干次后,不再继续执行withdraw,直接返回。
3.) balances[msg.sender] = 0;
2.) balances[msg.sender] = 0;
1.) balances[msg.sender] = 0;
攻击者调用Attacker.beginAttack并转入1trx,然后对Victim合约进行重入攻击,取走了其他用户的存入的TRX。
如何解决重入攻击
通过调整存储更新和外部调用的顺序,可以有效防止可重入攻击。如下示例中,withdraw函数先将存储的余额信息设置为0,再进行TRX转账,避免了恶意者对代码进行重入攻击。
contract NoLongerAVictim {
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
}
}
当你想要将TRX发送到一个不可信的地址或与一个未知的合约进行交互(例如调用用户提供的令牌地址的transfer())时,就为自己打开了重入的可能性。因此通过设计既不发送TRX也不调用不可信合约,将有效防止重入攻击。
更多类型的攻击
除了上述由智能合约编码导致的可重入攻击外,还有很多其它类型的攻击,例如:
- TRX发送拒绝
- 整数向上溢出和向下溢出
Updated 9 months ago