当您尝试向用户发送资金并且该功能依赖于资金转移成功时,功能中可能会发生 DoS(拒绝服务)攻击。
如果资金被发送到由坏人创建的智能合约,这可能会有问题,因为他们可以简单地创建一个回退功能来恢复所有支付。
For example:
// INSECURE
contract Auction {
address currentLeader;
uint highestBid;
function bid() payable {
require(msg.value > highestBid);
require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
currentLeader = msg.sender;
highestBid = msg.value;
}
}
这段代码是一个简单的拍卖合约,它允许参与者在拍卖中进行竞标。
让我们来详细解释代码的含义:
address currentLeader;
:声明一个地址类型的变量currentLeader
,用于存储当前领先者的地址。uint highestBid;
:声明一个无符号整数类型的变量highestBid
,用于存储当前的最高竞标价。function bid() payable { ... }
:定义了一个名为bid
的函数,它是一个可支付的函数,意味着在调用该函数时需要同时发送一定数量的以太币作为竞标价。require(msg.value > highestBid);
:使用require
断言语句,要求发送的以太币必须大于当前最高竞标价,否则函数执行会被中止。require(currentLeader.send(highestBid));
:使用require
断言语句,尝试将当前最高竞标价退款给之前的领先者(通过调用send
函数向currentLeader
地址发送以太币)。如果退款失败,即send
函数返回false
,则会触发异常,导致函数执行被中止并撤销之前的状态改变。currentLeader = msg.sender;
:将当前竞标者的地址msg.sender
赋值给currentLeader
,即将当前竞标者变为新的领先者。highestBid = msg.value;
:将发送的以太币数量msg.value
赋值给highestBid
,即更新最高竞标价为新的竞标价。
具体来说,以上存在以下几个潜在的安全风险:
- 当退款给之前的领先者时,使用的是
send
函数。这种方式是不安全的,因为在send
函数执行失败时,会继续执行后续的代码。这可能导致竞标者的资金被锁定,无法退回给他们。 - 没有对竞标者地址进行任何验证或授权。这意味着任何人都可以调用
bid
函数并发送竞标价,而不考虑其身份或资金来源。 - 没有提供竞标结束或拍卖结束的机制。这意味着合约可能永远不会结束,导致竞标资金无法被退回或分配给最高竞标者。
正如你在这个例子中看到的,如果攻击者从一个具有回退功能的智能合约中出价,那么他们将永远无法退还,因此,没有人可以出更高的价。
如果没有攻击者在场,这也可能会出现问题。 例如,您可能希望通过遍历数组来向一组用户支付费用,当然,您希望确保每个用户都得到适当的支付。 这里的问题是,如果一笔付款失败,该功能将被还原,并且没有人付款。如下段代码所示:
address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
}
}
这段代码涉及退款机制,旨在将参与拍卖的地址的退款金额发送回它们的地址。代码存在两个问题:一是使用 send
函数进行转账,二是当某个转账失败时,会阻塞其他地址的退款:
- 使用
send
函数进行转账:send
函数是一种不安全的转账方式。当转账失败时,它会返回false
,但代码并没有处理这种情况,导致退款过程可能会中断,无法继续向后执行。这可能导致部分地址无法收到退款,也会导致其他地址的退款被阻塞。 - 循环中的退款操作顺序问题:由于循环中的退款操作是按照
refundAddresses
数组的顺序执行的,如果某个地址的退款转账失败,将会阻塞后面地址的退款。这可能导致其他地址的退款被延迟或无法完成。
我们可以对上述代码做出如下改进,使用更安全的转账方式:替代使用 send
函数,可以使用 transfer
或 call
函数来进行转账。这样,如果转账失败,会抛出异常并中止当前的退款操作,但不会影响其他地址的退款:
address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
require(refundAddresses[x].transfer(refunds[refundAddresses[x]]));
}
}
总的来说,解决这个问题的一个有效方法是在当前的推式支付系统上使用拉式支付系统。 为此,将每笔付款分成自己的交易,并让收款人调用该函数。
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)("");
require(success);
}
}
address highestBidder;
:声明一个地址类型的变量highestBidder
,用于存储当前的最高出价者的地址。uint highestBid;
:声明一个无符号整数类型的变量highestBid
,用于存储当前的最高出价。mapping(address => uint) refunds;
:声明一个映射类型变量refunds
,将地址映射到对应的退款金额。function bid() payable external { ... }
:定义了一个名为bid
的外部函数,用于参与竞标。函数具有payable
修饰符,表示在调用函数时需要同时发送一定数量的以太币作为竞标价。require(msg.value >= highestBid);
:使用require
断言语句,要求发送的以太币必须大于或等于当前的最高出价,否则函数执行会被中止。if (highestBidder != address(0)) { ... }
:使用条件语句,检查是否存在之前的最高出价者。address(0)
表示一个空地址。refunds[highestBidder] += highestBid;
:记录之前的最高出价者可以领取的退款金额。将退款金额增加到refunds
映射中对应地址的值。highestBidder = msg.sender;
:将当前竞标者的地址msg.sender
赋值给highestBidder
,即将当前竞标者设为最高出价者。highestBid = msg.value;
:将发送的以太币数量msg.value
赋值给highestBid
,即更新最高出价为新的竞标价。function withdrawRefund() external { ... }
:定义了一个名为withdrawRefund
的外部函数,用于提取退款。uint refund = refunds[msg.sender];
:获取调用者(提取者)在refunds
映射中对应的退款金额。refunds[msg.sender] = 0;
:将调用者在refunds
映射中对应的退款金额设置为零,表示已提取退款。(bool success, ) = msg.sender.call.value(refund)("");
:使用低级别调用call.value
,将退款金额发送给调用者。require(success);
:使用require
断言语句,确保退款操作成功。如果退款失败,即call
执行返回的布尔值success
为false
,则触发异常,函数执行被中止。
这段代码的功能是允许用户进行竞标,并记录最高出价者和对应的退款金额。参与者可以通过调用 bid
函数进行竞标,发送一定数量的以太币作为出价。当有新的最高出价时,之前的最高出价者可以通过调用 withdrawRefund
函数提取其退款。