Skip to content

Commit

Permalink
improve accuracy of legacy penalty truncation
Browse files Browse the repository at this point in the history
  • Loading branch information
uprendis committed Feb 22, 2024
1 parent a75c75c commit 45fa796
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 26 deletions.
3 changes: 0 additions & 3 deletions contracts/sfc/SFC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ contract SFC is SFCBase, Version {
_delegate(libAddress);
}

event UpdatedBaseRewardPerSec(uint256 value);
event UpdatedOfflinePenaltyThreshold(uint256 blocksNum, uint256 period);

/*
Constructor
*/
Expand Down
15 changes: 15 additions & 0 deletions contracts/sfc/SFCI.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
pragma solidity ^0.5.0;

interface SFCI {
event CreatedValidator(uint256 indexed validatorID, address indexed auth, uint256 createdEpoch, uint256 createdTime);
event Delegated(address indexed delegator, uint256 indexed toValidatorID, uint256 amount);
event Undelegated(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount);
event Withdrawn(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount);
event ClaimedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 lockupExtraReward, uint256 lockupBaseReward, uint256 unlockedReward);
event RestakedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 lockupExtraReward, uint256 lockupBaseReward, uint256 unlockedReward);
event BurntFTM(uint256 amount);
event LockedUpStake(address indexed delegator, uint256 indexed validatorID, uint256 duration, uint256 amount);
event UnlockedStake(address indexed delegator, uint256 indexed validatorID, uint256 amount, uint256 penalty);
event UpdatedSlashingRefundRatio(uint256 indexed validatorID, uint256 refundRatio);
event RefundedSlashedLegacyDelegation(address indexed delegator, uint256 indexed validatorID, uint256 amount);

event DeactivatedValidator(uint256 indexed validatorID, uint256 deactivatedEpoch, uint256 deactivatedTime);
event ChangedValidatorStatus(uint256 indexed validatorID, uint256 status);

function currentSealedEpoch() external view returns (uint256);

function getEpochSnapshot(uint256) external view returns (uint256 endTime, uint256 epochFee, uint256 totalBaseRewardWeight, uint256 totalTxRewardWeight, uint256 _baseRewardPerSecond, uint256 totalStake, uint256 totalSupply);
Expand Down
113 changes: 97 additions & 16 deletions contracts/sfc/SFCLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ contract SFCLib is SFCBase {
delete getLockupInfo[delegator][toValidatorID];
delete getStashedLockupRewards[delegator][toValidatorID];
}
_truncateLegacyPenalty(delegator, toValidatorID);
return nonStashedReward.lockupBaseReward != 0 || nonStashedReward.lockupExtraReward != 0 || nonStashedReward.unlockedReward != 0;
}

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

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

function _truncateLegacyNonStashedPenalty(address delegator, uint256 toValidatorID, uint256 lockupAmount, uint256 duration) internal {
Rewards storage r = getStashedLockupRewards[delegator][toValidatorID];
{ // this block of code can be removed after a year from implementing multi penalty
uint256 avgFullReward = lockupAmount.mul(2219685438).mul(duration).div(1e18); // 0.000000002219685438 is reward per second per wei at 7% APR
Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration);
uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward;
uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2;
if (storedPenalty > 0 && storedPenalty > maxReasonablePenalty) {
r.lockupExtraReward = r.lockupExtraReward.mul(maxReasonablePenalty).div(storedPenalty);
r.lockupBaseReward = r.lockupBaseReward.mul(maxReasonablePenalty).div(storedPenalty);
}
}
}

function _popNonStashedUnlockPenalty(address delegator, uint256 toValidatorID, uint256 unlockAmount, uint256 totalAmount) internal returns (uint256) {
Rewards storage r = getStashedLockupRewards[delegator][toValidatorID];
uint256 lockupExtraRewardShare = r.lockupExtraReward.mul(unlockAmount).div(totalAmount);
Expand Down Expand Up @@ -572,7 +558,6 @@ contract SFCLib is SFCBase {

_stashRewards(delegator, toValidatorID);

_truncateLegacyNonStashedPenalty(delegator, toValidatorID, ld.lockedStake, ld.duration);
uint256 penalty = _popWholeUnlockPenalty(delegator, toValidatorID, amount, ld.lockedStake);
if (penalty > amount) {
penalty = amount;
Expand Down Expand Up @@ -605,4 +590,100 @@ contract SFCLib is SFCBase {
}
}
}

// code below can be erased after 1 year since deployment of multipenalties

function _getAvgEpochStep(uint256 duration) internal view returns(uint256) {
// estimate number of epochs such that we would make approximately 15 iterations
uint256 tryEpochs = currentSealedEpoch / 5;
if (tryEpochs > 10000) {
tryEpochs = 10000;
}
uint256 tryEndTime = getEpochSnapshot[currentSealedEpoch - tryEpochs].endTime;
if (tryEndTime == 0 || tryEpochs == 0) {
return 0;
}
uint256 secondsPerEpoch = _now().sub(tryEndTime) / tryEpochs;
return duration / (secondsPerEpoch * 15 + 1);
}

function _getAvgReceivedStake(uint256 validatorID, uint256 duration, uint256 step) internal view returns(uint256) {
uint256 receivedStakeSum = getValidator[validatorID].receivedStake;
uint256 samples = 1;

uint256 until = _now().sub(duration);
for (uint256 i = 1; i <= 30; i++) {
uint256 e = currentSealedEpoch - i * step;
EpochSnapshot storage s = getEpochSnapshot[e];
if (s.endTime < until) {
break;
}
uint256 sample = s.receivedStake[validatorID];
if (sample != 0) {
samples++;
receivedStakeSum += sample;
}
}
return receivedStakeSum / samples;
}

function _getAvgUptime(uint256 validatorID, uint256 duration, uint256 step) internal view returns(uint256) {
uint256 until = _now().sub(duration);
uint256 oldUptimeCounter = 0;
uint256 newUptimeCounter = 0;
for (uint256 i = 0; i <= 30; i++) {
uint256 e = currentSealedEpoch - i * step;
EpochSnapshot storage s = getEpochSnapshot[e];
uint256 endTime = s.endTime;
if (endTime < until) {
if (i <= 2) {
return duration;
}
break;
}
uint256 uptimeCounter = s.accumulatedUptime[validatorID];
if (uptimeCounter != 0) {
oldUptimeCounter = uptimeCounter;
if (newUptimeCounter == 0) {
newUptimeCounter = uptimeCounter;
}
}
}
uint256 uptime = newUptimeCounter - oldUptimeCounter;
if (uptime > duration*4/5) {
return duration;
}
return uptime;
}

function _truncateLegacyPenalty(address delegator, uint256 toValidatorID) internal {
Rewards storage r = getStashedLockupRewards[delegator][toValidatorID];
uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2;
if (storedPenalty == 0) {
return;
}
LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID];
uint256 duration = ld.duration;
uint256 lockedStake = ld.lockedStake;
uint256 step = _getAvgEpochStep(duration);
if (step == 0) {
return;
}
uint256 RPS = _getAvgUptime(toValidatorID, duration, step).mul(2092846271).div(duration); // corresponds to 6.6% APR
uint256 selfStake = getStake[delegator][toValidatorID];

uint256 avgFullReward = selfStake.mul(RPS).mul(duration).div(1e18).mul(Decimal.unit().sub(c.validatorCommission())).div(Decimal.unit()); // reward for self-stake
if (getValidator[toValidatorID].auth == delegator) { // reward for received portion of stake
uint256 receivedStakeAvg = _getAvgReceivedStake(toValidatorID, duration, step).mul(11).div(10);
avgFullReward += receivedStakeAvg.mul(RPS).mul(duration).div(1e18).mul(c.validatorCommission()).div(Decimal.unit());
}
avgFullReward = avgFullReward.mul(lockedStake).div(selfStake);
Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration);
uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward;
maxReasonablePenalty = maxReasonablePenalty;
if (storedPenalty > maxReasonablePenalty) {
r.lockupExtraReward = r.lockupExtraReward.mul(maxReasonablePenalty).div(storedPenalty);
r.lockupBaseReward = r.lockupBaseReward.mul(maxReasonablePenalty).div(storedPenalty);
}
}
}
8 changes: 8 additions & 0 deletions contracts/test/UnitTestSFC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ contract UnitTestSFCLib is SFCLib, UnitTestSFCBase {
}
return SFCBase.isNode(addr);
}

function _getAvgEpochStep(uint256) internal view returns(uint256) {
return 1;
}

function _getAvgUptime(uint256, uint256 duration, uint256) internal view returns(uint256) {
return duration;
}
}

contract UnitTestNetworkInitializer {
Expand Down
14 changes: 7 additions & 7 deletions test/SFC.js
Original file line number Diff line number Diff line change
Expand Up @@ -1714,20 +1714,20 @@ contract('SFC', async ([firstValidator, testValidator, firstDelegator, secondDel

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

expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('1'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000474828297807395'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000237414148903697'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000004748282978073'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('1'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000380540964546690'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000190270482273344'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000003805409645466'));
await this.sfc.unlockStake(testValidator3ID, amount18('0.5'), { from: thirdDelegator });
await expectRevert(this.sfc.unlockStake(testValidator3ID, amount18('0.51'), { from: thirdDelegator }), 'not enough locked stake');
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000237414148903697'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000004748282978073'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.5'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000190270482273344'));
expect(await this.sfc.unlockStake.call(testValidator3ID, amount18('0.01'), { from: thirdDelegator })).to.be.bignumber.equal(amount18('0.000003805409645466'));

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

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

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

0 comments on commit 45fa796

Please sign in to comment.