-
Notifications
You must be signed in to change notification settings - Fork 16
/
PledgeAgent.sol
953 lines (851 loc) · 36.7 KB
/
PledgeAgent.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
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
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
// SPDX-License-Identifier: Apache2.0
pragma solidity 0.8.4;
import "./interface/IPledgeAgent.sol";
import "./interface/IParamSubscriber.sol";
import "./interface/ICandidateHub.sol";
import "./interface/ISystemReward.sol";
import "./interface/ILightClient.sol";
import "./lib/Address.sol";
import "./lib/BitcoinHelper.sol";
import "./lib/BytesToTypes.sol";
import "./lib/Memory.sol";
import "./System.sol";
/// This contract manages user delegate, also known as stake
/// Including both coin delegate and hash delegate
/// HARDFORK V-1.0.3
/// `effective transfer` is introduced in this hardfork to keep the rewards for users
/// when transferring CORE tokens from one validator to another
/// `effective transfer` only contains the amount of CORE tokens transferred
/// which are eligible for claiming rewards in the acting round
contract PledgeAgent is IPledgeAgent, System, IParamSubscriber {
using BitcoinHelper for *;
using TypedMemView for *;
uint256 public constant INIT_REQUIRED_COIN_DEPOSIT = 1e18;
uint256 public constant INIT_HASH_POWER_FACTOR = 20000;
uint256 public constant POWER_BLOCK_FACTOR = 1e18;
uint32 public constant INIT_BTC_CONFIRM_BLOCK = 3;
uint256 public constant INIT_MIN_BTC_LOCK_ROUND = 7;
uint256 public constant ROUND_INTERVAL = 86400;
uint256 public constant INIT_MIN_BTC_VALUE = 1e6;
uint256 public constant INIT_BTC_FACTOR = 5e4;
uint256 public constant BTC_STAKE_MAGIC = 0x5341542b;
uint256 public constant CHAINID = 1116;
uint256 public constant FEE_FACTOR = 1e18;
int256 public constant CLAIM_ROUND_LIMIT = 500;
uint256 public constant BTC_UNIT_CONVERSION = 1e10;
uint256 public constant INIT_DELEGATE_BTC_GAS_PRICE = 1e12;
uint256 public requiredCoinDeposit;
// powerFactor/10000 determines the weight of BTC hash power vs CORE stakes
// the default value of powerFactor is set to 20000
// which means the overall BTC hash power takes 2/3 total weight
// when calculating hybrid score and distributing block rewards
uint256 public powerFactor;
// key: candidate's operateAddr
mapping(address => Agent) public agentsMap;
// This field is used to store `special` reward records of delegators.
// There are two cases
// 1, distribute hash power rewards dust to one miner when turn round
// 2, save the amount of tokens failed to claim by coin delegators
// key: delegator address
// value: amount of CORE tokens claimable
mapping(address => uint256) public rewardMap;
// This field is not used in the latest implementation
// It stays here in order to keep data compatibility for TestNet upgrade
mapping(bytes20 => address) public btc2ethMap;
// key: round index
// value: useful state information of round
mapping(uint256 => RoundState) public stateMap;
// roundTag is set to be timestamp / round interval,
// the valid value should be greater than 10,000 since the chain started.
// It is initialized to 1.
uint256 public roundTag;
// HARDFORK V-1.0.3
// debtDepositMap keeps delegator's amount of CORE which should be deducted when claiming rewards in every round
mapping(uint256 => mapping(address => uint256)) public debtDepositMap;
// HARDFORK V-1.0.7
// btcReceiptMap keeps all BTC staking receipts on Core
mapping(bytes32 => BtcReceipt) public btcReceiptMap;
// round2expireInfoMap keeps the amount of expired BTC staking value for each round
mapping(uint256 => BtcExpireInfo) round2expireInfoMap;
// staking weight of each BTC vs. CORE
uint256 public btcFactor;
// minimum rounds to stake for a BTC staking transaction
uint256 public minBtcLockRound;
// the number of blocks to mark a BTC staking transaction as confirmed
uint32 public btcConfirmBlock;
// minimum value to stake for a BTC staking transaction
uint256 public minBtcValue;
uint256 public delegateBtcGasPrice;
// HARDFORK V-1.0.7
struct BtcReceipt {
address agent;
address delegator;
uint256 value;
uint256 endRound;
uint256 rewardIndex;
address payable feeReceiver;
uint256 fee;
}
// HARDFORK V-1.0.7
struct BtcExpireInfo {
address[] agentAddrList;
mapping(address => uint256) agent2valueMap;
mapping(address => uint256) agentExistMap;
}
struct CoinDelegator {
uint256 deposit;
uint256 newDeposit;
uint256 changeRound;
uint256 rewardIndex;
// HARDFORK V-1.0.3
// transferOutDeposit keeps the `effective transfer` out of changeRound
// transferInDeposit keeps the `effective transfer` in of changeRound
uint256 transferOutDeposit;
uint256 transferInDeposit;
}
struct Reward {
uint256 totalReward;
uint256 remainReward;
uint256 score;
uint256 coin;
uint256 round;
}
// The Agent struct for Candidate.
struct Agent {
uint256 totalDeposit;
mapping(address => CoinDelegator) cDelegatorMap;
Reward[] rewardSet;
uint256 power;
uint256 coin;
uint256 btc;
uint256 totalBtc;
}
struct RoundState {
uint256 power;
uint256 coin;
uint256 powerFactor;
uint256 btc;
uint256 btcFactor;
}
/*********************** events **************************/
event paramChange(string key, bytes value);
event delegatedCoin(address indexed agent, address indexed delegator, uint256 amount, uint256 totalAmount);
event delegatedBtc(bytes32 indexed txid, address indexed agent, address indexed delegator, bytes script, uint32 blockHeight, uint256 outputIndex);
event undelegatedCoin(address indexed agent, address indexed delegator, uint256 amount);
event transferredCoin(
address indexed sourceAgent,
address indexed targetAgent,
address indexed delegator,
uint256 amount,
uint256 totalAmount
);
event transferredBtc(
bytes32 indexed txid,
address sourceAgent,
address targetAgent,
address delegator,
uint256 amount,
uint256 totalAmount
);
event btcPledgeExpired(bytes32 indexed txid, address indexed delegator);
event roundReward(address indexed agent, uint256 coinReward, uint256 powerReward, uint256 btcReward);
event claimedReward(address indexed delegator, address indexed operator, uint256 amount, bool success);
event transferredBtcFee(bytes32 indexed txid, address payable feeReceiver, uint256 fee);
event failedTransferBtcFee(bytes32 indexed txid, address payable feeReceiver, uint256 fee);
/// The validator candidate is inactive, it is expected to be active
/// @param candidate Address of the validator candidate
error InactiveAgent(address candidate);
/// Same source/target addressed provided, it is expected to be different
/// @param source Address of the source candidate
/// @param target Address of the target candidate
error SameCandidate(address source, address target);
function init() external onlyNotInit {
requiredCoinDeposit = INIT_REQUIRED_COIN_DEPOSIT;
powerFactor = INIT_HASH_POWER_FACTOR;
btcFactor = INIT_BTC_FACTOR;
minBtcLockRound = INIT_MIN_BTC_LOCK_ROUND;
btcConfirmBlock = INIT_BTC_CONFIRM_BLOCK;
minBtcValue = INIT_MIN_BTC_VALUE;
roundTag = block.timestamp / ROUND_INTERVAL;
alreadyInit = true;
}
/*********************** Interface implementations ***************************/
/// Receive round rewards from ValidatorSet, which is triggered at the beginning of turn round
/// @param agentList List of validator operator addresses
/// @param rewardList List of reward amount
function addRoundReward(address[] calldata agentList, uint256[] calldata rewardList)
external
payable
override
onlyValidator
{
uint256 agentSize = agentList.length;
require(agentSize == rewardList.length, "the length of agentList and rewardList should be equal");
RoundState memory rs = stateMap[roundTag];
for (uint256 i = 0; i < agentSize; ++i) {
Agent storage a = agentsMap[agentList[i]];
if (a.rewardSet.length == 0) {
continue;
}
Reward storage r = a.rewardSet[a.rewardSet.length - 1];
uint256 roundScore = r.score;
if (roundScore == 0) {
delete a.rewardSet[a.rewardSet.length - 1];
continue;
}
if (rewardList[i] == 0) {
continue;
}
r.totalReward = rewardList[i];
r.remainReward = rewardList[i];
uint256 coinReward = rewardList[i] * a.coin * rs.power / roundScore;
uint256 powerReward = rewardList[i] * a.power * rs.coin / 10000 * rs.powerFactor / roundScore;
uint256 btcReward = rewardList[i] * a.btc * rs.btcFactor * rs.power / roundScore;
emit roundReward(agentList[i], coinReward, powerReward, btcReward);
}
}
/// Calculate hybrid score for all candidates
/// @param candidates List of candidate operator addresses
/// @param powers List of power value in this round
/// @param round The new round tag
/// @return scores List of hybrid scores of all validator candidates in this round
function getHybridScore(
address[] calldata candidates,
uint256[] calldata powers,
uint256 round
) external override onlyCandidate returns (uint256[] memory scores) {
uint256 candidateSize = candidates.length;
require(candidateSize == powers.length, "the length of candidates and powers should be equal");
// HARDFORK V-1.0.7
// the expired BTC staking values will be removed before calculating hybrid score for validators
for (uint256 r = roundTag + 1; r <= round; ++r) {
BtcExpireInfo storage expireInfo = round2expireInfoMap[r];
uint256 j = expireInfo.agentAddrList.length;
while (j > 0) {
j--;
address agent = expireInfo.agentAddrList[j];
agentsMap[agent].totalBtc -= expireInfo.agent2valueMap[agent];
expireInfo.agentAddrList.pop();
delete expireInfo.agent2valueMap[agent];
delete expireInfo.agentExistMap[agent];
}
delete round2expireInfoMap[r];
}
uint256 totalPower = 1;
uint256 totalCoin = 1;
uint256 totalBtc;
// setup `power` and `coin` values for every candidate
// cost gas approximate candidateSize*20000
for (uint256 i = 0; i < candidateSize; ++i) {
Agent storage a = agentsMap[candidates[i]];
// in order to improve accuracy, the calculation of power is based on 10^18
a.power = powers[i] * POWER_BLOCK_FACTOR;
a.btc = a.totalBtc;
a.coin = a.totalDeposit;
totalPower += a.power;
totalCoin += a.coin;
totalBtc += a.btc;
}
uint256 bf = (btcFactor == 0 ? INIT_BTC_FACTOR : btcFactor) * BTC_UNIT_CONVERSION;
uint256 pf = powerFactor;
// calc hybrid score
// HARDFORK V-1.0.7 BTC staking is added when calculating the hybrid score
scores = new uint256[](candidateSize);
for (uint256 i = 0; i < candidateSize; ++i) {
Agent storage a = agentsMap[candidates[i]];
scores[i] = a.power * (totalCoin + totalBtc * bf) * pf / 10000 + (a.coin + a.btc * bf) * totalPower;
}
RoundState storage rs = stateMap[round];
rs.power = totalPower;
rs.coin = totalCoin;
rs.powerFactor = pf;
rs.btc = totalBtc;
rs.btcFactor = bf;
}
/// Start new round, this is called by the CandidateHub contract
/// @param validators List of elected validators in this round
/// @param round The new round tag
function setNewRound(address[] calldata validators, uint256 round) external override onlyCandidate {
RoundState storage rs = stateMap[round];
uint256 validatorSize = validators.length;
for (uint256 i = 0; i < validatorSize; ++i) {
Agent storage a = agentsMap[validators[i]];
// HARDFORK V-1.0.7 BTC staking is added when calculating the hybrid score
uint256 btcScore = a.btc * rs.btcFactor;
uint256 score = a.power * (rs.coin + rs.btc * rs.btcFactor) * rs.powerFactor / 10000 + (a.coin + btcScore) * rs.power;
a.rewardSet.push(Reward(0, 0, score, a.coin + btcScore, round));
}
roundTag = round;
}
/// Distribute rewards for delegated hash power on one validator candidate
/// This method is called at the beginning of `turn round` workflow
/// @param candidate The operator address of the validator candidate
/// @param miners List of BTC miners who delegated hash power to the candidate
function distributePowerReward(address candidate, address[] calldata miners) external override onlyCandidate {
// distribute rewards to every miner
// note that the miners are represented in the form of reward addresses
// and they can be duplicated because everytime a miner delegates a BTC block
// to a validator on Core blockchain, a new record is added in BTCLightClient
Agent storage a = agentsMap[candidate];
uint256 l = a.rewardSet.length;
if (l == 0) {
return;
}
Reward storage r = a.rewardSet[l-1];
if (r.totalReward == 0 || r.round != roundTag) {
return;
}
RoundState storage rs = stateMap[roundTag];
uint256 reward = (rs.coin + rs.btc * rs.btcFactor) * POWER_BLOCK_FACTOR * rs.powerFactor / 10000 * r.totalReward / r.score;
uint256 minerSize = miners.length;
uint256 powerReward = reward * minerSize;
uint256 undelegateCoinReward;
uint256 btcScore = a.btc * rs.btcFactor;
if (a.coin + btcScore > r.coin) {
// undelegatedCoin = a.coin - r.coin
undelegateCoinReward = r.totalReward * (a.coin + btcScore - r.coin) * rs.power / r.score;
}
uint256 remainReward = r.remainReward;
require(remainReward >= powerReward + undelegateCoinReward, "there is not enough reward");
for (uint256 i = 0; i < minerSize; i++) {
rewardMap[miners[i]] += reward;
}
if (r.coin == 0) {
delete a.rewardSet[l-1];
undelegateCoinReward = remainReward - powerReward;
} else if (powerReward != 0 || undelegateCoinReward != 0) {
r.remainReward -= (powerReward + undelegateCoinReward);
}
if (undelegateCoinReward != 0) {
ISystemReward(SYSTEM_REWARD_ADDR).receiveRewards{ value: undelegateCoinReward }();
}
}
function onFelony(address agent) external override onlyValidator {
Agent storage a = agentsMap[agent];
uint256 len = a.rewardSet.length;
if (len > 0) {
Reward storage r = a.rewardSet[len-1];
if (r.round == roundTag && r.coin == 0) {
delete a.rewardSet[len-1];
}
}
}
/*********************** External methods ***************************/
/// Delegate coin to a validator
/// @param agent The operator address of validator
function delegateCoin(address agent) external payable override {
if (!ICandidateHub(CANDIDATE_HUB_ADDR).canDelegate(agent)) {
revert InactiveAgent(agent);
}
uint256 newDeposit = delegateCoin(agent, msg.sender, msg.value, 0);
emit delegatedCoin(agent, msg.sender, msg.value, newDeposit);
}
/// Undelegate coin from a validator
/// @param agent The operator address of validator
function undelegateCoin(address agent) external override {
undelegateCoin(agent, 0);
}
/// Undelegate coin from a validator
/// @param agent The operator address of validator
/// @param amount The amount of CORE to undelegate
function undelegateCoin(address agent, uint256 amount) public override {
(uint256 deposit, ) = undelegateCoin(agent, msg.sender, amount, false);
Address.sendValue(payable(msg.sender), deposit);
emit undelegatedCoin(agent, msg.sender, deposit);
}
/// Transfer coin stake to a new validator
/// @param sourceAgent The validator to transfer coin stake from
/// @param targetAgent The validator to transfer coin stake to
function transferCoin(address sourceAgent, address targetAgent) external override {
transferCoin(sourceAgent, targetAgent, 0);
}
/// Transfer coin stake to a new validator
/// @param sourceAgent The validator to transfer coin stake from
/// @param targetAgent The validator to transfer coin stake to
/// @param amount The amount of CORE to transfer
function transferCoin(address sourceAgent, address targetAgent, uint256 amount) public override {
if (!ICandidateHub(CANDIDATE_HUB_ADDR).canDelegate(targetAgent)) {
revert InactiveAgent(targetAgent);
}
if (sourceAgent == targetAgent) {
revert SameCandidate(sourceAgent, targetAgent);
}
(uint256 deposit, uint256 deductedDeposit) = undelegateCoin(sourceAgent, msg.sender, amount, true);
uint256 newDeposit = delegateCoin(targetAgent, msg.sender, deposit, deductedDeposit);
emit transferredCoin(sourceAgent, targetAgent, msg.sender, deposit, newDeposit);
}
/// Claim reward for delegator
/// @param agentList The list of validators to claim rewards on, it can be empty
/// @return (Amount claimed, Are all rewards claimed)
function claimReward(address[] calldata agentList) external override returns (uint256, bool) {
// limit round count to control gas usage
int256 roundLimit = CLAIM_ROUND_LIMIT;
uint256 reward;
uint256 rewardSum = rewardMap[msg.sender];
if (rewardSum != 0) {
rewardMap[msg.sender] = 0;
}
uint256 agentSize = agentList.length;
for (uint256 i = 0; i < agentSize; ++i) {
Agent storage a = agentsMap[agentList[i]];
if (a.rewardSet.length == 0) {
continue;
}
CoinDelegator storage d = a.cDelegatorMap[msg.sender];
if (d.newDeposit == 0 && d.transferOutDeposit == 0) {
continue;
}
int256 roundCount = int256(a.rewardSet.length - d.rewardIndex);
reward = collectCoinReward(a, d, roundLimit);
roundLimit -= roundCount;
rewardSum += reward;
if (d.newDeposit == 0 && d.transferOutDeposit == 0) {
delete a.cDelegatorMap[msg.sender];
}
// if there are rewards to be collected, leave them there
if (roundLimit < 0) {
break;
}
}
if (rewardSum != 0) {
distributeReward(payable(msg.sender), rewardSum);
}
return (rewardSum, roundLimit >= 0);
}
// HARDFORK V-1.0.7
// User workflow to delegate BTC to Core blockchain
// 1. A user creates a bitcoin transaction, locks up certain amount ot Bitcoin in one of the transaction output for certain period.
// The transaction should also have an op_return output which contains the staking information, such as the validator and reward addresses.
// 2. Transmit the transaction to Core blockchain by calling the below method `delegateBtc`.
// 3. The user can claim rewards using the reward address set in step 1 during the staking period.
// 4. The user can spend the timelocked UTXO using the redeem script when the lock expires.
// The redeem script should start with a time lock. such as:
// <abstract locktime> OP_CLTV OP_DROP <pubKey> OP_CHECKSIG
// <abstract locktime> OP_CLTV OP_DROP OP_DUP OP_HASH160 <pubKey Hash> OP_EQUALVERIFY OP_CHECKSIG
// <abstract locktime> OP_CLTV OP_DROP M <pubKey1> <pubKey1> ... <pubKeyN> N OP_CHECKMULTISIG
/// delegate BTC to Core network
/// @param btcTx the BTC transaction data
/// @param blockHeight block height of the transaction
/// @param nodes part of the Merkle tree from the tx to the root in LE form (called Merkle proof)
/// @param index index of the tx in Merkle tree
/// @param script the corresponding redeem script of the locked up output
function delegateBtc(bytes calldata btcTx, uint32 blockHeight, bytes32[] memory nodes, uint256 index, bytes memory script) external override {
require(script[0] == bytes1(uint8(0x04)) && script[5] == bytes1(uint8(0xb1)), "not a valid redeem script");
bytes32 txid = btcTx.calculateTxId();
require(ILightClient(LIGHT_CLIENT_ADDR).
checkTxProof(txid, blockHeight, (btcConfirmBlock == 0 ? INIT_BTC_CONFIRM_BLOCK : btcConfirmBlock), nodes, index), "btc tx not confirmed");
BtcReceipt storage br = btcReceiptMap[txid];
require(br.value == 0, "btc tx confirmed");
uint32 lockTime = parseLockTime(script);
br.endRound = lockTime / ROUND_INTERVAL;
require(br.endRound > roundTag + (minBtcLockRound == 0 ? INIT_MIN_BTC_LOCK_ROUND : minBtcLockRound), "insufficient lock round");
(,,bytes29 voutView,) = btcTx.extractTx();
bytes29 payload;
uint256 outputIndex;
(br.value, payload, outputIndex) = voutView.parseToScriptValueAndData(script);
require(br.value >= (minBtcValue == 0 ? INIT_MIN_BTC_VALUE : minBtcValue), "staked value does not meet requirement");
uint256 fee;
(br.delegator, br.agent, fee) = parseAndCheckPayload(payload);
if (!ICandidateHub(CANDIDATE_HUB_ADDR).isCandidateByOperate(br.agent)) {
revert InactiveAgent(br.agent);
}
require(IRelayerHub(RELAYER_HUB_ADDR).isRelayer(msg.sender) || msg.sender == br.delegator, "only delegator or relayer can submit the BTC transaction");
require(tx.gasprice <= (delegateBtcGasPrice == 0 ? INIT_DELEGATE_BTC_GAS_PRICE : delegateBtcGasPrice), "gas price is too high");
emit delegatedBtc(txid, br.agent, br.delegator, script, blockHeight, outputIndex);
if (fee != 0) {
br.fee = fee;
br.feeReceiver = payable(msg.sender);
}
Agent storage a = agentsMap[br.agent];
br.rewardIndex = a.rewardSet.length;
addExpire(br);
a.totalBtc += br.value;
}
// HARDFORK V-1.0.7
/// transfer staked BTC to a different validator
/// @param txid id of the BTC staking transaction
/// @param targetAgent the new validator address to stake to
function transferBtc(bytes32 txid, address targetAgent) external override {
BtcReceipt storage br = btcReceiptMap[txid];
require(br.value != 0, "btc tx not found");
require(br.delegator == msg.sender, "not the delegator of this btc receipt");
address agent = br.agent;
require(agent != targetAgent, "can not transfer to the same validator");
require(br.endRound > roundTag + 1, "insufficient locking rounds");
if (!ICandidateHub(CANDIDATE_HUB_ADDR).canDelegate(targetAgent)) {
revert InactiveAgent(targetAgent);
}
(uint256 reward,) = collectBtcReward(txid, 0x7FFFFFFF);
Agent storage a = agentsMap[agent];
a.totalBtc -= br.value;
round2expireInfoMap[br.endRound].agent2valueMap[agent] -= br.value;
Reward storage r = a.rewardSet[a.rewardSet.length - 1];
if (r.round == roundTag && br.rewardIndex < a.rewardSet.length) {
r.coin -= br.value * stateMap[roundTag].btcFactor;
}
Agent storage ta = agentsMap[targetAgent];
br.agent = targetAgent;
br.rewardIndex = ta.rewardSet.length;
addExpire(br);
ta.totalBtc += br.value;
if (reward != 0) {
distributeReward(payable(msg.sender), reward);
}
emit transferredBtc(txid, agent, targetAgent, msg.sender, br.value, ta.totalBtc);
}
// HARDFORK V-1.0.7
/// claim BTC staking rewards
/// @param txidList the list of BTC staking transaction id to claim rewards
/// @return rewardSum amount of reward claimed
function claimBtcReward(bytes32[] calldata txidList) external override returns (uint256 rewardSum) {
int256 claimLimit = CLAIM_ROUND_LIMIT;
uint256 len = txidList.length;
for(uint256 i = 0; i < len && claimLimit != 0; i++) {
bytes32 txid = txidList[i];
BtcReceipt storage br = btcReceiptMap[txid];
require(br.value != 0, "btc tx not found");
address delegator = br.delegator;
require(delegator == msg.sender, "not the delegator of this btc receipt");
uint256 reward;
(reward, claimLimit) = collectBtcReward(txid, claimLimit);
rewardSum += reward;
if (br.value == 0) {
emit btcPledgeExpired(txid, delegator);
}
}
if (rewardSum != 0) {
distributeReward(payable(msg.sender), rewardSum);
}
return rewardSum;
}
/*********************** Internal methods ***************************/
function distributeReward(address payable delegator, uint256 reward) internal {
Address.sendValue(delegator, reward);
emit claimedReward(delegator, msg.sender, reward, true);
}
function delegateCoin(address agent, address delegator, uint256 deposit, uint256 transferInDeposit) internal returns (uint256) {
require(deposit >= requiredCoinDeposit, "deposit is too small");
Agent storage a = agentsMap[agent];
CoinDelegator storage d = a.cDelegatorMap[delegator];
uint256 rewardAmount;
if (d.changeRound != 0) {
rewardAmount = collectCoinReward(a, d, 0x7FFFFFFF);
}
a.totalDeposit += deposit;
if (d.newDeposit == 0 && d.transferOutDeposit == 0) {
d.newDeposit = deposit;
d.changeRound = roundTag;
d.rewardIndex = a.rewardSet.length;
} else {
if (d.changeRound < roundTag) {
d.deposit = d.newDeposit;
d.changeRound = roundTag;
}
d.newDeposit += deposit;
}
// HARDFORK V-1.0.3 receive `effective transfer`
if (transferInDeposit != 0) {
d.transferInDeposit += transferInDeposit;
}
if (rewardAmount != 0) {
distributeReward(payable(delegator), rewardAmount);
}
return d.newDeposit;
}
function undelegateCoin(address agent, address delegator, uint256 amount, bool isTransfer) internal returns (uint256, uint256) {
Agent storage a = agentsMap[agent];
CoinDelegator storage d = a.cDelegatorMap[delegator];
uint256 newDeposit = d.newDeposit;
if (amount == 0) {
amount = newDeposit;
}
require(newDeposit != 0, "delegator does not exist");
if (newDeposit != amount) {
require(amount >= requiredCoinDeposit, "undelegate amount is too small");
require(newDeposit >= requiredCoinDeposit + amount, "remaining amount is too small");
}
uint256 rewardAmount = collectCoinReward(a, d, 0x7FFFFFFF);
a.totalDeposit -= amount;
// HARDFORK V-1.0.3
// when handling undelegate, which can be triggered by either REAL undelegate or a transfer
// the delegated CORE tokens are consumed in the following order
// 1. the amount of CORE which are not eligible for claiming rewards
// 2. the amount of self-delegated CORE eligible for claiming rewards (d.deposit)
// 3. the amount of transferred in CORE eligible for claiming rewards (d.transferInDeposit)
// deductedInDeposit is the amount of transferred in CORE needs to be deducted from rewards calculation/distribution
// if it is a REAL undelegate, the value will be added to the DEBT map
// which takes effect when users claim rewards (or other actions that trigger claiming)
uint256 deposit = d.changeRound < roundTag ? newDeposit : d.deposit;
newDeposit -= amount;
uint256 deductedInDeposit;
uint256 deductedOutDeposit;
if (newDeposit < d.transferInDeposit) {
deductedInDeposit = d.transferInDeposit - newDeposit;
d.transferInDeposit = newDeposit;
if (!isTransfer) {
debtDepositMap[roundTag][msg.sender] += deductedInDeposit;
}
deductedOutDeposit = deposit;
} else if (newDeposit < d.transferInDeposit + deposit) {
deductedOutDeposit = d.transferInDeposit + deposit - newDeposit;
}
// HARDFORK V-1.0.3
// deductedOutDeposit is the amount of self-delegated CORE needs to be deducted from rewards calculation/distribution
// if it is a REAL undelegate, the amount is deducted from the reward set directly
// otherwise, the amount will be added to d.transferOutDeposit which can be used to claim rewards as d.deposit
if (deductedOutDeposit != 0) {
deposit -= deductedOutDeposit;
if (a.rewardSet.length != 0) {
Reward storage r = a.rewardSet[a.rewardSet.length - 1];
if (r.round == roundTag) {
if (isTransfer) {
d.transferOutDeposit += deductedOutDeposit;
} else {
r.coin -= deductedOutDeposit;
}
} else {
deductedOutDeposit = 0;
}
} else {
deductedOutDeposit = 0;
}
}
if (newDeposit == 0 && d.transferOutDeposit == 0) {
delete a.cDelegatorMap[delegator];
} else {
d.deposit = deposit;
d.newDeposit = newDeposit;
d.changeRound = roundTag;
}
if (rewardAmount != 0) {
distributeReward(payable(delegator), rewardAmount);
}
return (amount, deductedInDeposit + deductedOutDeposit);
}
function collectCoinReward(Reward storage r, uint256 deposit) internal returns (uint256 rewardAmount) {
require(r.coin >= deposit, "reward is not enough");
uint256 curReward;
if (r.coin == deposit) {
curReward = r.remainReward;
r.coin = 0;
} else {
uint256 rsPower = stateMap[r.round].power;
curReward = (r.totalReward * deposit * rsPower) / r.score;
require(r.remainReward >= curReward, "there is not enough reward");
r.coin -= deposit;
r.remainReward -= curReward;
}
return curReward;
}
function collectCoinReward(Agent storage a, CoinDelegator storage d, int256 roundLimit) internal returns (uint256 rewardAmount) {
uint256 changeRound = d.changeRound;
uint256 curRound = roundTag;
if (changeRound < curRound) {
d.transferInDeposit = 0;
}
uint256 rewardLength = a.rewardSet.length;
uint256 rewardIndex = d.rewardIndex;
if (rewardIndex >= rewardLength) {
return 0;
}
if (rewardIndex + uint256(roundLimit) < rewardLength) {
rewardLength = rewardIndex + uint256(roundLimit);
}
while (rewardIndex < rewardLength) {
Reward storage r = a.rewardSet[rewardIndex];
uint256 rRound = r.round;
if (rRound == curRound) {
break;
}
uint256 deposit = d.newDeposit;
// HARDFORK V-1.0.3
// d.deposit and d.transferOutDeposit are both eligible for claiming rewards
// however, d.transferOutDeposit will be used to pay the DEBT for the delegator before that
// the rewards from the DEBT will be collected and sent to the system reward contract
if (rRound == changeRound) {
uint256 transferOutDeposit = d.transferOutDeposit;
uint256 debt = debtDepositMap[rRound][msg.sender];
if (transferOutDeposit > debt) {
transferOutDeposit -= debt;
debtDepositMap[rRound][msg.sender] = 0;
} else {
debtDepositMap[rRound][msg.sender] -= transferOutDeposit;
transferOutDeposit = 0;
}
if (transferOutDeposit != d.transferOutDeposit) {
uint256 undelegateReward = collectCoinReward(r, d.transferOutDeposit - transferOutDeposit);
if (r.coin == 0) {
delete a.rewardSet[rewardIndex];
}
ISystemReward(SYSTEM_REWARD_ADDR).receiveRewards{ value: undelegateReward }();
}
deposit = d.deposit + transferOutDeposit;
d.deposit = d.newDeposit;
d.transferOutDeposit = 0;
}
if (deposit != 0) {
rewardAmount += collectCoinReward(r, deposit);
if (r.coin == 0) {
delete a.rewardSet[rewardIndex];
}
}
rewardIndex++;
}
// update index whenever claim happens
d.rewardIndex = rewardIndex;
return rewardAmount;
}
function parseAndCheckPayload(bytes29 payload) internal pure returns (address delegator, address agent, uint256 fee) {
require(payload.len() >= 48, "payload length is too small");
require(payload.indexUint(0, 4) == BTC_STAKE_MAGIC, "wrong magic");
require(payload.indexUint(4, 1) == 1, "wrong version");
require(payload.indexUint(5, 2) == CHAINID, "wrong chain id");
delegator= payload.indexAddress(7);
agent = payload.indexAddress(27);
fee = payload.indexUint(47, 1) * FEE_FACTOR;
}
function parseLockTime(bytes memory script) internal pure returns (uint32) {
uint256 t;
assembly {
let loc := add(script, 0x21)
t := mload(loc)
}
return uint32(t.reverseUint256() & 0xFFFFFFFF);
}
function addExpire(BtcReceipt storage br) internal {
BtcExpireInfo storage expireInfo = round2expireInfoMap[br.endRound];
if (expireInfo.agentExistMap[br.agent] == 0) {
expireInfo.agentAddrList.push(br.agent);
expireInfo.agentExistMap[br.agent] = 1;
}
expireInfo.agent2valueMap[br.agent] += br.value;
}
function collectBtcReward(bytes32 txid, int256 claimLimit) internal returns (uint256, int256) {
uint256 curRound = roundTag;
BtcReceipt storage br = btcReceiptMap[txid];
uint256 reward = 0;
Agent storage a = agentsMap[br.agent];
uint256 rewardIndex = br.rewardIndex;
uint256 rewardLength = a.rewardSet.length;
while (rewardIndex < rewardLength && claimLimit != 0) {
Reward storage r = a.rewardSet[rewardIndex];
uint256 rRound = r.round;
if (rRound == curRound || br.endRound <= rRound) {
break;
}
uint256 deposit = br.value * stateMap[rRound].btcFactor;
reward += collectCoinReward(r, deposit);
if (r.coin == 0) {
delete a.rewardSet[rewardIndex];
}
rewardIndex += 1;
claimLimit -= 1;
}
uint256 fee = br.fee;
uint256 feeReward;
if (fee != 0) {
if (fee <= reward) {
feeReward = fee;
} else {
feeReward = reward;
}
if (feeReward != 0) {
br.fee -= feeReward;
bool success = br.feeReceiver.send(feeReward);
if (success) {
reward -= feeReward;
emit transferredBtcFee(txid, br.feeReceiver, feeReward);
} else {
emit failedTransferBtcFee(txid, br.feeReceiver, feeReward);
}
}
}
if (br.endRound <= (rewardIndex == rewardLength ? curRound : a.rewardSet[rewardIndex].round)) {
delete btcReceiptMap[txid];
} else {
br.rewardIndex = rewardIndex;
}
return (reward, claimLimit);
}
/*********************** Governance ********************************/
/// Update parameters through governance vote
/// @param key The name of the parameter
/// @param value the new value set to the parameter
function updateParam(string calldata key, bytes calldata value) external override onlyInit onlyGov {
if (value.length != 32) {
revert MismatchParamLength(key);
}
if (Memory.compareStrings(key, "requiredCoinDeposit")) {
uint256 newRequiredCoinDeposit = BytesToTypes.bytesToUint256(32, value);
if (newRequiredCoinDeposit == 0) {
revert OutOfBounds(key, newRequiredCoinDeposit, 1, type(uint256).max);
}
requiredCoinDeposit = newRequiredCoinDeposit;
} else if (Memory.compareStrings(key, "powerFactor")) {
uint256 newHashPowerFactor = BytesToTypes.bytesToUint256(32, value);
if (newHashPowerFactor == 0) {
revert OutOfBounds(key, newHashPowerFactor, 1, type(uint256).max);
}
powerFactor = newHashPowerFactor;
} else if (Memory.compareStrings(key, "btcFactor")) {
uint256 newBtcFactor = BytesToTypes.bytesToUint256(32, value);
if (newBtcFactor == 0) {
revert OutOfBounds(key, newBtcFactor, 1, type(uint256).max);
}
btcFactor = newBtcFactor;
} else if (Memory.compareStrings(key, "minBtcLockRound")) {
uint256 newMinBtcLockRound = BytesToTypes.bytesToUint256(32, value);
if (newMinBtcLockRound == 0) {
revert OutOfBounds(key, newMinBtcLockRound, 1, type(uint256).max);
}
minBtcLockRound = newMinBtcLockRound;
} else if (Memory.compareStrings(key, "btcConfirmBlock")) {
uint256 newBtcConfirmBlock = BytesToTypes.bytesToUint256(32, value);
if (newBtcConfirmBlock == 0) {
revert OutOfBounds(key, newBtcConfirmBlock, 1, type(uint256).max);
}
btcConfirmBlock = uint32(newBtcConfirmBlock);
} else if (Memory.compareStrings(key, "minBtcValue")) {
uint256 newMinBtcValue = BytesToTypes.bytesToUint256(32, value);
if (newMinBtcValue == 0) {
revert OutOfBounds(key, newMinBtcValue, 1e4, type(uint256).max);
}
minBtcValue = newMinBtcValue;
} else if (Memory.compareStrings(key,"delegateBtcGasPrice")) {
uint256 newDelegateBtcGasPrice = BytesToTypes.bytesToUint256(32, value);
if (newDelegateBtcGasPrice < 1e9) {
revert OutOfBounds(key, newDelegateBtcGasPrice, 1e9, type(uint256).max);
}
delegateBtcGasPrice = newDelegateBtcGasPrice;
} else {
require(false, "unknown param");
}
emit paramChange(key, value);
}
/*********************** Public view ********************************/
/// Get delegator information
/// @param agent The operator address of validator
/// @param delegator The delegator address
/// @return CoinDelegator Information of the delegator
function getDelegator(address agent, address delegator) external view returns (CoinDelegator memory) {
CoinDelegator memory d = agentsMap[agent].cDelegatorMap[delegator];
return d;
}
/// Get reward information of a validator by index
/// @param agent The operator address of validator
/// @param index The reward index
/// @return Reward The reward information
function getReward(address agent, uint256 index) external view returns (Reward memory) {
Agent storage a = agentsMap[agent];
require(index < a.rewardSet.length, "out of up bound");
return a.rewardSet[index];
}
/// Get expire information of a validator by round and agent
/// @param round The end round of the btc lock
/// @param agent The operator address of validator
/// @return expireValue The expire value of the agent in the round
function getExpireValue(uint256 round, address agent) external view returns (uint256){
BtcExpireInfo storage expireInfo = round2expireInfoMap[round];
return expireInfo.agent2valueMap[agent];
}
}