26.SWC-126_Insufficient Gas Griefing
2023-07-13 16:12:20
# 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