26.SWC-126_Insufficient Gas Griefing
2023-07-13 15:55:27 # 09.SWC

SWC-126_Insufficient Gas Griefing

SWC content

Insufficient Gas Griefing

  • Description: Insufficient gas griefing attacks can be performed on contracts which accept data and use it in a sub-call on another contract. If the sub-call fails, either the whole transaction is reverted, or execution is continued. In the case of a relayer contract, the user who executes the transaction, the ‘forwarder’, can effectively censor transactions by using just enough gas to execute the transaction, but not enough for the sub-call to succeed.

  • Remediation:

vulnerability contract:

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
contract Relayer {
uint transactionId;

struct Tx {
bytes data;
bool executed;
}

mapping (uint => Tx) transactions;

function relay(Target target, bytes memory _data) public returns(bool) {
// replay protection; do not call the same transaction twice
require(transactions[transactionId].executed == false, 'same transaction twice');
transactions[transactionId].data = _data;
transactions[transactionId].executed = true;
transactionId += 1;

(bool success, ) = address(target).call(abi.encodeWithSignature("execute(bytes)", _data));
return success;
}
}

// Contract called by Relayer
contract Target {
function execute(bytes memory _data) public {
// Execute contract code
}
}

fix

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
pragma solidity ^0.5.0;

contract Relayer {
uint transactionId;

struct Tx {
bytes data;
bool executed;
}

mapping (uint => Tx) transactions;

function relay(Target target, bytes memory _data, uint _gasLimit) public {
// replay protection; do not call the same transaction twice
require(transactions[transactionId].executed == false, 'same transaction twice');
transactions[transactionId].data = _data;
transactions[transactionId].executed = true;
transactionId += 1;

address(target).call(abi.encodeWithSignature("execute(bytes)", _data, _gasLimit));
}
}

// Contract called by Relayer
contract Target {
function execute(bytes memory _data, uint _gasLimit) public {
require(gasleft() >= _gasLimit, 'not enough gas');
// Execute contract code
}
}
  • 一般这么设计的话项目方都会预估前面的流程需要多少gas,然后去计算一个gaslimit。如果超过了这个值就说明没有按照项目方预期的那样执行,既能避免程序本身可能的逻辑问题,也能避免一些攻击
  • 那直接require返回值不行吗?没执行成功就停止。意思是业务逻辑中那个方法无论成功与否执行,都要把那个业务执行完不得回退?
  • 不一样的,这个执行也许成功了,但是执行了一些其他的操作,例如被重入了啥的,这个时候require是能过的,但是gaslimit过不了

理解

1

1
(bool success,) = payable(receiver).call{gas: 3000, value: amount}(hex"");

虽然这个方法的其中一个返回值bytes memory data被省略了,但是在solidity中还是会被返回到内存当中。因此,如果外部合约在回调函数中返回了一个极大的内容,那么我们将这个极大的内容复制到内存的时候就会消耗极多的gas,就会造成gas不足交易失败。那么在这个SWC中也是相同的道理:外部合约实现的这个方法返回了一个极大的内容。解决方法如下:

1
2
3
4
bool success;
assembly {
success := call(3000, receiver, amount, 0, 0, 0, 0)
}

使用内联汇编,设置参数,使得返回的数据不会被复制到内存当中

2

其实这个SWC的解决方案并不好,原因:gasleft本身也消耗gas,又可能执行完之后刚好不够了,存在碰巧达到阈值导致gas不够的情况。

优化的解决方法1:

1
2
3
uint256 gasAvailable = gasleft() - E;
require(gasAvailable - gasAvailable / 64 >= `txGas`, "not enough gas provided")
to.call.gas(txGas)(data); // CALL

优化的解决方法2:

1
2
to.call.gas(txGas)(data); // CALL
assert(gasleft() > txGas / 63); // "not enough gas left"

reference

Link1

link2

link3

Prev
2023-07-13 15:55:27 # 09.SWC
Next