Silent But Vulnerable: Ethereum Gas Security Concerns

Steve Marx
Dec 18, 2018 · 6 min read
Photo by Mahkeo on Unsplash

He who sent it spent it

A fundamental truth about transactions is that they’re paid for by the sender—the account that signed the transaction.

  1. They can cause that transaction to consume a lot of gas.
pragma solidity 0.5.1;contract GasBurner {
uint256 counter;
function() external payable {
for (uint256 i = 0; i < 100; i++) {
counter += 1;
}
}
}

Further reading

The above code burns the gas pointlessly, but it could be put to a more productive use. Perhaps the attacker’s contract could do some valuable computation with that gas. As Level K recently observed, a great use for excess gas is to use it to mint GasTokens, which can be turned around and sold.

Mitigation

To keep yourself safe from this sort of exploit, make sure that you always set a reasonable gas limit on your transactions.

He who filled it killed it

Because Ethereum’s computing resources are finite, there’s a limit to how much gas can be used in a single block. This is known as the block gas limit. Miners try to pack transactions into a block to get as close as possible to that gas limit because the gas fees are paid to the miner.

pragma solidity 0.5.1;contract TerribleBank {
struct Deposit {
address depositor;
uint256 amount;
}
Deposit[] public deposits;

function deposit() external payable {
deposits.push(Deposit({
depositor: msg.sender,
amount: msg.value
}));
}

function withdrawAll() external {
uint256 amount = 0;
for (uint256 i = 0; i < deposits.length; i++) {
if (deposits[i].depositor == msg.sender) {
amount += deposits[i].amount;
delete deposits[i];
}
}

msg.sender.transfer(amount);
}
}

Futher reading

SWC-128, “DoS With Block Gas Limit,” describes the general class of bugs.

Mitigation

Smart contract auditors start sweating as soon as they see a for loop. Avoid them where possible, unless they’re bounded by a small constant number of iterations.

He who relayed it paid it

Finally, I’d like to examine a vulnerability class that I haven’t seen described before.

pragma solidity 0.5.1;contract IERC20 {
function transfer(address target, uint256 amount) external returns (bool);
}
contract RelayProxy {
address owner = msg.sender;
uint256 nonce = 0;
IERC20 token = IERC20(0x...);
function execute(
address payable target,
bytes calldata data,
uint256 _nonce,
uint8 v,
bytes32 r,
bytes32 s
)
external
{
uint256 startGas = gasleft();
require(_nonce == nonce, "Bad nonce."); bytes32 h = hash(target, data, _nonce);
require(ecrecover(h, v, r, s) == owner, "Bad signature.");
(bool success, ) = target.call(data);
if (success) {
nonce += 1;
}
// pay relayer for consumed gas in tokens
require(token.transfer(msg.sender, startGas - gasleft()));
}

function hash(
address target,
bytes memory data,
uint256 _nonce
)
internal
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(target, data, _nonce));
}
}

Mitigation

A tempting solution is to increment the nonce regardless of the success or failure of the call, but this opens up a denial of service attack vector. Every time the user broadcasts their meta transaction, a malicious relayer can pick it up and cause it to fail as before. Now that the nonce has been incremented, the user needs to sign a new meta transaction, and the process repeats.

Summary

Failing to take proper care with gas can lead to serious smart contract vulnerabilities. When reading code, such vulnerabilities are often invisible, but they really stink.

ConsenSys Diligence

ConsenSys Diligence has the mission of solving Ethereum smart contract security. Contact us for an audit at diligence@consensys.net.

Thanks to Valentin Wüstholz, Bernhard Mueller, and Maurelian.

Steve Marx

Written by

Working on Ethereum smart contract security at @ConsenSys. Co-creator of https://www.site44.com and https://programtheblockchain.com .

ConsenSys Diligence

ConsenSys Diligence has the mission of solving Ethereum smart contract security. Contact us for an audit at diligence@consensys.net.