forked from fetchai/staking-contract
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdutchStakingInterface.vy
675 lines (574 loc) · 28.9 KB
/
dutchStakingInterface.vy
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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
#------------------------------------------------------------------------------
#
# Copyright 2019 Fetch.AI Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#------------------------------------------------------------------------------
from vyper.interfaces import ERC20
units: {
tok: "smallest ERC20 token unit",
}
# maximum possible number of stakers a new auction can specify
MAX_SLOTS: constant(uint256) = 200
# number of blocks during which the auction remains open at reserve price
RESERVE_PRICE_DURATION: constant(uint256) = 25 # number of blocks
# number of seconds before deletion of the contract becomes possible after last lockupEnd() call
DELETE_PERIOD: constant(timedelta) = 60 * (3600 * 24)
# defining the decimals supported in pool rewards per token
REWARD_PER_TOK_DENOMINATOR: constant(uint256(tok)) = 100000
# Structs
struct Auction:
finalPrice: uint256(tok)
lockupEnd: uint256
slotsSold: uint256
start: uint256
end: uint256
startStake: uint256(tok)
reserveStake: uint256(tok)
declinePerBlock: uint256(tok)
slotsOnSale: uint256
rewardPerSlot: uint256(tok)
uniqueStakers: uint256
struct Pledge:
amount: uint256(tok)
AID: uint256
pool: address
struct Pool:
remainingReward: uint256(tok)
rewardPerTok: uint256(tok)
AID: uint256
struct VirtTokenHolder:
isHolder: bool
limit: uint256(tok)
rewards: uint256(tok)
# Events
Bid: event({AID: uint256, _from: indexed(address), currentPrice: uint256(tok), amount: uint256(tok)})
NewAuction: event({AID: uint256, start: uint256, end: uint256,
lockupEnd: uint256, startStake: uint256(tok), reserveStake: uint256(tok),
declinePerBlock: uint256(tok), slotsOnSale: uint256,
rewardPerSlot: uint256(tok)})
PoolRegistration: event({AID: uint256, _address: address,
maxStake: uint256(tok), rewardPerTok: uint256(tok)})
NewPledge: event({AID: uint256, _from: indexed(address), operator: address, amount: uint256(tok)})
IncreasedPledge: event({AID: uint256, _from: indexed(address), operator: address, topup: uint256(tok)})
AuctionFinalised: event({AID: uint256, finalPrice: uint256(tok), slotsSold: uint256(tok)})
LockupEnded: event({AID: uint256})
AuctionAborted: event({AID: uint256, rewardsPaid: bool})
SelfStakeWithdrawal: event({_from: indexed(address), amount: uint256(tok)})
PledgeWithdrawal: event({_from: indexed(address), amount: uint256(tok)})
# Contract state
token: ERC20
owner: public(address)
earliestDelete: public(timestamp)
# address -> uint256 Slots a staker has won in the current auction (cleared at endLockup())
stakerSlots: map(address, uint256)
# auction winners
stakers: address[MAX_SLOTS]
# pledged stake + committed pool reward, excl. selfStakerDeposit; pool -> deposits
poolDeposits: public(map(address, uint256(tok)))
# staker (through pool) -> Pledge{pool, amount}
pledges: public(map(address, Pledge))
# staker (directly) -> amount
selfStakerDeposits: public(map(address, uint256(tok)))
# staker (directly) -> price at which the bid was made
priceAtBid: public(map(address, uint256(tok)))
# pool address -> Pool
registeredPools: public(map(address, Pool))
# Auction details
currentAID: public(uint256)
auction: public(Auction)
totalAuctionRewards: public(uint256(tok))
# Virtual token management
virtTokenHolders: public(map(address, VirtTokenHolder))
################################################################################
# Constant functions
################################################################################
# @notice True from auction initialisation until either we hit the lower bound on being clear or
# the auction finalised through finaliseAuction()
@private
@constant
def _isBiddingPhase() -> bool:
return ((self.auction.lockupEnd > 0)
and (block.number < self.auction.end)
and (self.auction.slotsSold < self.auction.slotsOnSale)
and (self.auction.finalPrice == 0))
# @notice Returns true if either the auction has been finalised or the lockup has ended
# @dev self.auction will be cleared in endLockup() call
# @dev reserveStake > 0 condition in initialiseAuction() guarantees that finalPrice = 0 can never be
# a valid final price
@private
@constant
def _isFinalised() -> bool:
return (self.auction.finalPrice > 0) or (self.auction.lockupEnd == 0)
# @notice Calculate the scheduled, linearly declining price of the dutch auction
@private
@constant
def _getScheduledPrice() -> uint256(tok):
startStake_: uint256(tok) = self.auction.startStake
start: uint256 = self.auction.start
if (block.number <= start):
return startStake_
else:
# do not calculate max(startStake - decline, reserveStake) as that could throw on negative startStake - decline
decline: uint256(tok) = min(self.auction.declinePerBlock * (block.number - start),
startStake_ - self.auction.reserveStake)
return startStake_ - decline
# @notice Returns the scheduled price of the auction until the auction is finalised. Then returns
# the final price.
# @dev Auction price declines linearly from auction.start over _duration, then
# stays at _reserveStake for RESERVE_PRICE_DURATION
# @dev Returns zero If no auction is in bidding or lock-up phase
@private
@constant
def _getCurrentPrice() -> (uint256(tok)):
if self._isFinalised():
return self.auction.finalPrice
else:
scheduledPrice: uint256(tok) = self._getScheduledPrice()
return scheduledPrice
# @notice Returns the lockup needed by an address that stakes directly
# @dev Will throw if _address is a bidder in current auction & auciton not yet finalised, as the
# slot number & price are not final yet
# @dev Calling endLockup() will clear all stakerSlots flags and thereby set the required
# lockups to 0 for all participants
@private
@constant
def _calculateSelfStakeNeeded(_address: address) -> uint256(tok):
selfStakeNeeded: uint256(tok) = 0
# these slots can be outdated if auction is not yet finalised / lockup hasn't ended yet
slotsWon: uint256 = self.stakerSlots[_address]
if slotsWon > 0:
assert self._isFinalised(), "Is bidder and auction not finalised yet"
poolDeposit: uint256(tok) = self.poolDeposits[_address]
currentPrice: uint256(tok) = self._getCurrentPrice()
if (slotsWon * currentPrice) > poolDeposit:
selfStakeNeeded += (slotsWon * currentPrice) - poolDeposit
return selfStakeNeeded
################################################################################
# Main functions
################################################################################
@public
def __init__(_ERC20Address: address):
self.owner = msg.sender
self.token = ERC20(_ERC20Address)
# @notice Owner can initialise new auctions
# @dev First auction starts with AID 1
# @dev Requires the transfer of _reward to the contract to be approved with the
# underlying ERC20 token
# @param _start: start of the price decay
# @param _startStake: initial auction price
# @param _reserveStake: lowest possible auction price >= 1
# @param _duration: duration over which the auction price declines. Total bidding
# duration is _duration + RESERVE_PRICE_DURATION
# @param _lockup_duration: number of blocks the lockup phase will last
# @param _slotsOnSale: size of the assembly in this cycle
# @param _reward: added to any remaining reward of past auctions
@public
def initialiseAuction(_start: uint256,
_startStake: uint256(tok),
_reserveStake: uint256(tok),
_duration: uint256,
_lockup_duration: uint256,
_slotsOnSale: uint256,
_reward: uint256(tok)):
assert msg.sender == self.owner, "Owner only"
assert _startStake > _reserveStake, "Invalid startStake"
assert (_slotsOnSale > 0) and (_slotsOnSale <= MAX_SLOTS), "Invald slot number"
assert _start >= block.number, "Start before current block"
# NOTE: _isFinalised() relies on this requirement
assert _reserveStake > 0, "Reserve stake has to be at least 1"
assert self.auction.lockupEnd == 0, "End current auction"
self.currentAID += 1
# Use integer-ceil() of the fraction with (+ _duration - 1)
declinePerBlock: uint256(tok) = (_startStake - _reserveStake + _duration - 1) / _duration
end: uint256 = _start + _duration + RESERVE_PRICE_DURATION
self.auction.start = _start
self.auction.end = end
self.auction.lockupEnd = end + _lockup_duration
self.auction.startStake = _startStake
self.auction.reserveStake = _reserveStake
self.auction.declinePerBlock = declinePerBlock
self.auction.slotsOnSale = _slotsOnSale
# Also acts as the last checked price in _updatePrice()
self.auction.finalPrice = 0
# add auction rewards
self.totalAuctionRewards += _reward
self.auction.rewardPerSlot = self.totalAuctionRewards / self.auction.slotsOnSale
success: bool = self.token.transferFrom(msg.sender, self, as_unitless_number(_reward))
assert success, "Transfer failed"
log.NewAuction(self.currentAID, _start, end, end + _lockup_duration, _startStake,
_reserveStake, declinePerBlock, _slotsOnSale, self.auction.rewardPerSlot)
# @notice Move unclaimed auction rewards back to the contract owner
# @dev Requires that no auction is in bidding or lockup phase
@public
def retrieveUndistributedAuctionRewards():
assert msg.sender == self.owner, "Owner only"
assert self.auction.lockupEnd == 0, "Auction ongoing"
undistributed: uint256(tok) = self.totalAuctionRewards
clear(self.totalAuctionRewards)
success: bool = self.token.transfer(self.owner, as_unitless_number(undistributed))
assert success, "Transfer failed"
# @notice Enter a bid into the auction. Requires the sender's deposits + _topup >= currentPrice or
# specify _topup = 0 to automatically calculate and transfer the topup needed to make a bid at the
# current price. Beforehand the sender must have approved the ERC20 contract to allow the transfer
# of at least the topup to the auction contract via ERC20.approve(auctionContract.address, amount)
# @param _topup: Set to 0 to bid current price (automatically calculating and transfering required topup),
# o/w it will be interpreted as a topup to the existing deposits
# @dev Only one bid per address and auction allowed, as time of bidding also specifies the priority
# in slot allocation
# @dev No bids below current auction price allowed
@public
def bid(_topup: uint256(tok)):
assert self._isBiddingPhase(), "Not in bidding phase"
assert self.stakerSlots[msg.sender] == 0, "Sender already bid"
_currentAID: uint256 = self.currentAID
currentPrice: uint256(tok) = self._getCurrentPrice()
_isVirtTokenHolder: bool = self.virtTokenHolders[msg.sender].isHolder
assert (_isVirtTokenHolder == False) or (_topup <= self.virtTokenHolders[msg.sender].limit), "Virtual tokens above limit"
# If pool: move unclaimed rewards and clear
if self.registeredPools[msg.sender].AID == _currentAID:
unclaimed: uint256(tok) = self.registeredPools[msg.sender].remainingReward
clear(self.registeredPools[msg.sender])
self.poolDeposits[msg.sender] -= unclaimed
self.selfStakerDeposits[msg.sender] += unclaimed
# if address was a pool in a previous auction and not the current one: reset poolDeposits
# do not rely on self.registeredPools[msg.sender].AID as this gets cleared at certain points
elif self.poolDeposits[msg.sender] > 0:
clear(self.poolDeposits[msg.sender])
totDeposit: uint256(tok) = self.poolDeposits[msg.sender] + self.selfStakerDeposits[msg.sender]
# cannot modify input argument
topup: uint256(tok) = _topup
if (currentPrice > totDeposit) and(_topup == 0):
topup = currentPrice - totDeposit
else:
assert totDeposit + topup >= currentPrice, "Bid below current price"
# Update deposits & stakers
self.priceAtBid[msg.sender] = currentPrice
self.selfStakerDeposits[msg.sender] += topup
slots: uint256 = min((totDeposit + topup) / currentPrice, self.auction.slotsOnSale - self.auction.slotsSold)
self.stakerSlots[msg.sender] = slots
self.auction.slotsSold += slots
self.stakers[self.auction.uniqueStakers] = msg.sender
self.auction.uniqueStakers += 1
# Transfer topup if necessary
if (topup > 0) and (_isVirtTokenHolder == False):
success: bool = self.token.transferFrom(msg.sender, self, as_unitless_number(topup))
assert success, "Transfer failed"
log.Bid(_currentAID, msg.sender, currentPrice, totDeposit + topup)
# @Notice Anyone can supply the correct final price to finalise the auction and calculate the number of slots each
# staker has won. Required before lock-up can be ended or withdrawals can be made
# @param finalPrice: proposed solution for the final price. Throws if not the correct solution
# @dev Allows to move the calculation of the price that clear the auction off-chain
@public
def finaliseAuction(finalPrice: uint256(tok)):
currentPrice: uint256(tok) = self._getCurrentPrice()
assert finalPrice >= currentPrice, "Suggested solution below current price"
assert self.auction.finalPrice == 0, "Auction already finalised"
assert self.auction.lockupEnd >= 0, "Lockup has already ended"
slotsOnSale: uint256 = self.auction.slotsOnSale
slotsRemaining: uint256 = slotsOnSale
slotsRemainingP1: uint256 = slotsOnSale
finalPriceP1: uint256(tok) = finalPrice + 1
uniqueStakers_int128: int128 = convert(self.auction.uniqueStakers, int128)
staker: address = ZERO_ADDRESS
totDeposit: uint256(tok) = 0
slots: uint256 = 0
currentSlots: uint256 = 0
_priceAtBid: uint256(tok)= 0
for i in range(MAX_SLOTS):
if i >= uniqueStakers_int128:
break
staker = self.stakers[i]
_priceAtBid = self.priceAtBid[staker]
slots = 0
if finalPrice <= _priceAtBid:
totDeposit = self.selfStakerDeposits[staker] + self.poolDeposits[staker]
if slotsRemaining > 0:
# finalPrice will always be > 0 as reserveStake required to be > 0
slots = min(totDeposit / finalPrice, slotsRemaining)
currentSlots = self.stakerSlots[staker]
if slots != currentSlots:
self.stakerSlots[staker] = slots
slotsRemaining -= slots
if finalPriceP1 <= _priceAtBid:
slotsRemainingP1 -= min(totDeposit / finalPriceP1, slotsRemainingP1)
# later bidders dropping out of slot-allocation as earlier bidders already claim all slots at the final price
if slots == 0:
clear(self.stakerSlots[staker])
clear(self.stakers[i])
if (finalPrice == self.auction.reserveStake) and (self._isBiddingPhase() == False):
# a) reserveStake clears the auction and reserveStake + 1 does not
doesClear: bool = (slotsRemaining == 0) and (slotsRemainingP1 > 0)
# b) reserveStake does not clear the auction, accordingly neither will any other higher price
assert (doesClear or (slotsRemaining > 0)), "reserveStake is not the best solution"
else:
assert slotsRemaining == 0, "finalPrice does not clear auction"
assert slotsRemainingP1 > 0, "Not largest price clearing the auction"
self.auction.finalPrice = finalPrice
self.auction.slotsSold = slotsOnSale - slotsRemaining
log.AuctionFinalised(self.currentAID, finalPrice, slotsOnSale - slotsRemaining)
# @notice Anyone can end the lock-up of an auction, thereby allowing everyone to
# withdraw their stakes and rewards. Auction must first be finalised through finaliseAuction().
@private
def _endLockup(payoutRewards: bool):
assert self.auction.lockupEnd > 0, "No lockup to end"
slotsSold: uint256 = self.auction.slotsSold
rewardPerSlot_: uint256(tok) = 0
self.earliestDelete = block.timestamp + DELETE_PERIOD
if payoutRewards:
assert self._isFinalised(), "Not finalised"
rewardPerSlot_ = self.auction.rewardPerSlot
self.totalAuctionRewards -= slotsSold * rewardPerSlot_
# distribute rewards & cleanup
staker: address = ZERO_ADDRESS
for i in range(MAX_SLOTS):
staker = self.stakers[i]
if staker == ZERO_ADDRESS:
break
if payoutRewards:
if self.virtTokenHolders[staker].isHolder:
self.virtTokenHolders[staker].rewards += self.stakerSlots[staker] * rewardPerSlot_
else:
self.selfStakerDeposits[staker] += self.stakerSlots[staker] * rewardPerSlot_
clear(self.stakerSlots[staker])
if self.virtTokenHolders[staker].isHolder:
clear(self.selfStakerDeposits[staker])
clear(self.stakers)
clear(self.auction)
@public
def endLockup():
# Prevents repeated calls of this function as self.auction will get reset here
assert self.auction.finalPrice > 0, "Auction not finalised yet or no auction to end"
assert block.number >= self.auction.lockupEnd, "Lockup not over"
self._endLockup(True)
log.LockupEnded(self.currentAID)
# @notice The owner can clear the auction and all recorded slots in the case of an emergency and
# thereby immediately lift any lockups and allow the immediate withdrawal of any made deposits.
# @param payoutRewards: whether rewards get distributed to bidders
@public
def abortAuction(payoutRewards: bool):
assert msg.sender == self.owner, "Owner only"
self._endLockup(payoutRewards)
log.AuctionAborted(self.currentAID, payoutRewards)
# @param AID: auction ID, has to match self.currentAID
# @param _totalReward: total reward committed to stakers, has to be paid upon
# calling this and be approved with the ERC20 token
# @param _rewardPerTok: _rewardPerTok / REWARD_PER_TOK_DENOMINATOR will be paid
# for each stake pledged to the pool. Meaning _rewardPerTok should equal
# reward per token * REWARD_PER_TOK_DENOMINATOR (see getDenominator())
@public
def registerPool(AID: uint256,
_totalReward: uint256(tok),
_rewardPerTok: uint256(tok)):
assert AID == self.currentAID, "Not current auction"
assert self._isBiddingPhase(), "Not in bidding phase"
assert self.registeredPools[msg.sender].AID < AID, "Pool already exists"
assert self.registeredPools[msg.sender].remainingReward == 0, "Unclaimed rewards"
assert self.virtTokenHolders[msg.sender].isHolder == False, "Not allowed for virtTokenHolders"
self.registeredPools[msg.sender] = Pool({remainingReward: _totalReward,
rewardPerTok: _rewardPerTok,
AID: AID})
# overwrite any poolDeposits that existed for the last auction
self.poolDeposits[msg.sender] = _totalReward
success: bool = self.token.transferFrom(msg.sender, self, as_unitless_number(_totalReward))
assert success, "Transfer failed"
maxStake: uint256(tok) = (_totalReward * REWARD_PER_TOK_DENOMINATOR) / _rewardPerTok
log.PoolRegistration(AID, msg.sender, maxStake, _rewardPerTok)
# @notice Move pool rewards that were not claimed by anyone into
# selfStakerDeposits. Automatically done if pool enters a bid.
# @dev Requires that the auction has passed the bidding phase
@public
def retrieveUnclaimedPoolRewards():
assert ((self._isBiddingPhase() == False)
or (self.registeredPools[msg.sender].AID < self.currentAID)), "Bidding phase of AID not over"
unclaimed: uint256(tok) = self.registeredPools[msg.sender].remainingReward
clear(self.registeredPools[msg.sender])
self.poolDeposits[msg.sender] -= unclaimed
self.selfStakerDeposits[msg.sender] += unclaimed
@private
def _updatePoolRewards(pool: address, newAmount: uint256(tok)) -> uint256(tok):
newReward: uint256(tok) = ((self.registeredPools[pool].rewardPerTok * newAmount)
/ REWARD_PER_TOK_DENOMINATOR)
assert self.registeredPools[pool].remainingReward >= newReward, "Rewards depleted"
self.registeredPools[pool].remainingReward -= newReward
return newReward
# @notice Pledge stake to a staking pool. Possible from auction intialisation
# until the end of the bidding phase or until the pool has made a bid.
# Stake from the last auction can be taken over to the next auction. If amount
# exceeds the previous stake, this contract must be approved with the ERC20 token
# to transfer the difference to this contract.
# @dev Only one pledge per address and auction allowed
# @dev If decreasing the pledge, the difference is immediately paid out
# @dev If the pool operator has already bid, this will throw with "Rewards depleted"
# @param AID: The auction ID
# @param pool: The address of the pool
# @param amount: The new total amount, not the difference to existing pledges. If increasing the
# pledge, this has to include the pool rewards of the initial pledge
@public
def pledgeStake(AID: uint256, pool: address, amount: uint256(tok)):
assert AID == self.currentAID, "Not current AID"
assert self._isBiddingPhase(), "Not in bidding phase"
assert self.registeredPools[pool].AID == AID, "Not a registered pool"
assert self.virtTokenHolders[msg.sender].isHolder == False, "Not allowed for virtTokenHolders"
existingPledgeAmount: uint256(tok) = self.pledges[msg.sender].amount
assert self.pledges[msg.sender].AID < AID, "Already pledged"
newReward: uint256(tok) = self._updatePoolRewards(pool, amount)
# overwriting any existing amount
self.pledges[msg.sender] = Pledge({amount: amount + newReward,
AID: AID,
pool: pool})
# pool reward is already added to poolDeposits during registerPool() call
self.poolDeposits[pool] += amount
if amount > existingPledgeAmount:
success: bool = self.token.transferFrom(msg.sender, self, as_unitless_number(amount - existingPledgeAmount))
assert success, "Transfer failed"
elif amount < existingPledgeAmount:
success: bool = self.token.transfer(msg.sender, as_unitless_number(existingPledgeAmount - amount))
assert success, "Transfer failed"
log.NewPledge(AID, msg.sender, pool, amount)
# @notice Increase an existing pledge in the current auction
# @dev Requires the auction to be in bidding phase and the pool to have enough rewards remaining
# @param pool: The address of the pool. Has to match the pool of the initial pledge
# @param topup: Value by which to increase the pledge
@public
def increasePledge(pool: address, topup: uint256(tok)):
AID: uint256 = self.currentAID
assert self._isBiddingPhase(), "Not in bidding phase"
assert self.pledges[msg.sender].AID == AID, "No pledge made in this auction yet"
assert self.pledges[msg.sender].pool == pool, "Cannot change pool"
newReward: uint256(tok) = self._updatePoolRewards(pool, topup)
self.pledges[msg.sender].amount += topup + newReward
self.poolDeposits[pool] += topup
success: bool = self.token.transferFrom(msg.sender, self, as_unitless_number(topup))
assert success, "Transfer failed"
log.IncreasedPledge(AID, msg.sender, pool, topup)
# @notice Withdraw any self-stake exceeding the required lockup. In case sender is a bidder in the
# current auction, this requires the auction to be finalised through finaliseAuction(),
# o/w _calculateSelfStakeNeeded() will throw
@public
def withdrawSelfStake() -> uint256(tok):
# not guaranteed to be initialised to 0 without setting it explicitly
withdrawal: uint256(tok) = 0
if self.virtTokenHolders[msg.sender].isHolder:
withdrawal = self.virtTokenHolders[msg.sender].rewards
clear(self.virtTokenHolders[msg.sender].rewards)
else:
selfStake: uint256(tok) = self.selfStakerDeposits[msg.sender]
selfStakeNeeded: uint256(tok) = self._calculateSelfStakeNeeded(msg.sender)
if selfStake > selfStakeNeeded:
withdrawal = selfStake - selfStakeNeeded
self.selfStakerDeposits[msg.sender] -= withdrawal
elif selfStake < selfStakeNeeded:
assert False, "Critical failure"
success: bool = self.token.transfer(msg.sender, as_unitless_number(withdrawal))
assert success, "Transfer failed"
log.SelfStakeWithdrawal(msg.sender, withdrawal)
return withdrawal
# @notice Withdraw pledged stake after the lock-up has ended
@public
def withdrawPledgedStake() -> uint256(tok):
withdrawal: uint256(tok) = 0
if ((self.pledges[msg.sender].AID < self.currentAID)
or (self.auction.lockupEnd == 0)):
withdrawal += self.pledges[msg.sender].amount
clear(self.pledges[msg.sender])
success: bool = self.token.transfer(msg.sender, as_unitless_number(withdrawal))
assert success, "Transfer failed"
log.PledgeWithdrawal(msg.sender, withdrawal)
return withdrawal
# @notice Allow the owner to remove the contract, given that no auction is
# active and at least DELETE_PERIOD blocks have past since the last lock-up end.
@public
def deleteContract():
assert msg.sender == self.owner, "Owner only"
assert self.auction.lockupEnd == 0, "In lockup phase"
assert block.timestamp >= self.earliestDelete, "earliestDelete not reached"
contractBalance: uint256 = self.token.balanceOf(self)
success: bool = self.token.transfer(self.owner, contractBalance)
assert success, "Transfer failed"
selfdestruct(self.owner)
# @notice Allow the owner to set virtTokenHolder status for addresses, allowing them to participate
# with virtual tokens
# @dev Throws if the address has existing selfStakerDeposits, active slots, a registered pool for
# this auction, unretrieved pool rewards or existing pledges
# @param _address: address for which to set the value
# @param _isVirtTokenHolder: new value indicating whether isVirtTokenHolder or not
# @param preserveRewards: if setting isVirtTokenHolder to false and that address still has remaining rewards:
# whether to move those rewards into selfStakerDeposits or to add them back to the control of the owner
# by adding them to totalAuctionRewards
@public
def setVirtTokenHolder(_address: address, _isVirtTokenHolder: bool, limit: uint256(tok), preserveRewards: bool):
assert msg.sender == self.owner, "Owner only"
assert self.stakerSlots[_address] == 0, "Address has active slots"
assert self.selfStakerDeposits[_address] == 0, "Address has positive selfStakerDeposits"
assert self.registeredPools[_address].remainingReward == 0, "Address has remainingReward"
assert self.pledges[_address].amount == 0, "Address has positive pledges"
assert (self.registeredPools[_address].AID < self.currentAID) or (self.auction.finalPrice == 0), "Address has a pool in ongoing auction"
existingRewards: uint256(tok) = self.virtTokenHolders[_address].rewards
if (_isVirtTokenHolder == False) and (existingRewards > 0):
if preserveRewards:
self.selfStakerDeposits[_address] += existingRewards
else:
self.totalAuctionRewards += existingRewards
clear(self.virtTokenHolders[_address].rewards)
self.virtTokenHolders[_address].isHolder = _isVirtTokenHolder
self.virtTokenHolders[_address].limit = limit
@public
def setVirtTokenLimit(_address: address, _virtTokenLimit: uint256(tok)):
assert msg.sender == self.owner, "Owner only"
assert self.virtTokenHolders[_address].isHolder, "Not a virtTokenHolder"
self.virtTokenHolders[_address].limit = _virtTokenLimit
################################################################################
# Getters
################################################################################
@public
@constant
def getERC20Address() -> address:
return self.token
@public
@constant
def getDenominator() -> uint256(tok):
return REWARD_PER_TOK_DENOMINATOR
@public
@constant
def getFinalStakerSlots(staker: address) -> uint256:
assert self._isFinalised(), "Slots not yet final"
return self.stakerSlots[staker]
# @dev Always returns an array of MAX_SLOTS with elements > unique bidders = zero
@public
@constant
def getFinalStakers() -> address[MAX_SLOTS]:
assert self._isFinalised(), "Stakers not yet final"
return self.stakers
@public
@constant
def getFinalSlotsSold() -> uint256:
assert self._isFinalised(), "Slots not yet final"
return self.auction.slotsSold
@public
@constant
def isBiddingPhase() -> bool:
return self._isBiddingPhase()
@public
@constant
def isFinalised() -> bool:
return self._isFinalised()
@public
@constant
def getCurrentPrice() -> uint256(tok):
return self._getCurrentPrice()
@public
@constant
def calculateSelfStakeNeeded(_address: address) -> uint256(tok):
return self._calculateSelfStakeNeeded(_address)