diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs index 789736d50f..9451670e5b 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs @@ -231,7 +231,7 @@ var expectedProposerReward var validatorAddress = vote.ValidatorPublicKey.Address; var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorAddress); - var validatorRewardAddress = actualDelegatee.CurrentLumpSumRewardsRecordAddress(); + var validatorRewardAddress = actualDelegatee.DistributionPoolAddress(); var actualDelegationBalance = world.GetBalance(validatorAddress, DelegationCurrency); var actualCommission = world.GetBalance(validatorAddress, GuildAllocateRewardCurrency); var actualUnclaimedReward = world.GetBalance(validatorRewardAddress, GuildAllocateRewardCurrency); diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs index 8105c0839f..e1b05ca90c 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs @@ -25,6 +25,7 @@ namespace Lib9c.Tests.Action.ValidatorDelegation; using Nekoyume.ValidatorDelegation; using Xunit; using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Delegation; public class ValidatorDelegationTestBase { @@ -592,7 +593,10 @@ protected static FungibleAssetValue CalculateBonusPropserReward( } protected static FungibleAssetValue CalculateClaim(BigInteger share, BigInteger totalShare, FungibleAssetValue totalClaim) - => (totalClaim * share).DivRem(totalShare).Quotient; + { + var multiplier = BigInteger.Pow(10, (int)Math.Floor(BigInteger.Log10(totalShare)) + RewardBase.Margin); + return ((totalClaim * multiplier).DivRem(totalShare).Quotient * share).DivRem(multiplier).Quotient; + } protected static FungibleAssetValue CalculateCommunityFund(ImmutableArray votes, FungibleAssetValue reward) { diff --git a/.Lib9c.Tests/Delegation/DelegationFixture.cs b/.Lib9c.Tests/Delegation/DelegationFixture.cs index 961775d217..ceab140717 100644 --- a/.Lib9c.Tests/Delegation/DelegationFixture.cs +++ b/.Lib9c.Tests/Delegation/DelegationFixture.cs @@ -40,12 +40,18 @@ public DelegationFixture() new Address("0x67A44E11506b8f0Bb625fEECccb205b33265Bb48"), TestRepository.DelegateeAccountAddress, TestRepository); TestDelegatee2 = new TestDelegatee( new Address("0xea1C4eedEfC99691DEfc6eF2753FAfa8C17F4584"), TestRepository.DelegateeAccountAddress, TestRepository); + TestRepository.SetDelegator(TestDelegator1); + TestRepository.SetDelegator(TestDelegator2); + TestRepository.SetDelegatee(TestDelegatee1); + TestRepository.SetDelegatee(TestDelegatee2); DummyRepository = new DummyRepository(world, context); DummyDelegatee1 = new DummyDelegatee( new Address("0x67A44E11506b8f0Bb625fEECccb205b33265Bb48"), DummyRepository.DelegateeAccountAddress, DummyRepository); DummyDelegator1 = new DummyDelegator( new Address("0x0054E98312C47E7Fa0ABed45C23Fa187e31C373a"), DummyRepository.DelegateeAccountAddress, DummyRepository); + DummyRepository.SetDelegator(DummyDelegator1); + DummyRepository.SetDelegatee(DummyDelegatee1); } public TestRepository TestRepository { get; } diff --git a/.Lib9c.Tests/Delegation/DelegatorTest.cs b/.Lib9c.Tests/Delegation/DelegatorTest.cs index 8533601a90..65bc9df9df 100644 --- a/.Lib9c.Tests/Delegation/DelegatorTest.cs +++ b/.Lib9c.Tests/Delegation/DelegatorTest.cs @@ -328,13 +328,16 @@ public void RewardOnDelegate() delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); var delegator1RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator1.Address, c)); - var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + var collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + var legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1 * 2, delegator1Balance); Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); Assert.Equal(rewards1, delegator1RewardBalances); - Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f - s), collectedRewards); + Assert.Equal(rewards, legacyRewards); delegator2.Delegate(delegatee, delegatingFAV2, 11L); delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); @@ -342,7 +345,9 @@ public void RewardOnDelegate() c => repo.World.GetBalance(delegator1.Address, c)); var delegator2RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator2.Address, c)); - collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1 * 2, delegator1Balance); @@ -353,8 +358,9 @@ public void RewardOnDelegate() // Flushing to remainder pool is now inactive. // Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); Assert.Equal( - rewards.Select(r => r * 2).Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), + rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); + Assert.Equal(rewards, legacyRewards); } [Fact] @@ -374,7 +380,7 @@ public void RewardOnUndelegate() repo.MintAsset(delegatee.RewardPoolAddress, reward); } - // EndBlock after delegatee's reward + // BeginBlock after delegatee's reward delegatee.CollectRewards(10L); var delegatingFAV1 = delegatee.DelegationCurrency * 10; @@ -398,21 +404,24 @@ public void RewardOnUndelegate() repo.MintAsset(delegatee.RewardPoolAddress, reward); } - // EndBlock after delegatee's reward - delegatee.CollectRewards(10L); + // BeginBlock after delegatee's reward + delegatee.CollectRewards(11L); var shareToUndelegate = repo.GetBond(delegatee, delegator1.Address).Share / 3; delegator1.Undelegate(delegatee, shareToUndelegate, 11L); delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); var delegator1RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator1.Address, c)); - var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + var collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + var legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); Assert.Equal(rewards1, delegator1RewardBalances); - Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f - s), collectedRewards); + Assert.Equal(rewards, legacyRewards); shareToUndelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; delegator2.Undelegate(delegatee, shareToUndelegate, 11L); @@ -421,7 +430,9 @@ public void RewardOnUndelegate() c => repo.World.GetBalance(delegator1.Address, c)); var delegator2RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator2.Address, c)); - collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); @@ -430,10 +441,12 @@ public void RewardOnUndelegate() Assert.Equal(rewards2, delegator2RewardBalances); // Flushing to remainder pool is now inactive. - // Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); Assert.Equal( - rewards.Select(r => r * 2).Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), + rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); + Assert.Equal( + rewards, + legacyRewards); } [Fact] @@ -486,13 +499,16 @@ public void RewardOnRedelegate() delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); var delegator1RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator1.Address, c)); - var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + var collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + var legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); Assert.Equal(rewards1, delegator1RewardBalances); - Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f - s), collectedRewards); + Assert.Equal(rewards, legacyRewards); shareToRedelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; delegator2.Redelegate(delegatee, dstDelegatee, shareToRedelegate, 11L); @@ -501,7 +517,9 @@ public void RewardOnRedelegate() c => repo.World.GetBalance(delegator1.Address, c)); var delegator2RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator2.Address, c)); - collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); @@ -512,8 +530,9 @@ public void RewardOnRedelegate() // Flushing to remainder pool is now inactive. // Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); Assert.Equal( - rewards.Select(r => r * 2).Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), + rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); + Assert.Equal(rewards, legacyRewards); } [Fact] @@ -566,13 +585,16 @@ public void RewardOnClaim() delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); var delegator1RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator1.Address, c)); - var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + var collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + var legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); Assert.Equal(rewards1, delegator1RewardBalances); - Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f - s), collectedRewards); + Assert.Equal(rewards, legacyRewards); shareToRedelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; delegator2.ClaimReward(delegatee, 11L); @@ -581,7 +603,9 @@ public void RewardOnClaim() c => repo.World.GetBalance(delegator1.Address, c)); var delegator2RewardBalances = delegatee.RewardCurrencies.Select( c => repo.World.GetBalance(delegator2.Address, c)); - collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + collectedRewards = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegatee.DistributionPoolAddress(), c)); + legacyRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); @@ -592,8 +616,9 @@ public void RewardOnClaim() // Flushing to remainder pool is now inactive. // Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); Assert.Equal( - rewards.Select(r => r * 2).Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), + rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); + Assert.Equal(rewards, legacyRewards); } } } diff --git a/.Lib9c.Tests/Delegation/DummyRepository.cs b/.Lib9c.Tests/Delegation/DummyRepository.cs index 85332a4785..2293822e19 100644 --- a/.Lib9c.Tests/Delegation/DummyRepository.cs +++ b/.Lib9c.Tests/Delegation/DummyRepository.cs @@ -21,7 +21,8 @@ public DummyRepository(IWorld world, IActionContext context) unbondLockInAccountAddress: new Address("0000000000000000000000000000000000000005"), rebondGraceAccountAddress: new Address("0000000000000000000000000000000000000006"), unbondingSetAccountAddress: new Address("0000000000000000000000000000000000000007"), - lumpSumRewardRecordAccountAddress: new Address("0000000000000000000000000000000000000008")) + rewardBaseAccountAddress: new Address("0000000000000000000000000000000000000008"), + lumpSumRewardRecordAccountAddress: new Address("0000000000000000000000000000000000000009")) { } diff --git a/.Lib9c.Tests/Delegation/Migration/LegacyDelegateeMetadata.cs b/.Lib9c.Tests/Delegation/Migration/LegacyDelegateeMetadata.cs deleted file mode 100644 index 0cf0b90457..0000000000 --- a/.Lib9c.Tests/Delegation/Migration/LegacyDelegateeMetadata.cs +++ /dev/null @@ -1,221 +0,0 @@ -#nullable enable -namespace Lib9c.Tests.Delegation.Migration -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Numerics; - using Bencodex; - using Bencodex.Types; - using Libplanet.Crypto; - using Libplanet.Types.Assets; - using Nekoyume.Delegation; - - public class LegacyDelegateeMetadata : IDelegateeMetadata - { - private readonly IComparer _currencyComparer = new CurrencyComparer(); - private Address? _address; - - public LegacyDelegateeMetadata( - Address delegateeAddress, - Address delegateeAccountAddress, - Currency delegationCurrency, - IEnumerable rewardCurrencies, - Address delegationPoolAddress, - Address rewardPoolAddress, - Address rewardRemainderPoolAddress, - Address slashedPoolAddress, - long unbondingPeriod, - int maxUnbondLockInEntries, - int maxRebondGraceEntries) - : this( - delegateeAddress, - delegateeAccountAddress, - delegationCurrency, - rewardCurrencies, - delegationPoolAddress, - rewardPoolAddress, - rewardRemainderPoolAddress, - slashedPoolAddress, - unbondingPeriod, - maxUnbondLockInEntries, - maxRebondGraceEntries, - ImmutableSortedSet
.Empty, - delegationCurrency * 0, - BigInteger.Zero, - false, - -1L, - false, - ImmutableSortedSet.Empty) - { - } - - public LegacyDelegateeMetadata( - Address delegateeAddress, - Address delegateeAccountAddress, - IValue bencoded) - : this(delegateeAddress, delegateeAccountAddress, (List)bencoded) - { - } - - public LegacyDelegateeMetadata( - Address address, - Address accountAddress, - List bencoded) - : this( - address, - accountAddress, - new Currency(bencoded[0]), - ((List)bencoded[1]).Select(v => new Currency(v)), - new Address(bencoded[2]), - new Address(bencoded[3]), - new Address(bencoded[4]), - new Address(bencoded[5]), - (Integer)bencoded[6], - (Integer)bencoded[7], - (Integer)bencoded[8], - ((List)bencoded[9]).Select(item => new Address(item)), - new FungibleAssetValue(bencoded[10]), - (Integer)bencoded[11], - (Bencodex.Types.Boolean)bencoded[12], - (Integer)bencoded[13], - (Bencodex.Types.Boolean)bencoded[14], - ((List)bencoded[15]).Select(item => new UnbondingRef(item))) - { - } - - private LegacyDelegateeMetadata( - Address delegateeAddress, - Address delegateeAccountAddress, - Currency delegationCurrency, - IEnumerable rewardCurrencies, - Address delegationPoolAddress, - Address rewardPoolAddress, - Address rewardRemainderPoolAddress, - Address slashedPoolAddress, - long unbondingPeriod, - int maxUnbondLockInEntries, - int maxRebondGraceEntries, - IEnumerable
delegators, - FungibleAssetValue totalDelegated, - BigInteger totalShares, - bool jailed, - long jailedUntil, - bool tombstoned, - IEnumerable unbondingRefs) - { - if (!totalDelegated.Currency.Equals(delegationCurrency)) - { - throw new InvalidOperationException("Invalid currency."); - } - - if (totalDelegated.Sign < 0) - { - throw new ArgumentOutOfRangeException( - nameof(totalDelegated), - totalDelegated, - "Total delegated must be non-negative."); - } - - if (totalShares.Sign < 0) - { - throw new ArgumentOutOfRangeException( - nameof(totalShares), - totalShares, - "Total shares must be non-negative."); - } - - DelegateeAddress = delegateeAddress; - DelegateeAccountAddress = delegateeAccountAddress; - DelegationCurrency = delegationCurrency; - RewardCurrencies = rewardCurrencies.ToImmutableSortedSet(_currencyComparer); - DelegationPoolAddress = delegationPoolAddress; - RewardPoolAddress = rewardPoolAddress; - RewardRemainderPoolAddress = rewardRemainderPoolAddress; - SlashedPoolAddress = slashedPoolAddress; - UnbondingPeriod = unbondingPeriod; - MaxUnbondLockInEntries = maxUnbondLockInEntries; - MaxRebondGraceEntries = maxRebondGraceEntries; - Delegators = delegators.ToImmutableSortedSet(); - TotalDelegatedFAV = totalDelegated; - TotalShares = totalShares; - Jailed = jailed; - JailedUntil = jailedUntil; - Tombstoned = tombstoned; - UnbondingRefs = unbondingRefs.ToImmutableSortedSet(); - } - - public Address DelegateeAddress { get; } - - public Address DelegateeAccountAddress { get; } - - public Address Address - => _address ??= DelegationAddress.DelegateeMetadataAddress( - DelegateeAddress, - DelegateeAccountAddress); - - public Currency DelegationCurrency { get; } - - public ImmutableSortedSet RewardCurrencies { get; } - - public Address DelegationPoolAddress { get; internal set; } - - public Address RewardPoolAddress { get; } - - public Address RewardRemainderPoolAddress { get; } - - public Address SlashedPoolAddress { get; } - - public long UnbondingPeriod { get; private set; } - - public int MaxUnbondLockInEntries { get; } - - public int MaxRebondGraceEntries { get; } - - public ImmutableSortedSet
Delegators { get; private set; } - - public FungibleAssetValue TotalDelegatedFAV { get; private set; } - - public BigInteger TotalShares { get; private set; } - - public bool Jailed { get; internal set; } - - public long JailedUntil { get; internal set; } - - public bool Tombstoned { get; internal set; } - - public ImmutableSortedSet UnbondingRefs { get; private set; } - - // TODO : Better serialization - public List Bencoded => List.Empty - .Add(DelegationCurrency.Serialize()) - .Add(new List(RewardCurrencies.Select(c => c.Serialize()))) - .Add(DelegationPoolAddress.Bencoded) - .Add(RewardPoolAddress.Bencoded) - .Add(RewardRemainderPoolAddress.Bencoded) - .Add(SlashedPoolAddress.Bencoded) - .Add(UnbondingPeriod) - .Add(MaxUnbondLockInEntries) - .Add(MaxRebondGraceEntries) - .Add(new List(Delegators.Select(delegator => delegator.Bencoded))) - .Add(TotalDelegatedFAV.Serialize()) - .Add(TotalShares) - .Add(Jailed) - .Add(JailedUntil) - .Add(Tombstoned) - .Add(new List(UnbondingRefs.Select(unbondingRef => unbondingRef.Bencoded))); - - IValue IBencodable.Bencoded => Bencoded; - - public BigInteger ShareFromFAV(FungibleAssetValue fav) - => TotalShares.IsZero - ? fav.RawValue - : TotalShares * fav.RawValue / TotalDelegatedFAV.RawValue; - - public FungibleAssetValue FAVFromShare(BigInteger share) - => TotalShares == share - ? TotalDelegatedFAV - : (TotalDelegatedFAV * share).DivRem(TotalShares).Quotient; - } -} diff --git a/.Lib9c.Tests/Delegation/Migration/LegacyLumpSumRewardsRecord.cs b/.Lib9c.Tests/Delegation/Migration/LegacyLumpSumRewardsRecord.cs deleted file mode 100644 index abb6be9f37..0000000000 --- a/.Lib9c.Tests/Delegation/Migration/LegacyLumpSumRewardsRecord.cs +++ /dev/null @@ -1,138 +0,0 @@ -#nullable enable -namespace Lib9c.Tests.Delegation.Migration -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Numerics; - using Bencodex; - using Bencodex.Types; - using Libplanet.Crypto; - using Libplanet.Types.Assets; - using Nekoyume.Delegation; - - public class LegacyLumpSumRewardsRecord : IBencodable - { - private readonly IComparer _currencyComparer = new CurrencyComparer(); - - public LegacyLumpSumRewardsRecord( - Address address, - long startHeight, - BigInteger totalShares, - ImmutableSortedSet
delegators, - IEnumerable currencies) - : this( - address, - startHeight, - totalShares, - delegators, - currencies, - null) - { - } - - public LegacyLumpSumRewardsRecord( - Address address, - long startHeight, - BigInteger totalShares, - ImmutableSortedSet
delegators, - IEnumerable currencies, - long? lastStartHeight) - : this( - address, - startHeight, - totalShares, - delegators, - currencies.Select(c => c * 0), - lastStartHeight) - { - } - - public LegacyLumpSumRewardsRecord( - Address address, - long startHeight, - BigInteger totalShares, - ImmutableSortedSet
delegators, - IEnumerable lumpSumRewards, - long? lastStartHeight) - { - Address = address; - StartHeight = startHeight; - TotalShares = totalShares; - Delegators = delegators; - - if (!lumpSumRewards.Select(f => f.Currency).All(new HashSet().Add)) - { - throw new ArgumentException("Duplicated currency in lump sum rewards."); - } - - LumpSumRewards = lumpSumRewards.ToImmutableDictionary(f => f.Currency, f => f); - LastStartHeight = lastStartHeight; - } - - public LegacyLumpSumRewardsRecord(Address address, IValue bencoded) - : this(address, (List)bencoded) - { - } - - public LegacyLumpSumRewardsRecord(Address address, List bencoded) - : this( - address, - (Integer)bencoded[0], - (Integer)bencoded[1], - ((List)bencoded[2]).Select(a => new Address(a)).ToImmutableSortedSet(), - ((List)bencoded[3]).Select(v => new FungibleAssetValue(v)), - (Integer?)bencoded.ElementAtOrDefault(4)) - { - } - - private LegacyLumpSumRewardsRecord( - Address address, - long startHeight, - BigInteger totalShares, - ImmutableSortedSet
delegators, - ImmutableDictionary lumpSumRewards, - long? lastStartHeight) - { - Address = address; - StartHeight = startHeight; - TotalShares = totalShares; - Delegators = delegators; - LumpSumRewards = lumpSumRewards; - LastStartHeight = lastStartHeight; - } - - public Address Address { get; } - - public long StartHeight { get; } - - public BigInteger TotalShares { get; } - - public ImmutableDictionary LumpSumRewards { get; } - - public ImmutableSortedSet
Delegators { get; } - - public long? LastStartHeight { get; } - - public List Bencoded - { - get - { - var bencoded = List.Empty - .Add(StartHeight) - .Add(TotalShares) - .Add(new List(Delegators.Select(a => a.Bencoded))) - .Add(new List(LumpSumRewards - .OrderBy(r => r.Key, _currencyComparer) - .Select(r => r.Value.Serialize()))); - - return LastStartHeight is long lastStartHeight - ? bencoded.Add(lastStartHeight) - : bencoded; - } - } - - IValue IBencodable.Bencoded => Bencoded; - } -} diff --git a/.Lib9c.Tests/Delegation/Migration/LegacyTestDelegatee.cs b/.Lib9c.Tests/Delegation/Migration/LegacyTestDelegatee.cs new file mode 100644 index 0000000000..ef76cd00bc --- /dev/null +++ b/.Lib9c.Tests/Delegation/Migration/LegacyTestDelegatee.cs @@ -0,0 +1,471 @@ +#nullable enable +namespace Lib9c.Tests.Delegation.Migration +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Numerics; + using Bencodex.Types; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Delegation; + + public class LegacyTestDelegatee : IDelegatee + { + public LegacyTestDelegatee( + Address address, + Address accountAddress, + Currency delegationCurrency, + IEnumerable rewardCurrencies, + Address delegationPoolAddress, + Address rewardPoolAddress, + Address rewardRemainderPoolAddress, + Address slashedPoolAddress, + long unbondingPeriod, + int maxUnbondLockInEntries, + int maxRebondGraceEntries, + IDelegationRepository repository) + : this( + new DelegateeMetadata( + address, + accountAddress, + delegationCurrency, + rewardCurrencies, + delegationPoolAddress, + rewardPoolAddress, + rewardRemainderPoolAddress, + slashedPoolAddress, + unbondingPeriod, + maxUnbondLockInEntries, + maxRebondGraceEntries), + repository) + { + } + + public LegacyTestDelegatee( + Address address, + IDelegationRepository repository) + : this(repository.GetDelegateeMetadata(address), repository) + { + } + + private LegacyTestDelegatee(DelegateeMetadata metadata, IDelegationRepository repository) + { + Metadata = metadata; + Repository = repository; + } + + public event EventHandler? DelegationChanged; + + public event EventHandler? Enjailed; + + public event EventHandler? Unjailed; + + public DelegateeMetadata Metadata { get; } + + public IDelegationRepository Repository { get; } + + public Address Address => Metadata.DelegateeAddress; + + public Address AccountAddress => Metadata.DelegateeAccountAddress; + + public Address MetadataAddress => Metadata.Address; + + public Currency DelegationCurrency => Metadata.DelegationCurrency; + + public ImmutableSortedSet RewardCurrencies => Metadata.RewardCurrencies; + + public Address DelegationPoolAddress => Metadata.DelegationPoolAddress; + + public Address RewardPoolAddress => Metadata.RewardPoolAddress; + + public Address RewardRemainderPoolAddress => Metadata.RewardRemainderPoolAddress; + + public Address SlashedPoolAddress => Metadata.SlashedPoolAddress; + + public long UnbondingPeriod => Metadata.UnbondingPeriod; + + public int MaxUnbondLockInEntries => Metadata.MaxUnbondLockInEntries; + + public int MaxRebondGraceEntries => Metadata.MaxRebondGraceEntries; + + public FungibleAssetValue TotalDelegated => Metadata.TotalDelegatedFAV; + + public BigInteger TotalShares => Metadata.TotalShares; + + public bool Jailed => Metadata.Jailed; + + public long JailedUntil => Metadata.JailedUntil; + + public bool Tombstoned => Metadata.Tombstoned; + + public List MetadataBencoded => Metadata.Bencoded; + + public BigInteger ShareFromFAV(FungibleAssetValue fav) + => Metadata.ShareFromFAV(fav); + + public FungibleAssetValue FAVFromShare(BigInteger share) + => Metadata.FAVFromShare(share); + + public BigInteger Bond(IDelegator delegator, FungibleAssetValue fav, long height) + => Bond((LegacyTestDelegator)delegator, fav, height); + + public FungibleAssetValue Unbond(IDelegator delegator, BigInteger share, long height) + => Unbond((LegacyTestDelegator)delegator, share, height); + + public void DistributeReward(IDelegator delegator, long height) + => DistributeReward((LegacyTestDelegator)delegator, height); + + public void Jail(long releaseHeight) + { + Metadata.JailedUntil = releaseHeight; + Metadata.Jailed = true; + Repository.SetDelegateeMetadata(Metadata); + Enjailed?.Invoke(this, EventArgs.Empty); + } + + public void Unjail(long height) + { + if (!Jailed) + { + throw new InvalidOperationException("Cannot unjail non-jailed delegatee."); + } + + if (Tombstoned) + { + throw new InvalidOperationException("Cannot unjail tombstoned delegatee."); + } + + if (JailedUntil >= height) + { + throw new InvalidOperationException("Cannot unjail before jailed until."); + } + + Metadata.JailedUntil = -1L; + Metadata.Jailed = false; + Repository.SetDelegateeMetadata(Metadata); + Unjailed?.Invoke(this, EventArgs.Empty); + } + + public void Tombstone() + { + Jail(long.MaxValue); + Metadata.Tombstoned = true; + Repository.SetDelegateeMetadata(Metadata); + } + + public Address BondAddress(Address delegatorAddress) + => Metadata.BondAddress(delegatorAddress); + + public Address UnbondLockInAddress(Address delegatorAddress) + => Metadata.UnbondLockInAddress(delegatorAddress); + + public Address RebondGraceAddress(Address delegatorAddress) + => Metadata.RebondGraceAddress(delegatorAddress); + + public Address CurrentLumpSumRewardsRecordAddress() + => Metadata.CurrentLumpSumRewardsRecordAddress(); + + public Address LumpSumRewardsRecordAddress(long height) + => Metadata.LumpSumRewardsRecordAddress(height); + + public Address CurrentRewardBaseAddress() => throw new NotImplementedException(); + + public Address RewardBaseAddress(long height) => throw new NotImplementedException(); + + public BigInteger Bond(LegacyTestDelegator delegator, FungibleAssetValue fav, long height) + { + DistributeReward(delegator, height); + + if (!fav.Currency.Equals(DelegationCurrency)) + { + throw new InvalidOperationException( + "Cannot bond with invalid currency."); + } + + if (Tombstoned) + { + throw new InvalidOperationException( + "Cannot bond to tombstoned delegatee."); + } + + Bond bond = Repository.GetBond(this, delegator.Address); + BigInteger share = ShareFromFAV(fav); + bond = bond.AddShare(share); + Metadata.AddShare(share); + Metadata.AddDelegatedFAV(fav); + Repository.SetBond(bond); + StartNewRewardPeriod(height); + Repository.SetDelegateeMetadata(Metadata); + DelegationChanged?.Invoke(this, height); + + return share; + } + + BigInteger IDelegatee.Bond(IDelegator delegator, FungibleAssetValue fav, long height) + => Bond((LegacyTestDelegator)delegator, fav, height); + + public FungibleAssetValue Unbond(LegacyTestDelegator delegator, BigInteger share, long height) + { + DistributeReward(delegator, height); + if (TotalShares.IsZero || TotalDelegated.RawValue.IsZero) + { + throw new InvalidOperationException( + "Cannot unbond without bonding."); + } + + Bond bond = Repository!.GetBond(this, delegator.Address); + FungibleAssetValue fav = FAVFromShare(share); + bond = bond.SubtractShare(share); + if (bond.Share.IsZero) + { + bond = bond.ClearLastDistributeHeight(); + } + + Metadata.RemoveShare(share); + Metadata.RemoveDelegatedFAV(fav); + Repository.SetBond(bond); + StartNewRewardPeriod(height); + Repository.SetDelegateeMetadata(Metadata); + DelegationChanged?.Invoke(this, height); + + return fav; + } + + FungibleAssetValue IDelegatee.Unbond(IDelegator delegator, BigInteger share, long height) + => Unbond((LegacyTestDelegator)delegator, share, height); + + public void DistributeReward(LegacyTestDelegator delegator, long height) + { + Bond bond = Repository.GetBond(this, delegator.Address); + BigInteger share = bond.Share; + + if (!share.IsZero && bond.LastDistributeHeight.HasValue) + { + IEnumerable lumpSumRewardsRecords + = GetLumpSumRewardsRecords(bond.LastDistributeHeight); + + foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) + { + TransferReward(delegator, share, record); + // TransferRemainders(newRecord); + Repository.SetLumpSumRewardsRecord(record); + } + } + + if (bond.LastDistributeHeight != height) + { + bond = bond.UpdateLastDistributeHeight(height); + } + + Repository.SetBond(bond); + } + + void IDelegatee.DistributeReward(IDelegator delegator, long height) + => DistributeReward((LegacyTestDelegator)delegator, height); + + public void CollectRewards(long height) + { + var rewards = RewardCurrencies.Select(c => Repository.GetBalance(RewardPoolAddress, c)); + LumpSumRewardsRecord record = Repository.GetCurrentLumpSumRewardsRecord(this) + ?? new LumpSumRewardsRecord( + CurrentLumpSumRewardsRecordAddress(), + height, + TotalShares, + RewardCurrencies); + record = record.AddLumpSumRewards(rewards); + + foreach (var rewardsEach in rewards) + { + if (rewardsEach.Sign > 0) + { + Repository.TransferAsset(RewardPoolAddress, record.Address, rewardsEach); + } + } + + Repository.SetLumpSumRewardsRecord(record); + } + + public virtual void Slash(BigInteger slashFactor, long infractionHeight, long height) + { + FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem); + if (rem.Sign > 0) + { + slashed += FungibleAssetValue.FromRawValue(rem.Currency, 1); + } + + if (slashed > Metadata.TotalDelegatedFAV) + { + slashed = Metadata.TotalDelegatedFAV; + } + + Metadata.RemoveDelegatedFAV(slashed); + + foreach (var item in Metadata.UnbondingRefs) + { + var unbonding = UnbondingFactory.GetUnbondingFromRef(item, Repository); + + unbonding = unbonding.Slash(slashFactor, infractionHeight, height, out var slashedFAV); + + if (slashedFAV.HasValue) + { + slashed += slashedFAV.Value; + } + + if (unbonding.IsEmpty) + { + Metadata.RemoveUnbondingRef(item); + } + + switch (unbonding) + { + case UnbondLockIn unbondLockIn: + Repository.SetUnbondLockIn(unbondLockIn); + break; + case RebondGrace rebondGrace: + Repository.SetRebondGrace(rebondGrace); + break; + default: + throw new InvalidOperationException("Invalid unbonding type."); + } + } + + var delegationBalance = Repository.GetBalance(DelegationPoolAddress, DelegationCurrency); + if (delegationBalance < slashed) + { + slashed = delegationBalance; + } + + if (slashed > DelegationCurrency * 0) + { + Repository.TransferAsset(DelegationPoolAddress, SlashedPoolAddress, slashed); + } + + Repository.SetDelegateeMetadata(Metadata); + DelegationChanged?.Invoke(this, height); + } + + void IDelegatee.Slash(BigInteger slashFactor, long infractionHeight, long height) + => Slash(slashFactor, infractionHeight, height); + + public void AddUnbondingRef(UnbondingRef reference) + => Metadata.AddUnbondingRef(reference); + + public void RemoveUnbondingRef(UnbondingRef reference) + => Metadata.RemoveUnbondingRef(reference); + + public ImmutableDictionary CalculateReward( + BigInteger share, + IEnumerable lumpSumRewardsRecords) + { + ImmutableDictionary reward + = RewardCurrencies.ToImmutableDictionary(c => c, c => c * 0); + + foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) + { + var rewardDuringPeriod = record.RewardsDuringPeriod(share); + reward = rewardDuringPeriod.Aggregate(reward, (acc, pair) + => acc.SetItem(pair.Key, acc[pair.Key] + pair.Value)); + } + + return reward; + } + + private void StartNewRewardPeriod(long height) + { + LumpSumRewardsRecord? currentRecord = Repository.GetCurrentLumpSumRewardsRecord(this); + long? lastStartHeight = null; + if (currentRecord is LumpSumRewardsRecord lastRecord) + { + lastStartHeight = lastRecord.StartHeight; + if (lastStartHeight == height) + { + currentRecord = new ( + currentRecord.Address, + currentRecord.StartHeight, + TotalShares, + RewardCurrencies, + currentRecord.LastStartHeight); + + Repository.SetLumpSumRewardsRecord(currentRecord); + return; + } + + Address archiveAddress = LumpSumRewardsRecordAddress(lastRecord.StartHeight); + + foreach (var rewardCurrency in RewardCurrencies) + { + FungibleAssetValue reward = Repository.GetBalance(lastRecord.Address, rewardCurrency); + if (reward.Sign > 0) + { + Repository.TransferAsset(lastRecord.Address, archiveAddress, reward); + } + } + + lastRecord = lastRecord.MoveAddress(archiveAddress); + Repository.SetLumpSumRewardsRecord(lastRecord); + } + + LumpSumRewardsRecord newRecord = new ( + CurrentLumpSumRewardsRecordAddress(), + height, + TotalShares, + RewardCurrencies, + lastStartHeight); + + Repository.SetLumpSumRewardsRecord(newRecord); + } + + private List GetLumpSumRewardsRecords(long? lastRewardHeight) + { + List records = new (); + if (lastRewardHeight is null + || !(Repository.GetCurrentLumpSumRewardsRecord(this) is LumpSumRewardsRecord record)) + { + return records; + } + + while (record.StartHeight >= lastRewardHeight) + { + records.Add(record); + + if (!(record.LastStartHeight is long lastStartHeight)) + { + break; + } + + record = Repository.GetLumpSumRewardsRecord(this, lastStartHeight) + ?? throw new InvalidOperationException( + $"Lump sum rewards record for #{lastStartHeight} is missing"); + } + + return records; + } + + private void TransferReward(LegacyTestDelegator delegator, BigInteger share, LumpSumRewardsRecord record) + { + ImmutableSortedDictionary reward = record.RewardsDuringPeriod(share); + foreach (var rewardEach in reward) + { + if (rewardEach.Value.Sign > 0) + { + Repository.TransferAsset(record.Address, delegator.RewardAddress, rewardEach.Value); + } + } + } + + private void TransferRemainders(LumpSumRewardsRecord record) + { + foreach (var rewardCurrency in RewardCurrencies) + { + FungibleAssetValue remainder = Repository.GetBalance(record.Address, rewardCurrency); + + if (remainder.Sign > 0) + { + Repository.TransferAsset(record.Address, RewardRemainderPoolAddress, remainder); + } + } + } + } +} diff --git a/.Lib9c.Tests/Delegation/Migration/LegacyTestDelegator.cs b/.Lib9c.Tests/Delegation/Migration/LegacyTestDelegator.cs new file mode 100644 index 0000000000..a2ab3ade94 --- /dev/null +++ b/.Lib9c.Tests/Delegation/Migration/LegacyTestDelegator.cs @@ -0,0 +1,228 @@ +#nullable enable +namespace Lib9c.Tests.Delegation.Migration +{ + using System; + using System.Collections.Immutable; + using System.Numerics; + using Bencodex.Types; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Delegation; + + public class LegacyTestDelegator : IDelegator + { + public LegacyTestDelegator( + Address address, + Address accountAddress, + Address delegationPoolAddress, + Address rewardAddress, + IDelegationRepository repository) + : this( + new DelegatorMetadata( + address, + accountAddress, + delegationPoolAddress, + rewardAddress), + repository) + { + } + + public LegacyTestDelegator( + Address address, + IDelegationRepository repository) + : this(repository.GetDelegatorMetadata(address), repository) + { + } + + private LegacyTestDelegator(DelegatorMetadata metadata, IDelegationRepository repository) + { + Metadata = metadata; + Repository = repository; + } + + public DelegatorMetadata Metadata { get; } + + public IDelegationRepository Repository { get; } + + public Address Address => Metadata.DelegatorAddress; + + public Address AccountAddress => Metadata.DelegatorAccountAddress; + + public Address MetadataAddress => Metadata.Address; + + public Address DelegationPoolAddress => Metadata.DelegationPoolAddress; + + public Address RewardAddress => Metadata.RewardAddress; + + public ImmutableSortedSet
Delegatees => Metadata.Delegatees; + + public List MetadataBencoded => Metadata.Bencoded; + + public void Delegate( + LegacyTestDelegatee delegatee, FungibleAssetValue fav, long height) + { + if (fav.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(fav), fav, "Fungible asset value must be positive."); + } + + if (delegatee.Tombstoned) + { + throw new InvalidOperationException("Delegatee is tombstoned."); + } + + delegatee.Bond(this, fav, height); + Metadata.AddDelegatee(delegatee.Address); + Repository.TransferAsset(DelegationPoolAddress, delegatee.DelegationPoolAddress, fav); + Repository.SetDelegatorMetadata(Metadata); + } + + void IDelegator.Delegate( + IDelegatee delegatee, FungibleAssetValue fav, long height) + => Delegate((LegacyTestDelegatee)delegatee, fav, height); + + public void Undelegate( + LegacyTestDelegatee delegatee, BigInteger share, long height) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), share, "Share must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + UnbondLockIn unbondLockIn = Repository.GetUnbondLockIn(delegatee, Address); + + if (unbondLockIn.IsFull) + { + throw new InvalidOperationException("Undelegation is full."); + } + + FungibleAssetValue fav = delegatee.Unbond(this, share, height); + unbondLockIn = unbondLockIn.LockIn( + fav, height, height + delegatee.UnbondingPeriod); + + if (Repository.GetBond(delegatee, Address).Share.IsZero) + { + Metadata.RemoveDelegatee(delegatee.Address); + } + + delegatee.AddUnbondingRef(UnbondingFactory.ToReference(unbondLockIn)); + + Repository.SetUnbondLockIn(unbondLockIn); + Repository.SetUnbondingSet( + Repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); + Repository.SetDelegatorMetadata(Metadata); + } + + void IDelegator.Undelegate( + IDelegatee delegatee, BigInteger share, long height) + => Undelegate((LegacyTestDelegatee)delegatee, share, height); + + public void Redelegate( + LegacyTestDelegatee srcDelegatee, LegacyTestDelegatee dstDelegatee, BigInteger share, long height) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), share, "Share must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + if (dstDelegatee.Tombstoned) + { + throw new InvalidOperationException("Destination delegatee is tombstoned."); + } + + FungibleAssetValue fav = srcDelegatee.Unbond( + this, share, height); + dstDelegatee.Bond( + this, fav, height); + RebondGrace srcRebondGrace = Repository.GetRebondGrace(srcDelegatee, Address).Grace( + dstDelegatee.Address, + fav, + height, + height + srcDelegatee.UnbondingPeriod); + + if (Repository.GetBond(srcDelegatee, Address).Share.IsZero) + { + Metadata.RemoveDelegatee(srcDelegatee.Address); + } + + Metadata.AddDelegatee(dstDelegatee.Address); + + srcDelegatee.AddUnbondingRef(UnbondingFactory.ToReference(srcRebondGrace)); + + Repository.SetRebondGrace(srcRebondGrace); + Repository.SetUnbondingSet( + Repository.GetUnbondingSet().SetUnbonding(srcRebondGrace)); + Repository.SetDelegatorMetadata(Metadata); + } + + void IDelegator.Redelegate( + IDelegatee srcDelegatee, IDelegatee dstDelegatee, BigInteger share, long height) + => Redelegate((LegacyTestDelegatee)srcDelegatee, (LegacyTestDelegatee)dstDelegatee, share, height); + + public void CancelUndelegate( + LegacyTestDelegatee delegatee, FungibleAssetValue fav, long height) + { + if (fav.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(fav), fav, "Fungible asset value must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + UnbondLockIn unbondLockIn = Repository.GetUnbondLockIn(delegatee, Address); + + if (unbondLockIn.IsFull) + { + throw new InvalidOperationException("Undelegation is full."); + } + + delegatee.Bond(this, fav, height); + unbondLockIn = unbondLockIn.Cancel(fav, height); + Metadata.AddDelegatee(delegatee.Address); + + if (unbondLockIn.IsEmpty) + { + delegatee.RemoveUnbondingRef(UnbondingFactory.ToReference(unbondLockIn)); + } + + Repository.SetUnbondLockIn(unbondLockIn); + Repository.SetUnbondingSet( + Repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); + Repository.SetDelegatorMetadata(Metadata); + } + + void IDelegator.CancelUndelegate( + IDelegatee delegatee, FungibleAssetValue fav, long height) + => CancelUndelegate((LegacyTestDelegatee)delegatee, fav, height); + + public void ClaimReward( + LegacyTestDelegatee delegatee, long height) + { + delegatee.DistributeReward(this, height); + Repository.SetDelegatorMetadata(Metadata); + } + + void IDelegator.ClaimReward(IDelegatee delegatee, long height) + => ClaimReward((LegacyTestDelegatee)delegatee, height); + } +} diff --git a/.Lib9c.Tests/Delegation/Migration/MigrateLegacyStateTest.cs b/.Lib9c.Tests/Delegation/Migration/MigrateLegacyStateTest.cs deleted file mode 100644 index 4ed115b985..0000000000 --- a/.Lib9c.Tests/Delegation/Migration/MigrateLegacyStateTest.cs +++ /dev/null @@ -1,81 +0,0 @@ -namespace Lib9c.Tests.Delegation.Migration -{ - using System.Collections.Immutable; - using System.Linq; - using Libplanet.Crypto; - using Libplanet.Types.Assets; - using Nekoyume.Delegation; - using Xunit; - - public class MigrateLegacyStateTest - { - [Fact] - public void ParseLegacyDelegateeMetadata() - { - var address = new PrivateKey().Address; - var accountAddress = new PrivateKey().Address; - var delegationCurrency = Currency.Uncapped("del", 5, null); - var rewardCurrencies = new Currency[] { Currency.Uncapped("rew", 5, null), }; - var delegationPoolAddress = new PrivateKey().Address; - var rewardPoolAddress = new PrivateKey().Address; - var rewardRemainderPoolAddress = new PrivateKey().Address; - var slashedPoolAddress = new PrivateKey().Address; - var unbondingPeriod = 1L; - var maxUnbondLockInEntries = 2; - var maxRebondGraceEntries = 3; - - var legacyDelegateeMetadataBencoded = new LegacyDelegateeMetadata( - address, - accountAddress, - delegationCurrency, - rewardCurrencies, - delegationPoolAddress, - rewardPoolAddress, - rewardRemainderPoolAddress, - slashedPoolAddress, - unbondingPeriod, - maxUnbondLockInEntries, - maxRebondGraceEntries).Bencoded; - - var delegateeMetadata = new DelegateeMetadata(address, accountAddress, legacyDelegateeMetadataBencoded); - - Assert.Equal(address, delegateeMetadata.DelegateeAddress); - Assert.Equal(accountAddress, delegateeMetadata.DelegateeAccountAddress); - Assert.Equal(delegationCurrency, delegateeMetadata.DelegationCurrency); - Assert.Equal(rewardCurrencies, delegateeMetadata.RewardCurrencies); - Assert.Equal(delegationPoolAddress, delegateeMetadata.DelegationPoolAddress); - Assert.Equal(rewardPoolAddress, delegateeMetadata.RewardPoolAddress); - Assert.Equal(rewardRemainderPoolAddress, delegateeMetadata.RewardRemainderPoolAddress); - Assert.Equal(slashedPoolAddress, delegateeMetadata.SlashedPoolAddress); - Assert.Equal(unbondingPeriod, delegateeMetadata.UnbondingPeriod); - Assert.Equal(maxUnbondLockInEntries, delegateeMetadata.MaxUnbondLockInEntries); - Assert.Equal(maxRebondGraceEntries, delegateeMetadata.MaxRebondGraceEntries); - } - - [Fact] - public void ParseLegacyLumpSumRewardsRecord() - { - var address = new PrivateKey().Address; - var startHeight = 1L; - var totalShares = 2; - var delegators = ImmutableSortedSet.Create
(new PrivateKey().Address); - var currencies = new Currency[] { Currency.Uncapped("cur", 5, null), }; - var lastStartHeight = 3L; - - var legacyLumpSumRewardsRecordBencoded = new LegacyLumpSumRewardsRecord( - address, - startHeight, - totalShares, - delegators, - currencies, - lastStartHeight).Bencoded; - - var lumpSumRewardsRecord = new LumpSumRewardsRecord(address, legacyLumpSumRewardsRecordBencoded); - - Assert.Equal(address, lumpSumRewardsRecord.Address); - Assert.Equal(startHeight, lumpSumRewardsRecord.StartHeight); - Assert.Equal(totalShares, lumpSumRewardsRecord.TotalShares); - Assert.Equal(currencies, lumpSumRewardsRecord.LumpSumRewards.Select(c => c.Key)); - } - } -} diff --git a/.Lib9c.Tests/Delegation/Migration/RewardBaseMigrationTest.cs b/.Lib9c.Tests/Delegation/Migration/RewardBaseMigrationTest.cs new file mode 100644 index 0000000000..3dcca62e6c --- /dev/null +++ b/.Lib9c.Tests/Delegation/Migration/RewardBaseMigrationTest.cs @@ -0,0 +1,138 @@ +#nullable enable +namespace Lib9c.Tests.Delegation.Migration +{ + using System.Linq; + using Nekoyume.Delegation; + using Nekoyume.Extensions; + using Xunit; + + public class RewardBaseMigrationTest + { + private readonly DelegationFixture _fixture; + + public RewardBaseMigrationTest() + { + _fixture = new DelegationFixture(); + } + + public LegacyTestDelegatee LegacyDelegatee + => new LegacyTestDelegatee( + _fixture.TestDelegatee1.Address, + _fixture.TestRepository); + + public LegacyTestDelegator LegacyDelegator1 + => new LegacyTestDelegator( + _fixture.TestDelegator1.Address, + _fixture.TestRepository); + + public LegacyTestDelegator LegacyDelegator2 + => new LegacyTestDelegator( + _fixture.TestDelegator2.Address, + _fixture.TestRepository); + + [Fact] + public void Migrate() + { + var repo = _fixture.TestRepository; + + var delegatorInitialBalance = LegacyDelegatee.DelegationCurrency * 2000; + repo.MintAsset(LegacyDelegator1.Address, delegatorInitialBalance); + repo.MintAsset(LegacyDelegator2.Address, delegatorInitialBalance); + + var rewards = LegacyDelegatee.RewardCurrencies.Select(r => r * 100); + foreach (var reward in rewards) + { + repo.MintAsset(LegacyDelegatee.RewardPoolAddress, reward); + } + + LegacyDelegatee.CollectRewards(7L); + + var delegatingFAV = LegacyDelegatee.DelegationCurrency * 100; + LegacyDelegator1.Delegate(LegacyDelegatee, delegatingFAV, 10L); + + foreach (var reward in rewards) + { + repo.MintAsset(LegacyDelegatee.RewardPoolAddress, reward); + } + + LegacyDelegatee.CollectRewards(13L); + + var delegatingFAV1 = LegacyDelegatee.DelegationCurrency * 100; + LegacyDelegator2.Delegate(LegacyDelegatee, delegatingFAV1, 15L); + + foreach (var reward in rewards) + { + repo.MintAsset(LegacyDelegatee.RewardPoolAddress, reward); + } + + LegacyDelegatee.CollectRewards(17L); + var delegatingFAV2 = LegacyDelegatee.DelegationCurrency * 200; + LegacyDelegator2.Delegate(LegacyDelegatee, delegatingFAV2, 20L); + + foreach (var reward in rewards) + { + repo.MintAsset(LegacyDelegatee.RewardPoolAddress, reward); + } + + LegacyDelegatee.CollectRewards(23L); + var delegatingFAV3 = LegacyDelegatee.DelegationCurrency * 300; + LegacyDelegator2.Delegate(LegacyDelegatee, delegatingFAV3, 25L); + + foreach (var reward in rewards) + { + repo.MintAsset(LegacyDelegatee.RewardPoolAddress, reward); + } + + LegacyDelegatee.CollectRewards(27L); + var delegatingFAV4 = LegacyDelegatee.DelegationCurrency * 400; + LegacyDelegator2.Delegate(LegacyDelegatee, delegatingFAV4, 30L); + + foreach (var reward in rewards) + { + repo.MintAsset(LegacyDelegatee.RewardPoolAddress, reward); + } + + LegacyDelegatee.CollectRewards(23L); + + _fixture.TestRepository.UpdateWorld(_fixture.TestRepository.World.MutateAccount( + _fixture.TestRepository.DelegateeMetadataAccountAddress, + a => a.SetState(LegacyDelegatee.MetadataAddress, LegacyDelegatee.MetadataBencoded))); + _fixture.TestRepository.UpdateWorld(_fixture.TestRepository.World.MutateAccount( + _fixture.TestRepository.DelegatorMetadataAccountAddress, + a => a.SetState(LegacyDelegatee.Metadata.Address, LegacyDelegatee.Metadata.Bencoded))); + _fixture.TestRepository.UpdateWorld(_fixture.TestRepository.World.MutateAccount( + _fixture.TestRepository.DelegatorMetadataAccountAddress, + a => a.SetState(LegacyDelegatee.Metadata.Address, LegacyDelegatee.Metadata.Bencoded))); + + var delegator1 = _fixture.TestRepository.GetDelegator(_fixture.TestDelegator1.Address); + var delegator2 = _fixture.TestRepository.GetDelegator(_fixture.TestDelegator2.Address); + var delegatee = _fixture.TestRepository.GetDelegatee(_fixture.TestDelegatee1.Address); + + var delegatingFAV5 = delegatee.DelegationCurrency * 500; + delegator2.Delegate(delegatee, delegatingFAV5, 35L); + + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + delegatee.CollectRewards(37); + + var delegator1RewardBeforeDelegate = repo.GetBalance(delegator1.RewardAddress, DelegationFixture.TestRewardCurrency); + Assert.Equal(DelegationFixture.TestRewardCurrency * 0, delegator1RewardBeforeDelegate); + + delegator1.Delegate(delegatee, delegatingFAV5, 40L); + + var delegator1Reward = repo.GetBalance(delegator1.RewardAddress, DelegationFixture.TestRewardCurrency); + + var expectedReward = DelegationFixture.TestRewardCurrency * 100 + + (DelegationFixture.TestRewardCurrency * 100 * 100).DivRem(200).Quotient + + (DelegationFixture.TestRewardCurrency * 100 * 100).DivRem(400).Quotient + + (DelegationFixture.TestRewardCurrency * 100 * 100).DivRem(700).Quotient + + (DelegationFixture.TestRewardCurrency * 100 * 100).DivRem(1100).Quotient + + (DelegationFixture.TestRewardCurrency * 100 * 100).DivRem(1600).Quotient; + + Assert.Equal(expectedReward.MajorUnit, delegator1Reward.MajorUnit); + } + } +} diff --git a/.Lib9c.Tests/Delegation/TestRepository.cs b/.Lib9c.Tests/Delegation/TestRepository.cs index b072d9ce86..ccd60898c6 100644 --- a/.Lib9c.Tests/Delegation/TestRepository.cs +++ b/.Lib9c.Tests/Delegation/TestRepository.cs @@ -24,7 +24,8 @@ public TestRepository(IWorld world, IActionContext context) unbondLockInAccountAddress: new Address("0000000000000000000000000000000000000005"), rebondGraceAccountAddress: new Address("0000000000000000000000000000000000000006"), unbondingSetAccountAddress: new Address("0000000000000000000000000000000000000007"), - lumpSumRewardRecordAccountAddress: new Address("0000000000000000000000000000000000000008")) + rewardBaseAccountAddress: new Address("0000000000000000000000000000000000000008"), + lumpSumRewardRecordAccountAddress: new Address("0000000000000000000000000000000000000009")) { _context = context; } diff --git a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs index c68def6be6..b8863034bd 100644 --- a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs +++ b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs @@ -51,13 +51,13 @@ public override IWorld Execute(IActionContext context) switch (unbonding) { case UnbondLockIn unbondLockIn: - unbondLockIn.Release(context.BlockIndex, out var releasedFAV); + unbondLockIn = unbondLockIn.Release(context.BlockIndex, out var releasedFAV); repository.SetUnbondLockIn(unbondLockIn); repository.UpdateWorld( Unstake(repository.World, context, unbondLockIn, releasedFAV)); break; case RebondGrace rebondGrace: - rebondGrace.Release(context.BlockIndex, out _); + rebondGrace = rebondGrace.Release(context.BlockIndex, out _); repository.SetRebondGrace(rebondGrace); break; default: diff --git a/Lib9c/Addresses.cs b/Lib9c/Addresses.cs index 18097cdec2..5a0ac42632 100644 --- a/Lib9c/Addresses.cs +++ b/Lib9c/Addresses.cs @@ -146,6 +146,12 @@ public static readonly Address GuildLumpSumRewardsRecord public static readonly Address GuildUnbondingSet = new Address("0000000000000000000000000000000000000216"); + /// + /// An address of an account having . + /// + public static readonly Address GuildRewardBase + = new Address("0000000000000000000000000000000000000217"); + #endregion #region Validator @@ -233,6 +239,12 @@ public static readonly Address CommunityPool public static readonly Address NonValidatorDelegatee = new Address("0000000000000000000000000000000000000313"); + /// + /// An address of an account having . + /// + public static readonly Address ValidatorRewardBase + = new Address("0000000000000000000000000000000000000314"); + #endregion #region Migration diff --git a/Lib9c/Delegation/Delegatee.cs b/Lib9c/Delegation/Delegatee.cs index fbf1f40991..1aa75e5211 100644 --- a/Lib9c/Delegation/Delegatee.cs +++ b/Lib9c/Delegation/Delegatee.cs @@ -165,6 +165,15 @@ public Address UnbondLockInAddress(Address delegatorAddress) public Address RebondGraceAddress(Address delegatorAddress) => Metadata.RebondGraceAddress(delegatorAddress); + public Address DistributionPoolAddress() + => Metadata.DistributionPoolAddress(); + + public Address CurrentRewardBaseAddress() + => Metadata.CurrentRewardBaseAddress(); + + public Address RewardBaseAddress(long height) + => Metadata.RewardBaseAddress(height); + public Address CurrentLumpSumRewardsRecordAddress() => Metadata.CurrentLumpSumRewardsRecordAddress(); @@ -240,14 +249,23 @@ public void DistributeReward(T delegator, long height) if (!share.IsZero && bond.LastDistributeHeight.HasValue) { - IEnumerable lumpSumRewardsRecords - = GetLumpSumRewardsRecords(bond.LastDistributeHeight); - - foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) + if (Repository.GetCurrentRewardBase(this) is RewardBase rewardBase) { - TransferReward(delegator, share, record); + var lastRewardBase = Repository.GetRewardBase(this, bond.LastDistributeHeight.Value); + TransferReward(delegator, share, rewardBase, lastRewardBase); // TransferRemainders(newRecord); - Repository.SetLumpSumRewardsRecord(record); + } + else + { + IEnumerable lumpSumRewardsRecords + = GetLumpSumRewardsRecords(bond.LastDistributeHeight); + + foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) + { + TransferReward(delegator, share, record); + // TransferRemainders(newRecord); + Repository.SetLumpSumRewardsRecord(record); + } } } @@ -265,23 +283,40 @@ void IDelegatee.DistributeReward(IDelegator delegator, long height) public void CollectRewards(long height) { var rewards = RewardCurrencies.Select(c => Repository.GetBalance(RewardPoolAddress, c)); - LumpSumRewardsRecord record = Repository.GetCurrentLumpSumRewardsRecord(this) - ?? new LumpSumRewardsRecord( - CurrentLumpSumRewardsRecordAddress(), - height, - TotalShares, - RewardCurrencies); - record = record.AddLumpSumRewards(rewards); - - foreach (var rewardsEach in rewards) + if (Repository.GetCurrentRewardBase(this) is RewardBase rewardBase) { - if (rewardsEach.Sign > 0) + rewardBase = rewardBase.AddRewards(rewards); + + foreach (var rewardsEach in rewards) { - Repository.TransferAsset(RewardPoolAddress, record.Address, rewardsEach); + if (rewardsEach.Sign > 0) + { + Repository.TransferAsset(RewardPoolAddress, DistributionPoolAddress(), rewardsEach); + } } + + Repository.SetRewardBase(rewardBase); } + else + { + LumpSumRewardsRecord record = Repository.GetCurrentLumpSumRewardsRecord(this) + ?? new LumpSumRewardsRecord( + CurrentLumpSumRewardsRecordAddress(), + height, + TotalShares, + RewardCurrencies); + record = record.AddLumpSumRewards(rewards); + + foreach (var rewardsEach in rewards) + { + if (rewardsEach.Sign > 0) + { + Repository.TransferAsset(RewardPoolAddress, record.Address, rewardsEach); + } + } - Repository.SetLumpSumRewardsRecord(record); + Repository.SetLumpSumRewardsRecord(record); + } } public virtual void Slash(BigInteger slashFactor, long infractionHeight, long height) @@ -369,49 +404,38 @@ ImmutableDictionary reward return reward; } - private void StartNewRewardPeriod(long height) + public void StartNewRewardPeriod(long height) { - LumpSumRewardsRecord? currentRecord = Repository.GetCurrentLumpSumRewardsRecord(this); - long? lastStartHeight = null; - if (currentRecord is LumpSumRewardsRecord lastRecord) + MigrateLumpSumRewardsRecords(); + + RewardBase newRewardBase; + if (Repository.GetCurrentRewardBase(this) is RewardBase rewardBase) { - lastStartHeight = lastRecord.StartHeight; - if (lastStartHeight == height) + newRewardBase = rewardBase.UpdateTotalShares(TotalShares); + if (Repository.GetRewardBase(this, height) is not null) { - currentRecord = new( - currentRecord.Address, - currentRecord.StartHeight, - TotalShares, - RewardCurrencies, - currentRecord.LastStartHeight); - - Repository.SetLumpSumRewardsRecord(currentRecord); + Repository.SetRewardBase(newRewardBase); return; } - Address archiveAddress = LumpSumRewardsRecordAddress(lastRecord.StartHeight); - - foreach (var rewardCurrency in RewardCurrencies) + Address archiveAddress = RewardBaseAddress(height); + var archivedRewardBase = rewardBase.AttachHeight(archiveAddress, height); + Repository.SetRewardBase(archivedRewardBase); + } + else + { + if (TotalShares.IsZero) { - FungibleAssetValue reward = Repository.GetBalance(lastRecord.Address, rewardCurrency); - if (reward.Sign > 0) - { - Repository.TransferAsset(lastRecord.Address, archiveAddress, reward); - } + return; } - - lastRecord = lastRecord.MoveAddress(archiveAddress); - Repository.SetLumpSumRewardsRecord(lastRecord); - } - LumpSumRewardsRecord newRecord = new( - CurrentLumpSumRewardsRecordAddress(), - height, - TotalShares, - RewardCurrencies, - lastStartHeight); + newRewardBase = new( + CurrentRewardBaseAddress(), + TotalShares, + RewardCurrencies); + } - Repository.SetLumpSumRewardsRecord(newRecord); + Repository.SetRewardBase(newRewardBase); } private List GetLumpSumRewardsRecords(long? lastRewardHeight) @@ -452,6 +476,34 @@ private void TransferReward(T delegator, BigInteger share, LumpSumRewardsRecord } } + private void TransferReward( + T delegator, + BigInteger share, + RewardBase currentRewardBase, + RewardBase? lastRewardBase) + { + var currentCumulative = currentRewardBase.CumulativeRewardDuringPeriod(share); + var lastCumulative = lastRewardBase?.CumulativeRewardDuringPeriod(share) + ?? ImmutableSortedDictionary.Empty; + + foreach (var c in currentCumulative) + { + var lastCumulativeEach = lastCumulative.GetValueOrDefault(c.Key, defaultValue: c.Key * 0); + + if (c.Value < lastCumulativeEach) + { + throw new InvalidOperationException("Invalid reward base."); + } + + var reward = c.Value - lastCumulativeEach; + + if (reward.Sign > 0) + { + Repository.TransferAsset(DistributionPoolAddress(), delegator.RewardAddress, reward); + } + } + } + private void TransferRemainders(LumpSumRewardsRecord record) { foreach (var rewardCurrency in RewardCurrencies) @@ -464,5 +516,78 @@ private void TransferRemainders(LumpSumRewardsRecord record) } } } + + private void MigrateLumpSumRewardsRecords() + { + var growSize = 100; + var capacity = 5000; + List records = new(capacity); + if (!(Repository.GetCurrentLumpSumRewardsRecord(this) is LumpSumRewardsRecord record)) + { + return; + } + + while (record.LastStartHeight is long lastStartHeight) + { + if (records.Count == capacity) + { + capacity += growSize; + records.Capacity = capacity; + } + + records.Add(record); + record = Repository.GetLumpSumRewardsRecord(this, lastStartHeight) + ?? throw new InvalidOperationException( + $"Lump sum rewards record for #{lastStartHeight} is missing"); + } + + RewardBase? rewardBase = null; + RewardBase? newRewardBase = null; + for (var i = records.Count - 1; i >= 0; i--) + { + var recordEach = records[i]; + + if (rewardBase is null) + { + rewardBase = new RewardBase( + CurrentRewardBaseAddress(), + recordEach.TotalShares, + recordEach.LumpSumRewards.Keys); + } + else + { + newRewardBase = rewardBase.UpdateTotalShares(recordEach.TotalShares); + if (Repository.GetRewardBase(this, recordEach.StartHeight) is not null) + { + Repository.SetRewardBase(newRewardBase); + } + else + { + Address archiveAddress = RewardBaseAddress(recordEach.StartHeight); + var archivedRewardBase = rewardBase.AttachHeight(archiveAddress, recordEach.StartHeight); + Repository.SetRewardBase(archivedRewardBase); + } + + rewardBase = newRewardBase; + } + + rewardBase = rewardBase.AddRewards(recordEach.LumpSumRewards.Values); + foreach (var r in recordEach.LumpSumRewards) + { + var toTransfer = Repository.GetBalance(recordEach.Address, r.Key); + if (toTransfer.Sign > 0) + { + Repository.TransferAsset(recordEach.Address, DistributionPoolAddress(), toTransfer); + } + } + + Repository.RemoveLumpSumRewardsRecord(recordEach); + } + + if (rewardBase is RewardBase rewardBaseToSet) + { + Repository.SetRewardBase(rewardBaseToSet); + } + } } } diff --git a/Lib9c/Delegation/DelegateeMetadata.cs b/Lib9c/Delegation/DelegateeMetadata.cs index 3f9121a9cd..a322ea03d8 100644 --- a/Lib9c/Delegation/DelegateeMetadata.cs +++ b/Lib9c/Delegation/DelegateeMetadata.cs @@ -62,8 +62,8 @@ public DelegateeMetadata( } public DelegateeMetadata( - Address address, - Address accountAddress, + Address delegateeAddress, + Address delegateeAccountAddress, List bencoded) { Currency delegationCurrency; @@ -152,8 +152,8 @@ public DelegateeMetadata( "Total shares must be non-negative."); } - DelegateeAddress = address; - DelegateeAccountAddress = accountAddress; + DelegateeAddress = delegateeAddress; + DelegateeAccountAddress = delegateeAccountAddress; DelegationCurrency = delegationCurrency; RewardCurrencies = rewardCurrencies.ToImmutableSortedSet(_currencyComparer); DelegationPoolAddress = delegationPoolAddress; @@ -340,11 +340,20 @@ public Address UnbondLockInAddress(Address delegatorAddress) public virtual Address RebondGraceAddress(Address delegatorAddress) => DelegationAddress.RebondGraceAddress(Address, delegatorAddress); + public virtual Address DistributionPoolAddress() + => DelegationAddress.DistributionPoolAddress(Address); + + public virtual Address CurrentRewardBaseAddress() + => DelegationAddress.CurrentRewardBaseAddress(Address); + + public virtual Address RewardBaseAddress(long height) + => DelegationAddress.RewardBaseAddress(Address, height); + public virtual Address CurrentLumpSumRewardsRecordAddress() - => DelegationAddress.CurrentLumpSumRewardsRecordAddress(Address); + => DelegationAddress.CurrentRewardBaseAddress(Address); public virtual Address LumpSumRewardsRecordAddress(long height) - => DelegationAddress.LumpSumRewardsRecordAddress(Address, height); + => DelegationAddress.RewardBaseAddress(Address, height); public override bool Equals(object? obj) => obj is IDelegateeMetadata other && Equals(other); diff --git a/Lib9c/Delegation/DelegationAddress.cs b/Lib9c/Delegation/DelegationAddress.cs index 243b21e382..1e7a340b92 100644 --- a/Lib9c/Delegation/DelegationAddress.cs +++ b/Lib9c/Delegation/DelegationAddress.cs @@ -65,29 +65,29 @@ public static Address RebondGraceAddress( delegateeMetadataAddress, delegatorAddress.ByteArray); - public static Address CurrentLumpSumRewardsRecordAddress( + public static Address CurrentRewardBaseAddress( Address delegateeAddress, Address delegateeAccountAddress) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); - public static Address CurrentLumpSumRewardsRecordAddress( + public static Address CurrentRewardBaseAddress( Address delegateeMetadataAddress) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, delegateeMetadataAddress); - public static Address LumpSumRewardsRecordAddress( + public static Address RewardBaseAddress( Address delegateeAddress, Address delegateeAccountAddress, long height) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress), BitConverter.GetBytes(height)); - public static Address LumpSumRewardsRecordAddress( + public static Address RewardBaseAddress( Address delegateeMetadataAddress, long height) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, delegateeMetadataAddress, BitConverter.GetBytes(height)); @@ -103,6 +103,18 @@ public static Address RewardPoolAddress( DelegationElementType.RewardPool, delegateeMetadataAddress); + public static Address DistributionPoolAddress( + Address delegateeAddress, Address delegateeAccountAddress) + => DeriveAddress( + DelegationElementType.DistributionPool, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); + + public static Address DistributionPoolAddress( + Address delegateeMetadataAddress) + => DeriveAddress( + DelegationElementType.DistributionPool, + delegateeMetadataAddress); + public static Address DelegationPoolAddress( Address delegateeAddress, Address delegateeAccountAddress) => DeriveAddress( @@ -138,9 +150,10 @@ private enum DelegationElementType Bond, UnbondLockIn, RebondGrace, - LumpSumRewardsRecord, + RewardBase, RewardPool, DelegationPool, + DistributionPool, } } } diff --git a/Lib9c/Delegation/DelegationRepository.cs b/Lib9c/Delegation/DelegationRepository.cs index c4002c2372..d67bc2f614 100644 --- a/Lib9c/Delegation/DelegationRepository.cs +++ b/Lib9c/Delegation/DelegationRepository.cs @@ -19,6 +19,8 @@ public abstract class DelegationRepository : IDelegationRepository protected IAccount unbondLockInAccount; protected IAccount rebondGraceAccount; protected IAccount unbondingSetAccount; + protected IAccount rewardBaseAccount; + // TODO: [Migration] Remove this field after migration. protected IAccount lumpSumRewardsRecordAccount; public DelegationRepository( @@ -32,6 +34,7 @@ public DelegationRepository( Address unbondLockInAccountAddress, Address rebondGraceAccountAddress, Address unbondingSetAccountAddress, + Address rewardBaseAccountAddress, Address lumpSumRewardRecordAccountAddress) { previousWorld = world; @@ -44,6 +47,7 @@ public DelegationRepository( UnbondLockInAccountAddress = unbondLockInAccountAddress; RebondGraceAccountAddress = rebondGraceAccountAddress; UnbondingSetAccountAddress = unbondingSetAccountAddress; + RewardBaseAccountAddress = rewardBaseAccountAddress; LumpSumRewardsRecordAccountAddress = lumpSumRewardRecordAccountAddress; delegateeAccount = world.GetAccount(DelegateeAccountAddress); @@ -54,6 +58,7 @@ public DelegationRepository( unbondLockInAccount = world.GetAccount(UnbondLockInAccountAddress); rebondGraceAccount = world.GetAccount(RebondGraceAccountAddress); unbondingSetAccount = world.GetAccount(UnbondingSetAccountAddress); + rewardBaseAccount = world.GetAccount(RewardBaseAccountAddress); lumpSumRewardsRecordAccount = world.GetAccount(LumpSumRewardsRecordAccountAddress); } @@ -66,6 +71,7 @@ public DelegationRepository( .SetAccount(UnbondLockInAccountAddress, unbondLockInAccount) .SetAccount(RebondGraceAccountAddress, rebondGraceAccount) .SetAccount(UnbondingSetAccountAddress, unbondingSetAccount) + .SetAccount(RewardBaseAccountAddress, rewardBaseAccount) .SetAccount(LumpSumRewardsRecordAccountAddress, lumpSumRewardsRecordAccount); public IActionContext ActionContext { get; } @@ -74,19 +80,21 @@ public DelegationRepository( public Address DelegatorAccountAddress { get; } - private Address DelegateeMetadataAccountAddress { get; } + public Address DelegateeMetadataAccountAddress { get; } - private Address DelegatorMetadataAccountAddress { get; } + public Address DelegatorMetadataAccountAddress { get; } - private Address BondAccountAddress { get; } + public Address BondAccountAddress { get; } - private Address UnbondLockInAccountAddress { get; } + public Address UnbondLockInAccountAddress { get; } - private Address RebondGraceAccountAddress { get; } + public Address RebondGraceAccountAddress { get; } - private Address UnbondingSetAccountAddress { get; } + public Address UnbondingSetAccountAddress { get; } - private Address LumpSumRewardsRecordAccountAddress { get; } + public Address RewardBaseAccountAddress { get; } + + public Address LumpSumRewardsRecordAccountAddress { get; } public abstract IDelegatee GetDelegatee(Address address); @@ -162,6 +170,24 @@ public UnbondingSet GetUnbondingSet() ? new UnbondingSet(bencoded, this) : new UnbondingSet(this); + public RewardBase? GetCurrentRewardBase(IDelegatee delegatee) + { + Address address = delegatee.CurrentRewardBaseAddress(); + IValue? value = rewardBaseAccount.GetState(address); + return value is IValue bencoded + ? new RewardBase(address, bencoded) + : null; + } + + public RewardBase? GetRewardBase(IDelegatee delegatee, long height) + { + Address address = delegatee.RewardBaseAddress(height); + IValue? value = rewardBaseAccount.GetState(address); + return value is IValue bencoded + ? new RewardBase(address, bencoded) + : null; + } + public LumpSumRewardsRecord? GetLumpSumRewardsRecord(IDelegatee delegatee, long height) { Address address = delegatee.LumpSumRewardsRecordAddress(height); @@ -225,12 +251,22 @@ public void SetUnbondingSet(UnbondingSet unbondingSet) : unbondingSetAccount.SetState(UnbondingSet.Address, unbondingSet.Bencoded); } + public void SetRewardBase(RewardBase rewardBase) + { + rewardBaseAccount = rewardBaseAccount.SetState(rewardBase.Address, rewardBase.Bencoded); + } + public void SetLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord) { lumpSumRewardsRecordAccount = lumpSumRewardsRecordAccount.SetState( lumpSumRewardsRecord.Address, lumpSumRewardsRecord.Bencoded); } + public void RemoveLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord) + { + lumpSumRewardsRecordAccount = lumpSumRewardsRecordAccount.RemoveState(lumpSumRewardsRecord.Address); + } + public void TransferAsset(Address sender, Address recipient, FungibleAssetValue value) => previousWorld = previousWorld.TransferAsset(ActionContext, sender, recipient, value); @@ -245,6 +281,7 @@ public virtual void UpdateWorld(IWorld world) unbondLockInAccount = world.GetAccount(UnbondLockInAccountAddress); rebondGraceAccount = world.GetAccount(RebondGraceAccountAddress); unbondingSetAccount = world.GetAccount(UnbondingSetAccountAddress); + rewardBaseAccount = world.GetAccount(RewardBaseAccountAddress); lumpSumRewardsRecordAccount = world.GetAccount(LumpSumRewardsRecordAccountAddress); } } diff --git a/Lib9c/Delegation/Delegator.cs b/Lib9c/Delegation/Delegator.cs index 044d6a7f91..43159d17ea 100644 --- a/Lib9c/Delegation/Delegator.cs +++ b/Lib9c/Delegation/Delegator.cs @@ -221,6 +221,7 @@ public void ClaimReward( T delegatee, long height) { delegatee.DistributeReward(this, height); + delegatee.StartNewRewardPeriod(height); Repository.SetDelegator(this); } diff --git a/Lib9c/Delegation/IDelegatee.cs b/Lib9c/Delegation/IDelegatee.cs index 9cb3503613..54144830b1 100644 --- a/Lib9c/Delegation/IDelegatee.cs +++ b/Lib9c/Delegation/IDelegatee.cs @@ -65,6 +65,10 @@ public interface IDelegatee Address RebondGraceAddress(Address delegatorAddress); + Address CurrentRewardBaseAddress(); + + Address RewardBaseAddress(long height); + Address CurrentLumpSumRewardsRecordAddress(); Address LumpSumRewardsRecordAddress(long height); diff --git a/Lib9c/Delegation/IDelegationRepository.cs b/Lib9c/Delegation/IDelegationRepository.cs index 47c988a0b5..c3520d4c41 100644 --- a/Lib9c/Delegation/IDelegationRepository.cs +++ b/Lib9c/Delegation/IDelegationRepository.cs @@ -36,10 +36,14 @@ public interface IDelegationRepository UnbondingSet GetUnbondingSet(); - LumpSumRewardsRecord? GetLumpSumRewardsRecord(IDelegatee delegatee, long height); + RewardBase? GetCurrentRewardBase(IDelegatee delegatee); + + RewardBase? GetRewardBase(IDelegatee delegatee, long height); LumpSumRewardsRecord? GetCurrentLumpSumRewardsRecord(IDelegatee delegatee); + LumpSumRewardsRecord? GetLumpSumRewardsRecord(IDelegatee delegatee, long height); + FungibleAssetValue GetBalance(Address address, Currency currency); void SetDelegatee(IDelegatee delegatee); @@ -58,8 +62,12 @@ public interface IDelegationRepository void SetUnbondingSet(UnbondingSet unbondingSet); + void SetRewardBase(RewardBase rewardBase); + void SetLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord); + void RemoveLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord); + void TransferAsset(Address sender, Address recipient, FungibleAssetValue value); void UpdateWorld(IWorld world); diff --git a/Lib9c/Delegation/RewardBase.cs b/Lib9c/Delegation/RewardBase.cs new file mode 100644 index 0000000000..18102d0c26 --- /dev/null +++ b/Lib9c/Delegation/RewardBase.cs @@ -0,0 +1,404 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action; + +namespace Nekoyume.Delegation +{ + /// + /// RewardBase is a class that represents the base of the reward. + /// If it's multiplied by the number of shares, it will be the reward for the period. + /// Also, it holds the significant figure to calculate the reward. + /// + public class RewardBase : IBencodable, IEquatable + { + private const string StateTypeName = "reward_base"; + private const long StateVersion = 1; + + /// + /// Margin for significant figure. It's used to calculate the significant figure of the reward base. + /// + public const int Margin = 2; + private static readonly IComparer _currencyComparer = new CurrencyComparer(); + + /// + /// Constructor for new . + /// This constructor is used only for the initial reward base creation. + /// + /// + /// of . + /// + /// + /// of 's creation height. + /// + /// + /// of 's creation height. + /// It initializes the reward portion with 0. + /// + public RewardBase( + Address address, + BigInteger totalShares, + IEnumerable currencies) + : this( + address, + totalShares, + currencies.Select(c => (c, BigInteger.Zero)), + RecommendedSigFig(totalShares), + null) + { + } + + /// + /// Constructor for new from bencoded data. + /// + /// + /// of . + /// + /// + /// Bencoded data of . + /// + public RewardBase(Address address, IValue bencoded) + : this(address, (List)bencoded) + { + } + + /// + /// Constructor for new from bencoded data. + /// + /// + /// of . + /// + /// + /// Bencoded data of . + /// + /// Thrown when the bencoded data is not valid format for . + /// + /// + /// Thrown when the of the bencoded data is higher than the current version. + /// + /// + /// Thrown when the bencoded data has duplicated currency. + /// + public RewardBase(Address address, List bencoded) + { + if (bencoded[0] is not Text text || text != StateTypeName || bencoded[1] is not Integer integer) + { + throw new InvalidCastException(); + } + + if (integer > StateVersion) + { + throw new FailedLoadStateException("Un-deserializable state."); + } + + Address = address; + TotalShares = (Integer)bencoded[2]; + var bencodedRewardPortion = ((List)bencoded[3]).Select(v => (List)v); + var rewardPortion = bencodedRewardPortion.Select( + p => (Currency: new Currency(p[0]), Portion: (BigInteger)(Integer)p[1])); + + if (!rewardPortion.Select(f => f.Currency).All(new HashSet().Add)) + { + throw new ArgumentException("Duplicated currency in reward base."); + } + + RewardPortion = rewardPortion.ToImmutableSortedDictionary(f => f.Currency, f => f.Portion, _currencyComparer); + SigFig = (Integer)bencoded[4]; + + try + { + StartHeight = (Integer)bencoded[5]; + } + catch (IndexOutOfRangeException) + { + StartHeight = null; + } + } + + /// + /// Constructor for new . + /// + /// + /// of . + /// + /// + /// of 's creation height. + /// + /// + /// Cumulative reward portion of 's creation height. + /// + /// + /// Significant figure of . + /// + /// + /// Start height of that attached when archived. + /// + /// + /// Thrown when the is less than or equal to 0. + /// + /// + /// Thrown when the has duplicated currency. + /// + private RewardBase( + Address address, + BigInteger totalShares, + IEnumerable<(Currency, BigInteger)> rewardPortion, + int sigFig, + long? startHeight = null) + { + Address = address; + + if (totalShares.Sign <= 0) + { + throw new ArgumentOutOfRangeException(nameof(totalShares)); + } + + TotalShares = totalShares; + + if (!rewardPortion.Select(f => f.Item1).All(new HashSet().Add)) + { + throw new ArgumentException("Duplicated currency in reward base."); + } + + RewardPortion = rewardPortion.ToImmutableSortedDictionary(f => f.Item1, f => f.Item2, _currencyComparer); + SigFig = sigFig; + StartHeight = startHeight; + } + + /// + /// Constructor for new . + /// + /// + /// of . + /// + /// + /// of 's creation height. + /// + /// + /// Cumulative reward portion of 's creation height. + /// + /// + /// Significant figure of . + /// + /// + /// Start height of that attached when archived. + /// + private RewardBase( + Address address, + BigInteger totalShares, + ImmutableSortedDictionary rewardPortion, + int sigFig, + long? startHeight = null) + { + Address = address; + TotalShares = totalShares; + RewardPortion = rewardPortion; + SigFig = sigFig; + StartHeight = startHeight; + } + + /// + /// of . + /// + public Address Address { get; } + + /// + /// of 's creation height. + /// + public BigInteger TotalShares { get; } + + /// + /// Cumulative reward portion of . + /// When it's multiplied by the number of shares, it will be the reward for the period. + /// + public ImmutableSortedDictionary RewardPortion { get; } + + /// + /// Significant figure of . + /// + public int SigFig { get; private set; } + + /// + /// Start height of that attached when archived. + /// + public long? StartHeight { get; } + + public List Bencoded + { + get + { + var bencoded = List.Empty + .Add(StateTypeName) + .Add(StateVersion) + .Add(TotalShares) + .Add(new List(RewardPortion + .OrderBy(r => r.Key, _currencyComparer) + .Select(r => new List(r.Key.Serialize(), new Integer(r.Value))))) + .Add(SigFig); + + return StartHeight is long height + ? bencoded.Add(height) + : bencoded; + } + } + + IValue IBencodable.Bencoded => Bencoded; + + /// + /// Add rewards to the . + /// + /// + /// Rewards to add. + /// + /// + /// New with added rewards. + /// + public RewardBase AddRewards(IEnumerable rewards) + => rewards.Aggregate(this, (accum, next) => AddReward(accum, next)); + + /// + /// Add reward to the . + /// + /// + /// Reward to add. + /// + /// + /// New with added reward. + /// + public RewardBase AddReward(FungibleAssetValue reward) + => AddReward(this, reward); + + /// + /// Update the total shares of the . + /// + /// + /// New of the height that created. + /// + /// + /// New with updated total shares. + /// + public RewardBase UpdateTotalShares(BigInteger totalShares) + => UpdateTotalShares(this, totalShares); + + /// + /// Attach the start height to the to be archived. + /// + /// + /// of . + /// + /// + /// Start height of that attached when archived. + /// + /// + /// New with attached start height. + /// + /// + /// Thrown when the start height is already attached. + /// + public RewardBase AttachHeight(Address address, long startHeight) + => StartHeight is null + ? new RewardBase( + address, + TotalShares, + RewardPortion, + SigFig, + startHeight) + : throw new InvalidOperationException("StartHeight is already attached."); + + /// + /// Calculate the cumulative reward during the period. + /// + /// + /// The number of shares to calculate the reward. + /// + /// + /// Cumulative reward during the period. + /// + public ImmutableSortedDictionary CumulativeRewardDuringPeriod(BigInteger share) + => RewardPortion.Keys.Select(k => CumulativeRewardDuringPeriod(share, k)) + .ToImmutableSortedDictionary(f => f.Currency, f => f, _currencyComparer); + + /// + /// Calculate the cumulative reward during the period, for the specific currency. + /// + /// + /// The number of shares to calculate the reward. + /// + /// + /// The currency to calculate the reward. + /// + /// + /// Cumulative reward during the period, for the specific currency. + /// + /// + /// Thrown when the is not in the . + /// + public FungibleAssetValue CumulativeRewardDuringPeriod(BigInteger share, Currency currency) + => RewardPortion.TryGetValue(currency, out var portion) + ? FungibleAssetValue.FromRawValue(currency, (portion * share) / (Multiplier(SigFig))) + : throw new ArgumentException($"Invalid reward currency: {currency}"); + + private static RewardBase AddReward(RewardBase rewardBase, FungibleAssetValue reward) + { + if (!rewardBase.RewardPortion.TryGetValue(reward.Currency, out var portion)) + { + throw new ArgumentException( + $"Invalid reward currency: {reward.Currency}", nameof(reward)); + } + + var portionNumerator = reward.RawValue * Multiplier(rewardBase.SigFig); + var updatedPortion = portion + (portionNumerator / rewardBase.TotalShares); + + return new RewardBase( + rewardBase.Address, + rewardBase.TotalShares, + rewardBase.RewardPortion.SetItem(reward.Currency, updatedPortion), + rewardBase.SigFig, + rewardBase.StartHeight); + } + + private static RewardBase UpdateTotalShares(RewardBase rewardBase, BigInteger totalShares) + { + var newSigFig = Math.Max(rewardBase.SigFig, RecommendedSigFig(totalShares)); + var multiplier = Multiplier(newSigFig - rewardBase.SigFig); + var newPortion = rewardBase.RewardPortion.ToImmutableSortedDictionary( + kvp => kvp.Key, + kvp => kvp.Value * multiplier, + _currencyComparer); + + return new RewardBase( + rewardBase.Address, + totalShares, + newPortion, + newSigFig); + } + + private static int RecommendedSigFig(BigInteger totalShares) + => (int)Math.Floor(BigInteger.Log10(totalShares)) + Margin; + + private static BigInteger Multiplier(int sigFig) + => BigInteger.Pow(10, sigFig); + + public override bool Equals(object? obj) + => obj is RewardBase other && Equals(other); + + public bool Equals(RewardBase? other) + => ReferenceEquals(this, other) + || (other is RewardBase rewardBase + && Address == rewardBase.Address + && TotalShares == rewardBase.TotalShares + && RewardPortion.SequenceEqual(rewardBase.RewardPortion) + && SigFig == rewardBase.SigFig + && StartHeight == rewardBase.StartHeight); + + public override int GetHashCode() + => Address.GetHashCode(); + } +} diff --git a/Lib9c/Model/Guild/GuildRepository.cs b/Lib9c/Model/Guild/GuildRepository.cs index f1394e99e5..54bd7d223b 100644 --- a/Lib9c/Model/Guild/GuildRepository.cs +++ b/Lib9c/Model/Guild/GuildRepository.cs @@ -34,6 +34,7 @@ public GuildRepository(IWorld world, IActionContext actionContext) unbondLockInAccountAddress: Addresses.GuildUnbondLockIn, rebondGraceAccountAddress: Addresses.GuildRebondGrace, unbondingSetAccountAddress: Addresses.GuildUnbondingSet, + rewardBaseAccountAddress: Addresses.GuildRewardBase, lumpSumRewardRecordAccountAddress: Addresses.GuildLumpSumRewardsRecord) { _guildAccount = world.GetAccount(guildAddress); diff --git a/Lib9c/ValidatorDelegation/AbstainHistory.cs b/Lib9c/ValidatorDelegation/AbstainHistory.cs index 430d2c78cd..deaedb1f12 100644 --- a/Lib9c/ValidatorDelegation/AbstainHistory.cs +++ b/Lib9c/ValidatorDelegation/AbstainHistory.cs @@ -42,7 +42,7 @@ public AbstainHistory(List bencoded) public static int WindowSize => 10; - public static int MaxAbstainAllowance => 3; + public static int MaxAbstainAllowance => 9; public static Address Address => new Address( ImmutableArray.Create( diff --git a/Lib9c/ValidatorDelegation/ValidatorRepository.cs b/Lib9c/ValidatorDelegation/ValidatorRepository.cs index 69e7563c38..7e4a6d5760 100644 --- a/Lib9c/ValidatorDelegation/ValidatorRepository.cs +++ b/Lib9c/ValidatorDelegation/ValidatorRepository.cs @@ -32,6 +32,7 @@ public ValidatorRepository(IWorld world, IActionContext actionContext) Addresses.ValidatorUnbondLockIn, Addresses.ValidatorRebondGrace, Addresses.ValidatorUnbondingSet, + Addresses.ValidatorRewardBase, Addresses.ValidatorLumpSumRewardsRecord) { _validatorListAccount = world.GetAccount(validatorListAddress);