Skip to content

Commit 45fa796

Browse files
committed
improve accuracy of legacy penalty truncation
1 parent a75c75c commit 45fa796

File tree

5 files changed

+127
-26
lines changed

5 files changed

+127
-26
lines changed

contracts/sfc/SFC.sol

-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ contract SFC is SFCBase, Version {
3434
_delegate(libAddress);
3535
}
3636

37-
event UpdatedBaseRewardPerSec(uint256 value);
38-
event UpdatedOfflinePenaltyThreshold(uint256 blocksNum, uint256 period);
39-
4037
/*
4138
Constructor
4239
*/

contracts/sfc/SFCI.sol

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
pragma solidity ^0.5.0;
22

33
interface SFCI {
4+
event CreatedValidator(uint256 indexed validatorID, address indexed auth, uint256 createdEpoch, uint256 createdTime);
5+
event Delegated(address indexed delegator, uint256 indexed toValidatorID, uint256 amount);
6+
event Undelegated(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount);
7+
event Withdrawn(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount);
8+
event ClaimedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 lockupExtraReward, uint256 lockupBaseReward, uint256 unlockedReward);
9+
event RestakedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 lockupExtraReward, uint256 lockupBaseReward, uint256 unlockedReward);
10+
event BurntFTM(uint256 amount);
11+
event LockedUpStake(address indexed delegator, uint256 indexed validatorID, uint256 duration, uint256 amount);
12+
event UnlockedStake(address indexed delegator, uint256 indexed validatorID, uint256 amount, uint256 penalty);
13+
event UpdatedSlashingRefundRatio(uint256 indexed validatorID, uint256 refundRatio);
14+
event RefundedSlashedLegacyDelegation(address indexed delegator, uint256 indexed validatorID, uint256 amount);
15+
16+
event DeactivatedValidator(uint256 indexed validatorID, uint256 deactivatedEpoch, uint256 deactivatedTime);
17+
event ChangedValidatorStatus(uint256 indexed validatorID, uint256 status);
18+
419
function currentSealedEpoch() external view returns (uint256);
520

621
function getEpochSnapshot(uint256) external view returns (uint256 endTime, uint256 epochFee, uint256 totalBaseRewardWeight, uint256 totalTxRewardWeight, uint256 _baseRewardPerSecond, uint256 totalStake, uint256 totalSupply);

contracts/sfc/SFCLib.sol

+97-16
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ contract SFCLib is SFCBase {
398398
delete getLockupInfo[delegator][toValidatorID];
399399
delete getStashedLockupRewards[delegator][toValidatorID];
400400
}
401+
_truncateLegacyPenalty(delegator, toValidatorID);
401402
return nonStashedReward.lockupBaseReward != 0 || nonStashedReward.lockupExtraReward != 0 || nonStashedReward.unlockedReward != 0;
402403
}
403404

@@ -484,7 +485,6 @@ contract SFCLib is SFCBase {
484485
// stash the previous penalty and clean getStashedLockupRewards
485486
LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID];
486487
if (relock) {
487-
_truncateLegacyNonStashedPenalty(delegator, toValidatorID, ld.lockedStake, ld.duration);
488488
Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID];
489489

490490
uint256 penalty = _popNonStashedUnlockPenalty(delegator, toValidatorID, ld.lockedStake, ld.lockedStake);
@@ -519,20 +519,6 @@ contract SFCLib is SFCBase {
519519
_lockStake(delegator, toValidatorID, lockupDuration, amount, true);
520520
}
521521

522-
function _truncateLegacyNonStashedPenalty(address delegator, uint256 toValidatorID, uint256 lockupAmount, uint256 duration) internal {
523-
Rewards storage r = getStashedLockupRewards[delegator][toValidatorID];
524-
{ // this block of code can be removed after a year from implementing multi penalty
525-
uint256 avgFullReward = lockupAmount.mul(2219685438).mul(duration).div(1e18); // 0.000000002219685438 is reward per second per wei at 7% APR
526-
Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration);
527-
uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward;
528-
uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2;
529-
if (storedPenalty > 0 && storedPenalty > maxReasonablePenalty) {
530-
r.lockupExtraReward = r.lockupExtraReward.mul(maxReasonablePenalty).div(storedPenalty);
531-
r.lockupBaseReward = r.lockupBaseReward.mul(maxReasonablePenalty).div(storedPenalty);
532-
}
533-
}
534-
}
535-
536522
function _popNonStashedUnlockPenalty(address delegator, uint256 toValidatorID, uint256 unlockAmount, uint256 totalAmount) internal returns (uint256) {
537523
Rewards storage r = getStashedLockupRewards[delegator][toValidatorID];
538524
uint256 lockupExtraRewardShare = r.lockupExtraReward.mul(unlockAmount).div(totalAmount);
@@ -572,7 +558,6 @@ contract SFCLib is SFCBase {
572558

573559
_stashRewards(delegator, toValidatorID);
574560

575-
_truncateLegacyNonStashedPenalty(delegator, toValidatorID, ld.lockedStake, ld.duration);
576561
uint256 penalty = _popWholeUnlockPenalty(delegator, toValidatorID, amount, ld.lockedStake);
577562
if (penalty > amount) {
578563
penalty = amount;
@@ -605,4 +590,100 @@ contract SFCLib is SFCBase {
605590
}
606591
}
607592
}
593+
594+
// code below can be erased after 1 year since deployment of multipenalties
595+
596+
function _getAvgEpochStep(uint256 duration) internal view returns(uint256) {
597+
// estimate number of epochs such that we would make approximately 15 iterations
598+
uint256 tryEpochs = currentSealedEpoch / 5;
599+
if (tryEpochs > 10000) {
600+
tryEpochs = 10000;
601+
}
602+
uint256 tryEndTime = getEpochSnapshot[currentSealedEpoch - tryEpochs].endTime;
603+
if (tryEndTime == 0 || tryEpochs == 0) {
604+
return 0;
605+
}
606+
uint256 secondsPerEpoch = _now().sub(tryEndTime) / tryEpochs;
607+
return duration / (secondsPerEpoch * 15 + 1);
608+
}
609+
610+
function _getAvgReceivedStake(uint256 validatorID, uint256 duration, uint256 step) internal view returns(uint256) {
611+
uint256 receivedStakeSum = getValidator[validatorID].receivedStake;
612+
uint256 samples = 1;
613+
614+
uint256 until = _now().sub(duration);
615+
for (uint256 i = 1; i <= 30; i++) {
616+
uint256 e = currentSealedEpoch - i * step;
617+
EpochSnapshot storage s = getEpochSnapshot[e];
618+
if (s.endTime < until) {
619+
break;
620+
}
621+
uint256 sample = s.receivedStake[validatorID];
622+
if (sample != 0) {
623+
samples++;
624+
receivedStakeSum += sample;
625+
}
626+
}
627+
return receivedStakeSum / samples;
628+
}
629+
630+
function _getAvgUptime(uint256 validatorID, uint256 duration, uint256 step) internal view returns(uint256) {
631+
uint256 until = _now().sub(duration);
632+
uint256 oldUptimeCounter = 0;
633+
uint256 newUptimeCounter = 0;
634+
for (uint256 i = 0; i <= 30; i++) {
635+
uint256 e = currentSealedEpoch - i * step;
636+
EpochSnapshot storage s = getEpochSnapshot[e];
637+
uint256 endTime = s.endTime;
638+
if (endTime < until) {
639+
if (i <= 2) {
640+
return duration;
641+
}
642+
break;
643+
}
644+
uint256 uptimeCounter = s.accumulatedUptime[validatorID];
645+
if (uptimeCounter != 0) {
646+
oldUptimeCounter = uptimeCounter;
647+
if (newUptimeCounter == 0) {
648+
newUptimeCounter = uptimeCounter;
649+
}
650+
}
651+
}
652+
uint256 uptime = newUptimeCounter - oldUptimeCounter;
653+
if (uptime > duration*4/5) {
654+
return duration;
655+
}
656+
return uptime;
657+
}
658+
659+
function _truncateLegacyPenalty(address delegator, uint256 toValidatorID) internal {
660+
Rewards storage r = getStashedLockupRewards[delegator][toValidatorID];
661+
uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2;
662+
if (storedPenalty == 0) {
663+
return;
664+
}
665+
LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID];
666+
uint256 duration = ld.duration;
667+
uint256 lockedStake = ld.lockedStake;
668+
uint256 step = _getAvgEpochStep(duration);
669+
if (step == 0) {
670+
return;
671+
}
672+
uint256 RPS = _getAvgUptime(toValidatorID, duration, step).mul(2092846271).div(duration); // corresponds to 6.6% APR
673+
uint256 selfStake = getStake[delegator][toValidatorID];
674+
675+
uint256 avgFullReward = selfStake.mul(RPS).mul(duration).div(1e18).mul(Decimal.unit().sub(c.validatorCommission())).div(Decimal.unit()); // reward for self-stake
676+
if (getValidator[toValidatorID].auth == delegator) { // reward for received portion of stake
677+
uint256 receivedStakeAvg = _getAvgReceivedStake(toValidatorID, duration, step).mul(11).div(10);
678+
avgFullReward += receivedStakeAvg.mul(RPS).mul(duration).div(1e18).mul(c.validatorCommission()).div(Decimal.unit());
679+
}
680+
avgFullReward = avgFullReward.mul(lockedStake).div(selfStake);
681+
Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration);
682+
uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward;
683+
maxReasonablePenalty = maxReasonablePenalty;
684+
if (storedPenalty > maxReasonablePenalty) {
685+
r.lockupExtraReward = r.lockupExtraReward.mul(maxReasonablePenalty).div(storedPenalty);
686+
r.lockupBaseReward = r.lockupBaseReward.mul(maxReasonablePenalty).div(storedPenalty);
687+
}
688+
}
608689
}

contracts/test/UnitTestSFC.sol

+8
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ contract UnitTestSFCLib is SFCLib, UnitTestSFCBase {
6262
}
6363
return SFCBase.isNode(addr);
6464
}
65+
66+
function _getAvgEpochStep(uint256) internal view returns(uint256) {
67+
return 1;
68+
}
69+
70+
function _getAvgUptime(uint256, uint256 duration, uint256) internal view returns(uint256) {
71+
return duration;
72+
}
6573
}
6674

6775
contract UnitTestNetworkInitializer {

test/SFC.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -1714,20 +1714,20 @@ contract('SFC', async ([firstValidator, testValidator, firstDelegator, secondDel
17141714

17151715
await sealEpoch(this.sfc, (new BN(100)).toString());
17161716

1717-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('1'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000474828297807395'));
1718-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000237414148903697'));
1719-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000004748282978073'));
1717+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('1'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000380540964546690'));
1718+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000190270482273344'));
1719+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000003805409645466'));
17201720
await this.sfc.unlockStake(testValidator3ID, amount18('0.5'), { from: thirdDelegator });
17211721
await expectRevert(this.sfc.unlockStake(testValidator3ID, amount18('0.51'), { from: thirdDelegator }), 'not enough locked stake');
1722-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000237414148903697'));
1723-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000004748282978073'));
1722+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000190270482273344'));
1723+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000003805409645466'));
17241724

17251725
await this.sfc.relockStake(testValidator3ID, (60 * 60 * 24 * 14), amount18('1'),
17261726
{ from: thirdDelegator });
17271727

17281728
await expectRevert(this.sfc.unlockStake(testValidator3ID, amount18('1.51'), { from: thirdDelegator }), 'not enough locked stake');
1729-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('1.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000237414148903697'));
1730-
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000079138049634565')); // 3 times smaller
1729+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('1.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000190270482273344'));
1730+
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000063423494091114')); // 3 times smaller
17311731
});
17321732

17331733
it('Should unlock after period ended and stash rewards', async () => {

0 commit comments

Comments
 (0)