-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLUPA.sol
210 lines (180 loc) · 6.97 KB
/
LUPA.sol
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
import "./SimpleCommit.sol";
using SimpleCommit for SimpleCommit.CommitType;
/**
* @title LUPA (Lowest-Unmatched Price Auction)
* @notice An implementation of a sealed-bid auction where the lowest unique bid wins
* @dev Uses a commit-reveal scheme to ensure bid privacy and prevent manipulation
*/
contract LUPA {
// Custom errors for gas-efficient error handling
error InvalidState();
error InvalidValue();
error NotOwner();
error NoBidToReveal();
error IncorrectReveal();
error InsufficientDeposit();
error NoDepositToWithdraw();
error AuctionNotFinished();
// Events for tracking auction activity
event DepositReceived(address indexed bidder, uint256 depositAmount);
event CommitmentStored(address indexed bidder, bytes32 commitHash);
event AttemptedReveal(
address indexed bidder,
uint256 originalDeposit,
uint256 revealedBid,
bool isCorrect
);
event BidCounted(uint256 bidValue, uint256 bidderCount, bool isUnmatched);
event AuctionFinished(address indexed winner, uint256 winningBid);
// Auction states
enum State {
Bid, // Bidding phase
Reveal, // Reveal phase
Finished // Auction finished
}
// Stores information about bids at each price point
struct BidValue {
uint96 biddersCount; // Number of bidders at this price
bool isUnmatched; // True if only one bidder at this price
}
// Stores individual bidder information
struct PlayerInfo {
SimpleCommit.CommitType commitment; // Bid commitment
uint96 deposit; // Deposited ETH
bool hasWithdrawn; // Whether deposit was withdrawn
}
// State variables
State public currentState; // Current auction state
uint48 public immutable endBlock; // Block number when bidding ends
address public immutable owner; // Auction creator
uint96 public immutable prizeValue; // Amount to be won
uint96 public immutable requiredDeposit; // Required deposit to participate
// Mappings
mapping(uint256 => BidValue) public bids; // Tracks bids at each price
mapping(address => PlayerInfo) public players; // Tracks individual bidder info
/**
* @dev Ensures function is called in the correct auction state
*/
modifier onlyState(State _state) {
if (getState() != _state) revert InvalidState();
_;
}
/**
* @notice Initializes the auction
* @param _biddingDuration Number of blocks the bidding phase will last
* @param _requiredDeposit Amount of ETH required as deposit
* @dev The constructor is payable and the sent ETH becomes the prize
*/
constructor(uint48 _biddingDuration, uint96 _requiredDeposit) payable {
// Validate inputs
if (msg.value == 0) revert InvalidValue();
if (_requiredDeposit == 0) revert InvalidValue();
// Initialize auction parameters
owner = msg.sender;
endBlock = uint48(block.number) + _biddingDuration;
prizeValue = uint96(msg.value);
requiredDeposit = _requiredDeposit;
currentState = State.Bid;
}
/**
* @notice Submit a bid commitment
* @param commitHash Keccak256 hash of bid value and nonce
* @dev Requires sending the required deposit amount
*/
function bid(bytes32 commitHash) external payable onlyState(State.Bid) {
// Ensure sufficient deposit
if (msg.value < requiredDeposit) revert InsufficientDeposit();
// Store player information
PlayerInfo storage player = players[msg.sender];
player.commitment.commit(commitHash);
player.deposit = uint96(msg.value);
// Emit events for deposit and commitment
emit DepositReceived(msg.sender, msg.value);
emit CommitmentStored(msg.sender, commitHash);
}
/**
* @notice Reveal a previously committed bid
* @param nonce Random value used in commitment
* @param bidValue Original bid value
* @dev Incorrect reveals forfeit deposit
*/
function revealBid(
bytes32 nonce,
uint256 bidValue
) external onlyState(State.Reveal) {
// Retrieve player information
PlayerInfo storage player = players[msg.sender];
if (!player.commitment.isCommitted()) revert NoBidToReveal();
uint256 originalDeposit = player.deposit;
// Reveal the bid
player.commitment.reveal(nonce, bidValue);
bool isCorrect = player.commitment.isCorrect();
// Emit event for attempted reveal
emit AttemptedReveal(msg.sender, originalDeposit, bidValue, isCorrect);
// Handle incorrect reveals
if (!isCorrect) {
player.hasWithdrawn = true;
return;
}
// Process correct reveals
BidValue storage bidEntry = bids[bidValue];
if (bidEntry.biddersCount == 0) {
// First bid at this value
bidEntry.biddersCount = 1;
bidEntry.isUnmatched = true;
} else {
unchecked {
bidEntry.biddersCount++;
}
bidEntry.isUnmatched = false;
}
emit BidCounted(bidValue, bidEntry.biddersCount, bidEntry.isUnmatched);
// Finish auction if bid is unmatched
if (bidEntry.isUnmatched) {
_finishAuction(msg.sender, bidValue);
}
}
/**
* @notice Withdraw deposit after auction ends
* @dev Can only be called after bidding phase and if deposit hasn't been withdrawn
*/
function withdrawDeposit() external {
// Check auction state
State currentAuctionState = getState();
if (currentAuctionState == State.Bid) revert AuctionNotFinished();
// Retrieve player information
PlayerInfo storage player = players[msg.sender];
if (player.deposit == 0 || player.hasWithdrawn)
revert NoDepositToWithdraw();
// Process withdrawal
uint96 depositAmount = player.deposit;
player.deposit = 0;
player.hasWithdrawn = true;
// Transfer deposit back to player
payable(msg.sender).transfer(depositAmount);
}
/**
* @notice Get the current state of the auction
* @return Current auction state
*/
function getState() public view returns (State) {
if (currentState == State.Finished) return State.Finished;
if (block.number > endBlock) return State.Reveal;
return currentState;
}
/**
* @dev Internal function to finalize the auction
* @param winner Address of the auction winner
* @param winningBid Value of the winning bid
*/
function _finishAuction(address winner, uint256 winningBid) private {
currentState = State.Finished;
// Transfer prize and mark deposit as withdrawn
payable(winner).transfer(prizeValue);
PlayerInfo storage winnerInfo = players[winner];
winnerInfo.hasWithdrawn = true;
emit AuctionFinished(winner, winningBid);
}
}