diff --git a/hmy/staking.go b/hmy/staking.go index acc0dcfaa..448a423ee 100644 --- a/hmy/staking.go +++ b/hmy/staking.go @@ -139,6 +139,11 @@ func (hmy *Harmony) IsPreStakingEpoch(epoch *big.Int) bool { return hmy.BlockChain.Config().IsPreStaking(epoch) } +// IsNoEarlyUnlockEpoch ... +func (hmy *Harmony) IsNoEarlyUnlockEpoch(epoch *big.Int) bool { + return hmy.BlockChain.Config().IsNoEarlyUnlock(epoch) +} + // IsCommitteeSelectionBlock checks if the given block is the committee selection block func (hmy *Harmony) IsCommitteeSelectionBlock(header *block.Header) bool { return chain.IsCommitteeSelectionBlock(hmy.BlockChain, header) @@ -547,8 +552,9 @@ func (hmy *Harmony) GetUndelegationPayouts( if err != nil || wrapper == nil { continue // Not a validator at this epoch or unable to fetch validator info because of pruned state. } + noEarlyUnlock := hmy.IsNoEarlyUnlockEpoch(epoch) for _, delegation := range wrapper.Delegations { - withdraw := delegation.RemoveUnlockedUndelegations(epoch, wrapper.LastEpochInCommittee, lockingPeriod) + withdraw := delegation.RemoveUnlockedUndelegations(epoch, wrapper.LastEpochInCommittee, lockingPeriod, noEarlyUnlock) if withdraw.Cmp(bigZero) == 1 { if totalPayout, ok := undelegationPayouts[delegation.DelegatorAddress]; ok { undelegationPayouts[delegation.DelegatorAddress] = new(big.Int).Add(totalPayout, withdraw) diff --git a/internal/chain/engine.go b/internal/chain/engine.go index ea5597bc8..5019e443a 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -302,10 +302,11 @@ func payoutUndelegations( ) } lockPeriod := GetLockPeriodInEpoch(chain, header.Epoch()) + noEarlyUnlock := chain.Config().IsNoEarlyUnlock(header.Epoch()) for i := range wrapper.Delegations { delegation := &wrapper.Delegations[i] totalWithdraw := delegation.RemoveUnlockedUndelegations( - header.Epoch(), wrapper.LastEpochInCommittee, lockPeriod, + header.Epoch(), wrapper.LastEpochInCommittee, lockPeriod, noEarlyUnlock, ) state.AddBalance(delegation.DelegatorAddress, totalWithdraw) } diff --git a/internal/params/config.go b/internal/params/config.go index 9f3660362..bebde39d6 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -49,6 +49,7 @@ var ( TwoSecondsEpoch: big.NewInt(366), // Around Tuesday Dec 8th 2020, 8AM PST SixtyPercentEpoch: EpochTBD, RedelegationEpoch: big.NewInt(290), + NoEarlyUnlockEpoch: EpochTBD, EIP155Epoch: big.NewInt(28), S3Epoch: big.NewInt(28), IstanbulEpoch: big.NewInt(314), @@ -70,6 +71,7 @@ var ( TwoSecondsEpoch: big.NewInt(73000), SixtyPercentEpoch: big.NewInt(73282), RedelegationEpoch: big.NewInt(36500), + NoEarlyUnlockEpoch: big.NewInt(73580), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(43800), @@ -92,6 +94,7 @@ var ( TwoSecondsEpoch: big.NewInt(0), SixtyPercentEpoch: big.NewInt(0), RedelegationEpoch: big.NewInt(0), + NoEarlyUnlockEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -114,6 +117,7 @@ var ( TwoSecondsEpoch: big.NewInt(0), SixtyPercentEpoch: big.NewInt(0), RedelegationEpoch: big.NewInt(0), + NoEarlyUnlockEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -136,6 +140,7 @@ var ( TwoSecondsEpoch: big.NewInt(0), SixtyPercentEpoch: big.NewInt(10), RedelegationEpoch: big.NewInt(0), + NoEarlyUnlockEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -157,6 +162,7 @@ var ( TwoSecondsEpoch: big.NewInt(3), SixtyPercentEpoch: EpochTBD, // Never enable it for localnet as localnet has no external validator setup RedelegationEpoch: big.NewInt(0), + NoEarlyUnlockEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -180,6 +186,7 @@ var ( big.NewInt(0), // TwoSecondsEpoch big.NewInt(0), // SixtyPercentEpoch big.NewInt(0), // RedelegationEpoch + big.NewInt(0), // NoEarlyUnlockEpoch big.NewInt(0), // EIP155Epoch big.NewInt(0), // S3Epoch big.NewInt(0), // IstanbulEpoch @@ -203,6 +210,7 @@ var ( big.NewInt(0), // TwoSecondsEpoch big.NewInt(0), // SixtyPercentEpoch big.NewInt(0), // RedelegationEpoch + big.NewInt(0), // NoEarlyUnlockEpoch big.NewInt(0), // EIP155Epoch big.NewInt(0), // S3Epoch big.NewInt(0), // IstanbulEpoch @@ -276,6 +284,10 @@ type ChainConfig struct { // is restored to 7 epoch RedelegationEpoch *big.Int `json:"redelegation-epoch,omitempty"` + // NoEarlyUnlockEpoch is the epoch when the early unlock of undelegated token from validators who were elected for + // more than 7 epochs is disabled + NoEarlyUnlockEpoch *big.Int `json:"no-early-unlock-epoch,omitempty"` + // EIP155 hard fork epoch (include EIP158 too) EIP155Epoch *big.Int `json:"eip155-epoch,omitempty"` @@ -357,6 +369,11 @@ func (c *ChainConfig) IsRedelegation(epoch *big.Int) bool { return isForked(c.RedelegationEpoch, epoch) } +// IsNoEarlyUnlock determines whether it is the epoch to stop early unlock +func (c *ChainConfig) IsNoEarlyUnlock(epoch *big.Int) bool { + return isForked(c.NoEarlyUnlockEpoch, epoch) +} + // IsPreStaking determines whether staking transactions are allowed func (c *ChainConfig) IsPreStaking(epoch *big.Int) bool { return isForked(c.PreStakingEpoch, epoch) diff --git a/staking/types/delegation.go b/staking/types/delegation.go index 786a06eba..9f0a0c622 100644 --- a/staking/types/delegation.go +++ b/staking/types/delegation.go @@ -178,13 +178,13 @@ func (d *Delegation) DeleteEntry(epoch *big.Int) { // RemoveUnlockedUndelegations removes all fully unlocked // undelegations and returns the total sum func (d *Delegation) RemoveUnlockedUndelegations( - curEpoch, lastEpochInCommittee *big.Int, lockPeriod int, + curEpoch, lastEpochInCommittee *big.Int, lockPeriod int, noEarlyUnlock bool, ) *big.Int { totalWithdraw := big.NewInt(0) count := 0 for j := range d.Undelegations { if big.NewInt(0).Sub(curEpoch, d.Undelegations[j].Epoch).Int64() >= int64(lockPeriod) || - big.NewInt(0).Sub(curEpoch, lastEpochInCommittee).Int64() >= int64(lockPeriod) { + (!noEarlyUnlock && big.NewInt(0).Sub(curEpoch, lastEpochInCommittee).Int64() >= int64(lockPeriod)) { // need to wait at least 7 epochs to withdraw; or the validator has been out of committee for 7 epochs totalWithdraw.Add(totalWithdraw, d.Undelegations[j].Amount) count++ diff --git a/staking/types/delegation_test.go b/staking/types/delegation_test.go index 104ad9044..6c9b49f4d 100644 --- a/staking/types/delegation_test.go +++ b/staking/types/delegation_test.go @@ -75,7 +75,7 @@ func TestUnlockedLastEpochInCommittee(t *testing.T) { amount4 := big.NewInt(4000) delegation.Undelegate(epoch4, amount4) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) if result.Cmp(big.NewInt(8000)) != 0 { t.Errorf("removing an unlocked undelegation fails") } @@ -90,7 +90,7 @@ func TestUnlockedLastEpochInCommitteeFail(t *testing.T) { amount4 := big.NewInt(4000) delegation.Undelegate(epoch4, amount4) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("premature delegation shouldn't be unlocked") } @@ -104,7 +104,7 @@ func TestUnlockedFullPeriod(t *testing.T) { amount5 := big.NewInt(4000) delegation.Undelegate(epoch5, amount5) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) if result.Cmp(big.NewInt(4000)) != 0 { t.Errorf("removing an unlocked undelegation fails") } @@ -118,7 +118,7 @@ func TestQuickUnlock(t *testing.T) { amount7 := big.NewInt(4000) delegation.Undelegate(epoch7, amount7) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 0) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 0, false) if result.Cmp(big.NewInt(4000)) != 0 { t.Errorf("removing an unlocked undelegation fails") } @@ -133,7 +133,7 @@ func TestUnlockedFullPeriodFail(t *testing.T) { amount5 := big.NewInt(4000) delegation.Undelegate(epoch5, amount5) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("premature delegation shouldn't be unlocked") } @@ -147,8 +147,22 @@ func TestUnlockedPremature(t *testing.T) { amount6 := big.NewInt(4000) delegation.Undelegate(epoch6, amount6) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("premature delegation shouldn't be unlocked") } } + +func TestNoEarlyUnlock(t *testing.T) { + lastEpochInCommittee := big.NewInt(17) + curEpoch := big.NewInt(24) + + epoch4 := big.NewInt(21) + amount4 := big.NewInt(4000) + delegation.Undelegate(epoch4, amount4) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) + if result.Cmp(big.NewInt(0)) != 0 { + t.Errorf("should not allow early unlock") + } +}