This repository has been archived by the owner on Aug 8, 2022. It is now read-only.
forked from paritytech/second-price-auction
-
Notifications
You must be signed in to change notification settings - Fork 11
/
SecondPriceAuction.sol
416 lines (333 loc) · 12.4 KB
/
SecondPriceAuction.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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! Copyright Parity Technologies, 2017.
//! Released under the Apache Licence 2.
pragma solidity ^0.4.17;
/// Stripped down ERC20 standard token interface.
contract Token {
function transfer(address _to, uint256 _value) public returns (bool success);
}
// From Certifier.sol
contract Certifier {
event Confirmed(address indexed who);
event Revoked(address indexed who);
function certified(address) public constant returns (bool);
function get(address, string) public constant returns (bytes32);
function getAddress(address, string) public constant returns (address);
function getUint(address, string) public constant returns (uint);
}
/// Simple modified second price auction contract. Price starts high and monotonically decreases
/// until all tokens are sold at the current price with currently received funds.
/// The price curve has been chosen to resemble a logarithmic curve
/// and produce a reasonable auction timeline.
contract SecondPriceAuction {
// Events:
/// Someone bought in at a particular max-price.
event Buyin(address indexed who, uint accounted, uint received, uint price);
/// Admin injected a purchase.
event Injected(address indexed who, uint accounted, uint received);
/// Admin uninjected a purchase.
event Uninjected(address indexed who);
/// At least 5 minutes has passed since last Ticked event.
event Ticked(uint era, uint received, uint accounted);
/// The sale just ended with the current price.
event Ended(uint price);
/// Finalised the purchase for `who`, who has been given `tokens` tokens.
event Finalised(address indexed who, uint tokens);
/// Auction is over. All accounts finalised.
event Retired();
// Constructor:
/// Simple constructor.
/// Token cap should take be in whole tokens, not smallest divisible units.
function SecondPriceAuction(
address _certifierContract,
address _tokenContract,
address _treasury,
address _admin,
uint _beginTime,
uint _tokenCap
)
public
{
certifier = Certifier(_certifierContract);
tokenContract = Token(_tokenContract);
treasury = _treasury;
admin = _admin;
beginTime = _beginTime;
tokenCap = _tokenCap;
endTime = beginTime + 28 days;
}
// No default function, entry-level users
function() public { assert(false); }
// Public interaction:
/// Buyin function. Throws if the sale is not active and when refund would be needed.
function buyin(uint8 v, bytes32 r, bytes32 s)
public
payable
when_not_halted
when_active
only_eligible(msg.sender, v, r, s)
{
flushEra();
// Flush bonus period:
if (currentBonus > 0) {
// Bonus is currently active...
if (now >= beginTime + BONUS_MIN_DURATION // ...but outside the automatic bonus period
&& lastNewInterest + BONUS_LATCH <= block.number // ...and had no new interest for some blocks
) {
currentBonus--;
}
if (now >= beginTime + BONUS_MAX_DURATION) {
currentBonus = 0;
}
if (buyins[msg.sender].received == 0) { // We have new interest
lastNewInterest = uint32(block.number);
}
}
uint accounted;
bool refund;
uint price;
(accounted, refund, price) = theDeal(msg.value);
/// No refunds allowed.
require (!refund);
// record the acceptance.
buyins[msg.sender].accounted += uint128(accounted);
buyins[msg.sender].received += uint128(msg.value);
totalAccounted += accounted;
totalReceived += msg.value;
endTime = calculateEndTime();
Buyin(msg.sender, accounted, msg.value, price);
// send to treasury
treasury.transfer(msg.value);
}
/// Like buyin except no payment required and bonus automatically given.
function inject(address _who, uint128 _received)
public
only_admin
only_basic(_who)
before_beginning
{
uint128 bonus = _received * uint128(currentBonus) / 100;
uint128 accounted = _received + bonus;
buyins[_who].accounted += accounted;
buyins[_who].received += _received;
totalAccounted += accounted;
totalReceived += _received;
endTime = calculateEndTime();
Injected(_who, accounted, _received);
}
/// Reverses a previous `inject` command.
function uninject(address _who)
public
only_admin
before_beginning
{
totalAccounted -= buyins[_who].accounted;
totalReceived -= buyins[_who].received;
delete buyins[_who];
endTime = calculateEndTime();
Uninjected(_who);
}
/// Mint tokens for a particular participant.
function finalise(address _who)
public
when_not_halted
when_ended
only_buyins(_who)
{
// end the auction if we're the first one to finalise.
if (endPrice == 0) {
endPrice = totalAccounted / tokenCap;
Ended(endPrice);
}
// enact the purchase.
uint total = buyins[_who].accounted;
uint tokens = total / endPrice;
totalFinalised += total;
delete buyins[_who];
require (tokenContract.transfer(_who, tokens));
Finalised(_who, tokens);
if (totalFinalised == totalAccounted) {
Retired();
}
}
// Prviate utilities:
/// Ensure the era tracker is prepared in case the current changed.
function flushEra() private {
uint currentEra = (now - beginTime) / ERA_PERIOD;
if (currentEra > eraIndex) {
Ticked(eraIndex, totalReceived, totalAccounted);
}
eraIndex = currentEra;
}
// Admin interaction:
/// Emergency function to pause buy-in and finalisation.
function setHalted(bool _halted) public only_admin { halted = _halted; }
/// Emergency function to drain the contract of any funds.
function drain() public only_admin { treasury.transfer(this.balance); }
// Inspection:
/**
* The formula for the price over time.
*
* This is a hand-crafted formula (no named to the constants) in order to
* provide the following requirements:
*
* - Simple reciprocal curve (of the form y = a + b / (x + c));
* - Would be completely unreasonable to end in the first 48 hours;
* - Would reach $65m effective cap in 4 weeks.
*
* The curve begins with an effective cap (EC) of over $30b, more ether
* than is in existance. After 48 hours, the EC reduces to approx. $1b.
* At just over 10 days, the EC has reduced to $200m, and half way through
* the 19th day it has reduced to $100m.
*
* Here's the curve: https://www.desmos.com/calculator/k6iprxzcrg?embed
*/
/// The current end time of the sale assuming that nobody else buys in.
function calculateEndTime() public constant returns (uint) {
var factor = tokenCap / DIVISOR * USDWEI;
return beginTime + 40000000 * factor / (totalAccounted + 5 * factor) - 5760;
}
/// The current price for a single indivisible part of a token. If a buyin happens now, this is
/// the highest price per indivisible token part that the buyer will pay. This doesn't
/// include the discount which may be available.
function currentPrice() public constant when_active returns (uint weiPerIndivisibleTokenPart) {
return (USDWEI * 40000000 / (now - beginTime + 5760) - USDWEI * 5) / DIVISOR;
}
/// Returns the total indivisible token parts available for purchase right now.
function tokensAvailable() public constant when_active returns (uint tokens) {
uint _currentCap = totalAccounted / currentPrice();
if (_currentCap >= tokenCap) {
return 0;
}
return tokenCap - _currentCap;
}
/// The largest purchase than can be made at present, not including any
/// discount.
function maxPurchase() public constant when_active returns (uint spend) {
return tokenCap * currentPrice() - totalAccounted;
}
/// Get the number of `tokens` that would be given if the sender were to
/// spend `_value` now. Also tell you what `refund` would be given, if any.
function theDeal(uint _value)
public
constant
when_active
returns (uint accounted, bool refund, uint price)
{
uint _bonus = bonus(_value);
price = currentPrice();
accounted = _value + _bonus;
uint available = tokensAvailable();
uint tokens = accounted / price;
refund = (tokens > available);
}
/// Any applicable bonus to `_value`.
function bonus(uint _value)
public
constant
when_active
returns (uint extra)
{
return _value * uint(currentBonus) / 100;
}
/// True if the sale is ongoing.
function isActive() public constant returns (bool) { return now >= beginTime && now < endTime; }
/// True if all buyins have finalised.
function allFinalised() public constant returns (bool) { return now >= endTime && totalAccounted == totalFinalised; }
/// Returns true if the sender of this transaction is a basic account.
function isBasicAccount(address _who) internal constant returns (bool) {
uint senderCodeSize;
assembly {
senderCodeSize := extcodesize(_who)
}
return senderCodeSize == 0;
}
// Modifiers:
/// Ensure the sale is ongoing.
modifier when_active { require (isActive()); _; }
/// Ensure the sale has not begun.
modifier before_beginning { require (now < beginTime); _; }
/// Ensure the sale is ended.
modifier when_ended { require (now >= endTime); _; }
/// Ensure we're not halted.
modifier when_not_halted { require (!halted); _; }
/// Ensure `_who` is a participant.
modifier only_buyins(address _who) { require (buyins[_who].accounted != 0); _; }
/// Ensure sender is admin.
modifier only_admin { require (msg.sender == admin); _; }
/// Ensure that the signature is valid, `who` is a certified, basic account,
/// the gas price is sufficiently low and the value is sufficiently high.
modifier only_eligible(address who, uint8 v, bytes32 r, bytes32 s) {
require (
ecrecover(STATEMENT_HASH, v, r, s) == who &&
certifier.certified(who) &&
isBasicAccount(who) &&
msg.value >= DUST_LIMIT
);
_;
}
/// Ensure sender is not a contract.
modifier only_basic(address who) { require (isBasicAccount(who)); _; }
// State:
struct Account {
uint128 accounted; // including bonus & hit
uint128 received; // just the amount received, without bonus & hit
}
/// Those who have bought in to the auction.
mapping (address => Account) public buyins;
/// Total amount of ether received, excluding phantom "bonus" ether.
uint public totalReceived = 0;
/// Total amount of ether accounted for, including phantom "bonus" ether.
uint public totalAccounted = 0;
/// Total amount of ether which has been finalised.
uint public totalFinalised = 0;
/// The current end time. Gets updated when new funds are received.
uint public endTime;
/// The price per token; only valid once the sale has ended and at least one
/// participant has finalised.
uint public endPrice;
/// Must be false for any public function to be called.
bool public halted;
/// The current percentage of bonus that purchasers get.
uint8 public currentBonus = 15;
/// The last block that had a new participant.
uint32 public lastNewInterest;
// Constants after constructor:
/// The tokens contract.
Token public tokenContract;
/// The certifier.
Certifier public certifier;
/// The treasury address; where all the Ether goes.
address public treasury;
/// The admin address; auction can be paused or halted at any time by this.
address public admin;
/// The time at which the sale begins.
uint public beginTime;
/// Maximum amount of tokens to mint. Once totalAccounted / currentPrice is
/// greater than this, the sale ends.
uint public tokenCap;
// Era stuff (isolated)
/// The era for which the current consolidated data represents.
uint public eraIndex;
/// The size of the era in seconds.
uint constant public ERA_PERIOD = 5 minutes;
// Static constants:
/// Anything less than this is considered dust and cannot be used to buy in.
uint constant public DUST_LIMIT = 5 finney;
/// The hash of the statement which must be signed in order to buyin.
/// The meaning of this hash is:
///
/// parity.api.util.sha3(parity.api.util.asciiToHex("\x19Ethereum Signed Message:\n" + tscs.length + tscs))
/// where `toUTF8 = x => unescape(encodeURIComponent(x))`
/// and `tscs` is the toUTF8 called on the contents of https://gist.githubusercontent.com/gavofyork/5a530cad3b19c1cafe9148f608d729d2/raw/a116b507fd6d96036037f3affd393994b307c09a/gistfile1.txt
bytes32 constant public STATEMENT_HASH = 0x2cedb9c5443254bae6c4f44a31abcb33ec27a0bd03eb58e22e38cdb8b366876d;
/// Minimum duration after sale begins that bonus is active.
uint constant public BONUS_MIN_DURATION = 1 hours;
/// Minimum duration after sale begins that bonus is active.
uint constant public BONUS_MAX_DURATION = 24 hours;
/// Number of consecutive blocks where there must be no new interest before bonus ends.
uint constant public BONUS_LATCH = 2;
/// Number of Wei in one USD, constant.
uint constant public USDWEI = 3226 szabo;
/// Divisor of the token.
uint constant public DIVISOR = 1000;
}