-
Notifications
You must be signed in to change notification settings - Fork 0
/
CompoundContract.py
848 lines (736 loc) · 35.7 KB
/
CompoundContract.py
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
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
import math
from pyteal import *
from typing import Literal
from util import *
from src.config import *
from algosdk.v2client import algod
# ----- Address of Staking Contract -----
SC_address = AppParam.address(App.globalGet(CC_SC_ID))
# ----- Subroutines -----
# floor_local_stake() -> Expr:
# Get rounded down integer amount of user's local stake
#
@Subroutine(TealType.uint64)
def floor_local_stake() -> Expr:
# Variable for storing the local stake
ls = ScratchVar()
return Seq(
ls.store(App.localGet(Txn.sender(), CC_local_stake)),
If(
Len(ls.load()) > Int(LOCAL_STAKE_N)
).Then(
Btoi(Extract(ls.load(), Int(0), Len(ls.load()) - Int(LOCAL_STAKE_N)))
).Else(
Int(0)
)
)
# closeAccountTo(account: Expr) -> Expr:
# Sends remaining balance of the application account to a specified account, i.e. it closes the application account.
# Fee for the inner transaction is set to zero, thus fee pooling needs to be used.
#
@Subroutine(TealType.none)
def closeAccountTo(account: Expr) -> Expr:
return If(Balance(Global.current_application_address()) != Int(0)).Then(
Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.fee: Int(0),
TxnField.type_enum: TxnType.Payment,
TxnField.close_remainder_to: account,
}
),
InnerTxnBuilder.Submit(),
)
)
# payTo(account: Expr, amount: Expr) -> Expr:
# Sends a payment transaction of amount to account
#
@Subroutine(TealType.none)
def payTo(account: Expr, amount: Expr) -> Expr:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.fee: Int(0),
TxnField.type_enum: TxnType.Payment,
TxnField.receiver: account,
TxnField.amount: amount
}
),
InnerTxnBuilder.Submit(),
)
# closeAssetToCreator() -> Expr:
# Sends whole amount of S_ASA_ID to CC creator
#
@Subroutine(TealType.none)
def closeAssetToCreator() -> Expr:
return Seq(
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.AssetTransfer,
TxnField.xfer_asset: App.globalGet(CC_S_ASA_ID),
TxnField.asset_close_to: Global.creator_address(),
TxnField.fee: Int(0),
}
)
)
# stake_to_SC(amt: Expr, payFee: Expr) -> Expr:
# Issue app call to SC to stake additional amount amt
# If payFee == PAY_FEE, CC.address will pay the fee for the staking operation. Otherwise, the fee needs to be pooled.
#
@Subroutine(TealType.none)
def stake_to_SC(amt: Expr, payFee: Expr) -> Expr:
return Seq(
# Assert address of SC
SC_address,
Assert(SC_address.hasValue()),
# Stake to SC
InnerTxnBuilder.Begin(),
# First create an asset transfer transaction
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.AssetTransfer,
TxnField.xfer_asset: App.globalGet(CC_S_ASA_ID),
TxnField.asset_receiver: SC_address.value(),
TxnField.asset_amount: amt,
TxnField.fee: Int(0),
}
),
InnerTxnBuilder.Next(),
# Then create an app call to stake
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.application_id: App.globalGet(CC_SC_ID),
TxnField.applications: [App.globalGet(CC_AC_ID)],
TxnField.assets: [App.globalGet(CC_S_ASA_ID)],
TxnField.accounts: [SC_address.value()],
TxnField.on_completion: OnComplete.NoOp,
TxnField.application_args: [
Bytes("base64", "AA=="),
Bytes("base64", "Aw=="),
Bytes("base64", "AAAAAAAAAAA="),
Concat(Bytes("base16", "0x02"), Itob(amt)),
],
TxnField.fee: If(
payFee == Int(PAY_FEE),
).Then(
Int(STAKE_TO_SC_FEE),
).Else(
Int(0),
)
}
),
# Submit the created group transaction
InnerTxnBuilder.Submit(),
)
# claim_stake_record(amt: Expr, payFee: Expr) -> Expr:
# First, claim rewards from SC. Then create the recording of the claimed amount by storing it in a newly created box.
# Lastly, stake the claimed amount plus any additional amount to SC, and update the total stake as well as the last
# compound round.
# If payFee == PAY_FEE, CC.address will pay the fee for the operations. Otherwise, the fee needs to be pooled.
#
@Subroutine(TealType.none)
def claim_stake_record(amt: Expr, payFee: Expr) -> Expr:
# Variable for storing the claimed amount
claim_amt = ScratchVar()
# Variable for storing the amount to stake
stake_amt = ScratchVar()
# Boxes are sequentially numbered
box_name = Itob(App.globalGet(CC_number_of_boxes))
# Amount of increase: 1 + (claim_amt / total stake)
increase = BytesAdd(
Concat(Itob(Int(1)), BytesZero(Int(LOCAL_STAKE_N))),
BytesDiv(
Concat(Itob(claim_amt.load()), BytesZero(Int(LOCAL_STAKE_N))),
Concat(BytesZero(Int(LOCAL_STAKE_N)), Itob(App.globalGet(CC_total_stake)))
)
)
return Seq(
# Claiming makes sense only if current total stake was non-zero
Assert(App.globalGet(CC_total_stake) > Int(0)),
# Assert address of SC
SC_address,
Assert(SC_address.hasValue()),
# Claim from SC
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.application_id: App.globalGet(CC_SC_ID),
TxnField.applications: [App.globalGet(CC_AC_ID)],
TxnField.assets: [App.globalGet(CC_S_ASA_ID)],
TxnField.accounts: [SC_address.value()],
TxnField.on_completion: OnComplete.NoOp,
TxnField.application_args: [
Bytes("base64", "AA=="),
Bytes("base64", "Aw=="),
Bytes("base64", "AAAAAAAAAAA="),
Bytes("base64", "AAAAAAAAAAAA"),
],
TxnField.fee: If(
payFee == Int(PAY_FEE)
).Then(
Int(CLAIM_FROM_SC_FEE),
).Else(
Int(0),
)
}
),
# Store the claimed amount, which is written in the last log of the call claim from SC, 8 bytes starting from
# byte 16
claim_amt.store(Btoi(Extract(InnerTxn.last_log(), Int(16), Int(8)))),
# Increase the counter of boxes created
App.globalPut(CC_number_of_boxes, App.globalGet(CC_number_of_boxes) + Int(1)),
# Create a new box with name equal to the number of boxes and populate it with the increase from this
# compounding
App.box_put(box_name, increase),
# Stake the claimed amount plus any additional stake - if they are non-zero
stake_amt.store(claim_amt.load() + amt),
If(stake_amt.load() > Int(0)).Then(
stake_to_SC(stake_amt.load(), payFee),
# Update the total stake in CC
App.globalPut(CC_total_stake, App.globalGet(CC_total_stake) + stake_amt.load()),
),
# Update the round of last compound to current round
App.globalPut(CC_last_compound_round, Global.round())
)
# unstake_from_SC(amt: Expr, payFee: Expr) -> Expr:
# Issue app call to SC to unstake amount amt
# If payFee == PAY_FEE, CC.address will pay the fee for the unstaking operation. Otherwise, the fee needs to be pooled.
#
@Subroutine(TealType.none)
def unstake_from_SC(amt: Expr, payFee: Expr) -> Expr:
return Seq(
# Assert address of SC
SC_address,
Assert(SC_address.hasValue()),
# Unstake from SC
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.application_id: App.globalGet(CC_SC_ID),
TxnField.applications: [App.globalGet(CC_AC_ID)],
TxnField.assets: [App.globalGet(CC_S_ASA_ID)],
TxnField.accounts: [SC_address.value()],
TxnField.on_completion: OnComplete.NoOp,
TxnField.application_args: [
Bytes("base64", "AA=="),
Bytes("base64", "Aw=="),
Bytes("base64", "AAAAAAAAAAA="),
Concat(Bytes("base16", "0x03"), Itob(amt)),
],
TxnField.fee: If(
payFee == Int(PAY_FEE)
).Then(
Int(UNSTAKE_FROM_SC_FEE),
).Else(
Int(0),
)
}
),
)
# sendAssetToSender(amt: Expr) -> Expr:
# Sends amount amt of S_ASA_ID to Txn.sender()
#
@Subroutine(TealType.none)
def sendAssetToSender(amt: Expr) -> Expr:
return Seq(
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.AssetTransfer,
TxnField.xfer_asset: App.globalGet(CC_S_ASA_ID),
TxnField.asset_amount: amt,
TxnField.asset_receiver: Txn.sender(),
TxnField.fee: Int(0),
}
)
)
# local_claim_box(box_i: Expr) -> Expr:
# Adds amount received by user from a compounding that was done and recorded in box
#
@Subroutine(TealType.none)
def local_claim_box(box_int: Expr) -> Expr:
# Box name = sequential number of the box (uint64)
box_name = Itob(box_int)
# Box contents
contents = App.box_get(box_name)
# Get increase from the box
increase = contents.value()
return Seq(
# Assert the box already exists
contents,
Assert(contents.hasValue()),
# Claims must be done in a strictly increasing order, without skipping any claim.
# This is checked by asserting the local number of boxes is one smaller than the current box_int
# This is needed to be able to track what has already been (locally) claimed and what not, without having to
# record this info explicitly for each user.
Assert(box_int == App.localGet(Txn.sender(), CC_local_number_of_boxes) + Int(1)),
# Increase local stake for the contribution of the user to the time the compounding has been done,
# i.e. += (box[round].claimed * LS) / box[round].total_stake_at_time_of_compounding, which equals
# *= box[round].increase
App.localPut(Txn.sender(), CC_local_stake, BytesDiv(
BytesMul(App.localGet(Txn.sender(), CC_local_stake), increase),
Concat(Itob(Int(1)), BytesZero(Int(LOCAL_STAKE_N)))
)
),
# Update local number of boxes
App.localPut(Txn.sender(), CC_local_number_of_boxes, box_int)
)
# ----- On delete -----
on_delete = Seq(
# Only the contract creator can delete the contract
Assert(Txn.sender() == Global.creator_address()),
# Only when there are no more accounts opted into the CC and the pool has ended, or the claiming period has passed
Assert(
Or(
And(App.globalGet(CC_number_of_stakers) == Int(0), Global.round() > App.globalGet(CC_pool_end_round)),
Global.round() > (App.globalGet(CC_pool_end_round) + App.globalGet(CC_claiming_period))
)
),
# Only when all boxes were deleted
Assert(App.globalGet(CC_number_of_boxes) == Int(0)),
# Ensure all funds have either already been claimed from SC to CC or do it now
If(App.globalGet(CC_last_compound_done) == Int(LAST_COMPOUND_NOT_DONE)).Then(
Seq(
# Make a claim to SC
# Assert address of SC
SC_address,
Assert(SC_address.hasValue()),
# Claim from SC - fees should be pooled
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.application_id: App.globalGet(CC_SC_ID),
TxnField.applications: [App.globalGet(CC_AC_ID)],
TxnField.assets: [App.globalGet(CC_S_ASA_ID)],
TxnField.accounts: [SC_address.value()],
TxnField.on_completion: OnComplete.NoOp,
TxnField.application_args: [
Bytes("base64", "AA=="),
Bytes("base64", "Aw=="),
Bytes("base64", "AAAAAAAAAAA="),
Bytes("base64", "AAAAAAAAAAAA"),
],
TxnField.fee: Int(0),
}
),
# Unstake total stake from SC - fees should be pooled
unstake_from_SC(App.globalGet(CC_total_stake), Int(DO_NOT_PAY_FEE)),
)
),
# Close all S_ASA_ID to the CC creator
closeAssetToCreator(),
# Clear state of CC in SC
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.on_completion: OnComplete.ClearState,
TxnField.application_id: App.globalGet(CC_SC_ID),
TxnField.fee: Int(0),
}
),
# Close the contract account to the CC creator
closeAccountTo(Global.creator_address()),
Approve(),
)
# ----- On close out -----
on_close_out = Seq(
# Allow opting out only if user has withdrawn all (integer part) of the stake - otherwise funds would be lost
Assert(floor_local_stake() == Int(0)),
# Reduce number of opted-in accounts
App.globalPut(CC_number_of_stakers, App.globalGet(CC_number_of_stakers) - Int(1)),
Approve()
)
# ----- On opt in -----
on_opt_in = Seq(
# Opt-ins are allowed only until the pool is live
Assert(App.globalGet(CC_pool_end_round) > Global.round()),
# Opt-ins are allowed only if the contract has already been setup - which is reflected in last compound round
Assert(App.globalGet(CC_last_compound_round) > Int(0)),
# Initialize local state
App.localPut(Txn.sender(), CC_local_number_of_boxes, App.globalGet(CC_number_of_boxes)),
App.localPut(Txn.sender(), CC_local_stake, LOCAL_STAKE_ZERO_BYTES),
# Increase the number of opted-in accounts
App.globalPut(CC_number_of_stakers, App.globalGet(CC_number_of_stakers) + Int(1)),
Approve()
)
# ----- Clear state -----
clear_state = Seq(
# Note: User will forfeit their stake
# Reduce number of opted-in accounts
App.globalPut(CC_number_of_stakers, App.globalGet(CC_number_of_stakers) - Int(1)),
Approve()
)
# ----- Calculation of next round to compound -----
number_of_triggers = (Balance(Global.current_application_address()) - MinBalance(Global.current_application_address()))/Int(CC_FEE_FOR_COMPOUND)
next_compound_round = (App.globalGet(CC_pool_end_round) - App.globalGet(CC_last_compound_round))/number_of_triggers + App.globalGet(CC_last_compound_round)
# ----- Router -----
def getRouter():
# Main router class
router = Router(
# Name of the contract
"CompoundContract",
# What to do for each on-complete type when no arguments are passed (bare call)
BareCallActions(
# Updating the contract is not allowed
update_application=OnCompleteAction.always(Reject()),
# Deleting the contract is allowed in certain cases
delete_application=OnCompleteAction.call_only(on_delete),
# Closing out is allowed in certain cases
close_out=OnCompleteAction.call_only(on_close_out),
# Clearing the state is discouraged because it will result in loss of funds
clear_state=OnCompleteAction.call_only(clear_state),
# Opt-in is always allowed but requires some logic to execute first
opt_in=OnCompleteAction.call_only(on_opt_in),
),
)
@router.method(no_op=CallConfig.CREATE)
def create_app(SC_ID: abi.Uint64, AC_ID: abi.Uint64, claimPeriod: abi.Uint64):
# Get global state of SC at key value of 0x00
SC_glob_state = App.globalGetEx(App.globalGet(CC_SC_ID), Bytes("base64", "AA=="))
return Seq(
# Set global variables
App.globalPut(CC_SC_ID, SC_ID.get()),
App.globalPut(CC_AC_ID, AC_ID.get()),
App.globalPut(CC_claiming_period, claimPeriod.get()),
# Fetch start round for the pool from the SC
# Assert SC has a global state
SC_glob_state,
Assert(SC_glob_state.hasValue()),
# Assign 8 bytes starting at byte 56 as start round
App.globalPut(CC_pool_start_round, Btoi(Extract(SC_glob_state.value(), Int(56), Int(8)))),
# Fetch end round for the pool from the SC
# Assign 8 bytes starting at byte 64 as end round
App.globalPut(CC_pool_end_round, Btoi(Extract(SC_glob_state.value(), Int(64), Int(8)))),
# Fetch asset of the pool from the SC
# Assign 8 bytes starting at byte 48 as ASA ID
App.globalPut(CC_S_ASA_ID, Btoi(Extract(SC_glob_state.value(), Int(48), Int(8)))),
# Initialize remaining global variables
App.globalPut(CC_total_stake, Int(0)),
App.globalPut(CC_last_compound_done, Int(LAST_COMPOUND_NOT_DONE)),
App.globalPut(CC_last_compound_round, Int(0)),
App.globalPut(CC_number_of_stakers, Int(0)),
App.globalPut(CC_number_of_boxes, Int(0)),
Approve()
)
@router.method(no_op=CallConfig.CALL)
def on_setup():
return Seq(
# Assert sender is contract creator - only one that can setup the contract
Assert(Txn.sender() == Global.creator_address()),
# Assert last compounded round is zero - only at the start (i.e. setup can be done only once)
Assert(App.globalGet(CC_last_compound_round) == Int(0)),
# Assign start of pool as the last time compounding took place, thus it can't be done before it's meaningful
App.globalPut(CC_last_compound_round, App.globalGet(CC_pool_start_round)),
# Opt-in to S_ASA_ID
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.AssetTransfer,
TxnField.xfer_asset: App.globalGet(CC_S_ASA_ID),
TxnField.asset_receiver: Global.current_application_address(),
TxnField.fee: Int(0),
}
),
# Opt-in to SC_ID
InnerTxnBuilder.Execute(
{
TxnField.type_enum: TxnType.ApplicationCall,
TxnField.on_completion: OnComplete.OptIn,
TxnField.application_id: App.globalGet(CC_SC_ID),
TxnField.fee: Int(0),
}
),
# Approve the call
Approve(),
)
@router.method(no_op=CallConfig.CALL)
def trigger_compound():
return Seq(
# Compounding can be done only if enough time has passed since last compounding
Assert(next_compound_round <= Global.round(), comment="Trigger compounding"),
# Compounding does not make sense if the pool is not yet live - prevents also box creation prior to PSR
Assert(Global.round() > App.globalGet(CC_pool_start_round), comment="Trigger compounding - pool live"),
# Claim from SC and record the claiming in a box, without adding any additional stake. The fee is paid by CC
claim_stake_record(Int(0), Int(PAY_FEE)),
# Approve the call
Approve(),
)
@router.method(no_op=CallConfig.CALL)
def stake():
# The request to stake must be accompanied by a payment transaction to deposit funds to cover the fees for at
# least one compounding
pay_txn_idx = Txn.group_index() - Int(2)
# Amount for payment of compounding fees
amt = Gtxn[pay_txn_idx].amount()
# The request to stake must be accompanied by a transaction transferring the amount of S_ASA_ID to be staked
xfer_txn_idx = Txn.group_index() - Int(1)
# Amount of S_ASA_ID transferred to be staked
amt_xfer = Gtxn[xfer_txn_idx].asset_amount()
return Seq(
# Deposits are allowed only if pool is still live
Assert(Global.round() < App.globalGet(CC_pool_end_round)),
# Staking is allowed only if user has either:
# claimed all compounding contributions (otherwise local contributions would not be correctly reflected) or
# user has currently a zero local stake, in which case the local number of boxes must be updated
If(App.localGet(Txn.sender(), CC_local_number_of_boxes) == App.globalGet(CC_number_of_boxes)).Then(
Assert(Int(1), comment="boxes up-to-date, user can stake")
).Else(
If(BytesEq(App.localGet(Txn.sender(), CC_local_stake), LOCAL_STAKE_ZERO_BYTES)).Then(
Seq(
App.localPut(Txn.sender(), CC_local_number_of_boxes, App.globalGet(CC_number_of_boxes)),
Assert(Int(1), comment="user can stake since it has zero stake")
)
).Else(
Reject()
)
),
# The request to stake must be accompanied by a payment transaction to deposit funds to cover the fees for
# at least one compounding
# Assert transaction is payment
Assert(Gtxn[pay_txn_idx].type_enum() == TxnType.Payment),
# Assert transaction receiver is CC.address
Assert(Gtxn[pay_txn_idx].receiver() == Global.current_application_address()),
# The request to stake must be accompanied by a transaction transferring the amount to be staked
# Assert transaction is asset transfer
Assert(Gtxn[xfer_txn_idx].type_enum() == TxnType.AssetTransfer),
# Assert transaction receiver is CC.address
Assert(Gtxn[xfer_txn_idx].asset_receiver() == Global.current_application_address()),
# Assert transaction is transferring correct asset (redundant since opted-in only one asset, thus others
# would fail)
Assert(Gtxn[xfer_txn_idx].xfer_asset() == App.globalGet(CC_S_ASA_ID)),
# If request to stake is done when the pool is already live and a stake has already been deposited, it is
# necessary to claim the amount first. For this claiming, the new staker needs to pay the fees, including
# additional deposit for the newly created box while claiming.
If(
And(
Global.round() > App.globalGet(CC_pool_start_round),
App.globalGet(CC_total_stake) > Int(0)
)
).Then(
Seq(
# Assert the payment transferred is enough to cover the fees for this initial compounding due to
# staking plus for another trigger (at any later point)
Assert(amt >= Int(2 * CC_FEE_FOR_COMPOUND), comment="staking while pool live, stake already deposited"),
# Claim from SC and record the claiming in a box, while adding the newly deposited additional stake.
# Everything also get recorded in total stake.
claim_stake_record(amt_xfer, Int(PAY_FEE)),
# Local claim the results of this compounding (it is still with respect to user's old stake)
local_claim_box(App.globalGet(CC_number_of_boxes)),
)
).Else(
Seq(
# Assert the payment transferred is enough to cover the fees for the transfer to SC plus for a
# compound trigger (at any later point)
Assert(amt >= Int(CC_FEE_FOR_COMPOUND + STAKE_TO_SC_FEE)),
# Stake the deposited amount
stake_to_SC(amt_xfer, Int(PAY_FEE)),
# Update the total stake
App.globalPut(CC_total_stake, App.globalGet(CC_total_stake) + amt_xfer),
)
),
# Update the local stake
App.localPut(
Txn.sender(),
CC_local_stake,
BytesAdd(
App.localGet(Txn.sender(), CC_local_stake),
Concat(Itob(amt_xfer), BytesZero(Int(LOCAL_STAKE_N))),
)
),
# Approve the call
Approve(),
)
@router.method(no_op=CallConfig.CALL)
def compound_now():
# To compound now, a payment transaction needs to deposit funds to cover the fees for the compounding
pay_txn_idx = Txn.group_index() - Int(1)
# Amount for payment of compounding fees
amt = Gtxn[pay_txn_idx].amount()
return Seq(
# Makes sense to allow additional compounding only when the pool is live
Assert(Global.round() < App.globalGet(CC_pool_end_round)),
Assert(Global.round() > App.globalGet(CC_pool_start_round)),
# The request to stake must be accompanied by a payment transaction to deposit funds to cover the fees for
# the compounding
# Assert transaction is payment
Assert(Gtxn[pay_txn_idx].type_enum() == TxnType.Payment),
# Assert transaction receiver is CC.address
Assert(Gtxn[pay_txn_idx].receiver() == Global.current_application_address()),
# Assert the payment transferred is enough to cover the fees for the compounding
Assert(amt >= Int(CC_FEE_FOR_COMPOUND)),
# Claim from SC and record the claiming in a box (do not add additional stake).
claim_stake_record(Int(0), Int(PAY_FEE)),
# Approve the call
Approve(),
)
@router.method(no_op=CallConfig.CALL)
def withdraw(amt: abi.Uint64, *, output: abi.Uint64) -> Expr:
# The request to unstake must be accompanied by a payment transaction to deposit funds to cover the fees
# This could be optimized depending on when the unstaking is done, fees can be simply pooled.
pay_txn_idx = Txn.group_index() - Int(1)
# Amount for payment of fees
amt_fee = Gtxn[pay_txn_idx].amount()
# For storing user's local state at the start of the call (since it can increase due to claiming)
local_stake_b = ScratchVar()
# For storing amount which user actually gets withdraw - which can be higher than amt if another claiming has to
# be done
amt_b = ScratchVar()
return Seq(
# Withdrawals are allowed only if user has claimed all compounding contributions - otherwise one could lose
# funds (i.e. give up the rewards)
Assert(App.localGet(Txn.sender(), CC_local_number_of_boxes) == App.globalGet(CC_number_of_boxes)),
# The request to unstake must be accompanied by a payment transaction to deposit funds to cover the fees
# Assert transaction is payment
Assert(Gtxn[pay_txn_idx].type_enum() == TxnType.Payment),
# Assert transaction receiver is CC.address
Assert(Gtxn[pay_txn_idx].receiver() == Global.current_application_address()),
# Get user's local state (rounded down) - since it can increase due to claiming later on
local_stake_b.store(floor_local_stake()),
# Requested withdrawal can be at most up to the local stake
Assert(amt.get() <= local_stake_b.load()),
# Withdrawals are processed differently depending on when they are done
If(Global.round() < App.globalGet(CC_pool_start_round)).Then(
Seq(
# If pool has not yet started, there has been no compounding done so far, thus simply ustake the
# requested amount from SC
unstake_from_SC(amt.get(), Int(PAY_FEE)),
# That amount will be withdrawn
amt_b.store(amt.get()),
# Assert fees for the unstaking have been deposited
Assert(amt_fee >= Int(UNSTAKE_FROM_SC_FEE)),
)
).Else(
# If pool is still live
If(Global.round() <= App.globalGet(CC_pool_end_round)).Then(
Seq(
# Claim from SC and record the claiming in a box, without adding any additional stake.
# Everything also get recorded in total stake.
claim_stake_record(Int(0), Int(PAY_FEE)),
# Local claim the results of this compounding
local_claim_box(App.globalGet(CC_number_of_boxes)),
# Unstake correct amount from SC
# If withdraw amt equaled the whole local stake, interpret as a request to withdraw also the
# effect of the last claim - which requires to call floor_local_stake() again for the unstaking
If(amt.get() == local_stake_b.load()).Then(
amt_b.store(floor_local_stake()),
).Else(
amt_b.store(amt.get()),
),
unstake_from_SC(amt_b.load(), Int(PAY_FEE)),
# Assert fees for the compounding and unstaking have been deposited
Assert(amt_fee >= Int(CC_FEE_FOR_COMPOUND + UNSTAKE_FROM_SC_FEE)),
)
).Else(
# If pool has already ended, a last compounding has to be done and all funds can be withdrawn from
# the pool to CC.address
If(App.globalGet(CC_last_compound_done) == Int(LAST_COMPOUND_NOT_DONE)).Then(
Seq(
# Claim from SC and record the claiming in a box, without adding any additional stake.
# Everything also get recorded in total stake.
claim_stake_record(Int(0), Int(PAY_FEE)),
# Local claim the results of this compounding
local_claim_box(App.globalGet(CC_number_of_boxes)),
# If withdraw amt equaled the whole local stake, interpret as a request to withdraw also
# the effect of the last claim - which requires to call floor_local_stake() again
If(amt.get() == local_stake_b.load()).Then(
amt_b.store(floor_local_stake()),
).Else(
amt_b.store(amt.get()),
),
# Unstake total stake from SC
unstake_from_SC(App.globalGet(CC_total_stake), Int(PAY_FEE)),
# Assert fees for the compounding and unstaking have been deposited
Assert(amt_fee >= Int(CC_FEE_FOR_COMPOUND + UNSTAKE_FROM_SC_FEE)),
# Mark that the last compounding has now been done
App.globalPut(CC_last_compound_done, Int(LAST_COMPOUND_DONE)),
)
)
.Else(
# If pool has already ended and a last compounding has already been done, stake can simply be
# withdrawn from CC.address since it’s already there.
# The amount to withdraw is simply the requested stake since no additional claiming happened
amt_b.store(amt.get())
)
)
),
# Send the requested amount (in case of a full withdrawal, also the possible last compounding) to the user.
# Fees for this action are pooled by the user.
sendAssetToSender(amt_b.load()),
# Record the new total stake
App.globalPut(CC_total_stake, App.globalGet(CC_total_stake) - amt_b.load()),
# Record that the new local stake for the user
App.localPut(Txn.sender(), CC_local_stake, BytesMinus(
App.localGet(Txn.sender(), CC_local_stake),
Concat(Itob(amt_b.load()), BytesZero(Int(LOCAL_STAKE_N)))
)
),
# Output the withdrawn amount
output.set(amt_b.load()),
# Approve the call
# Approve(), - leaving this in prevents outputting the results from the method because Approve() returns
# sooner than the output.set returns
)
@router.method(no_op=CallConfig.CALL)
def local_claim(up_to_box: abi.Uint64):
# Make a local claim of the compounding contribution for each box from last claimed one (i.e.
# local_number_of_boxes) up to (including) up_to_box.
# All the boxes need to be provided in the box array call.
idx = ScratchVar()
init = idx.store(App.localGet(Txn.sender(), CC_local_number_of_boxes) + Int(1))
cond = idx.load() <= up_to_box.get()
iter = idx.store(idx.load() + Int(1))
return Seq(
# Process each supplied box
For(init, cond, iter).Do(
local_claim_box(idx.load())
),
# Approve the call
Approve(),
)
@router.method(no_op=CallConfig.CALL)
def delete_boxes(down_to_box: abi.Uint64):
# Delete each box from (including) number_of_boxes down to (excluding) down_to_box
# All need to be supplied in the box array
idx = ScratchVar()
init = idx.store(App.globalGet(CC_number_of_boxes))
cond = idx.load() > down_to_box.get()
iter = idx.store(idx.load() - Int(1))
return Seq(
# Only app creator can delete boxes
Assert(Txn.sender() == Global.creator_address()),
# Boxes can be deleted only if there are no more accounts opted into the CC and the pool has ended, or the
# claiming period has passed
Assert(Or(
And(App.globalGet(CC_number_of_stakers) == Int(0), Global.round() > App.globalGet(CC_pool_end_round)),
Global.round() > App.globalGet(CC_pool_end_round) + App.globalGet(CC_claiming_period)
)
),
# Delete each supplied box
For(init, cond, iter).Do(
Assert(App.box_delete(Itob(idx.load()))),
),
# Update new number of boxes
App.globalPut(CC_number_of_boxes, idx.load()),
# Approve the call
Approve(),
)
return router
def compileCompoundContract(algod_client):
# Compile the program
approval_program, clear_program, contract = getRouter().compile_program(version=8)
with open("./compiled_files/CompoundContract_approval.teal", "w") as f:
f.write(approval_program)
with open("./compiled_files/CompoundContract_clear.teal", "w") as f:
f.write(clear_program)
with open("./compiled_files/CompoundContract.json", "w") as f:
import json
f.write(json.dumps(contract.dictify()))
# Compile program to binary
approval_program_compiled = compile_program(algod_client, approval_program)
# Compile program to binary
clear_state_program_compiled = compile_program(algod_client, clear_program)
approval_program_compiled_b64 = compile_program_b64(algod_client, approval_program)
ExtraProgramPages = math.ceil(len(base64.b64decode(approval_program_compiled_b64)) / 2048) - 1
return [approval_program_compiled, clear_state_program_compiled,
ExtraProgramPages, contract]