diff --git a/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs b/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs index 729bef6536..d4509a2998 100644 --- a/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs +++ b/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs @@ -706,6 +706,35 @@ public void Execute_V6() } } + [Fact] + public void Execute_Throw_When_Validator_Tries_To_Claim() + { + // When + var world = _initialState; + var validatorKey = new PrivateKey().PublicKey; + var validatorAddress = validatorKey.Address; + var height = 0L; + world = DelegationUtil.EnsureValidatorPromotionReady(world, validatorKey, height); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); + var stakeStateV2 = PrepareStakeStateV2( + _stakePolicySheet, + 0, + LegacyStakeState.RewardInterval); + var action = new ClaimStakeReward(validatorAddress); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorAddress, + BlockIndex = height, + }; + + // When + var e = Assert.Throws(() => action.Execute(actionContext)); + + // Then + Assert.Equal("The validator cannot claim the stake reward.", e.Message); + } + private static StakeState PrepareStakeStateV2( StakePolicySheet stakePolicySheet, long startedBlockIndex, diff --git a/.Lib9c.Tests/Action/StakeTest.cs b/.Lib9c.Tests/Action/StakeTest.cs index fd4aa3c6e9..c2f2d499f3 100644 --- a/.Lib9c.Tests/Action/StakeTest.cs +++ b/.Lib9c.Tests/Action/StakeTest.cs @@ -28,6 +28,7 @@ public class StakeTest { private readonly IWorld _initialState; private readonly Currency _ncg; + private readonly PublicKey _agentPublicKey = new PrivateKey().PublicKey; private readonly Address _agentAddr; private readonly StakePolicySheet _stakePolicySheet; @@ -66,7 +67,9 @@ public StakeTest(ITestOutputHelper outputHelper) _agentAddr, _, _initialState - ) = InitializeUtil.InitializeStates(sheetsOverride: sheetsOverride); + ) = InitializeUtil.InitializeStates( + sheetsOverride: sheetsOverride, + agentAddr: _agentPublicKey.Address); _ncg = _initialState.GetGoldCurrency(); _stakePolicySheet = _initialState.GetSheet(); } @@ -471,6 +474,80 @@ public void Execute_Success_When_Exist_StakeStateV3_Without_Guild( Assert.Equal(Currencies.GuildGold * amount, stakeBalance); } + [Theory] + [InlineData(0, 500, false)] + [InlineData(50, 100, false)] + [InlineData(0, long.MaxValue, false)] + [InlineData(0, 500, true)] + [InlineData(50, 100, true)] + [InlineData(0, long.MaxValue, true)] + public void Execute_Success_When_Validator_Tries_To_Increase_Amount_Without_Claim( + long previousAmount, + long amount, + bool withoutInterval) + { + if (previousAmount >= amount) + { + throw new ArgumentException( + "previousAmount should be less than amount.", nameof(previousAmount)); + } + + var interval = LegacyStakeState.RewardInterval; + var stakeStateAddr = StakeState.DeriveAddress(_agentAddr); + var stakeState = new StakeState( + contract: new Contract(_stakePolicySheet), + startedBlockIndex: 0L, + receivedBlockIndex: interval, + stateVersion: 3); + var world = _initialState; + var height = 0L; + + world = DelegationUtil.EnsureValidatorPromotionReady(world, _agentPublicKey, height++); + + if (previousAmount > 0) + { + var ncgToStake = _ncg * previousAmount; + var gg = FungibleAssetValue.Parse(Currencies.GuildGold, ncgToStake.GetQuantityString(true)); + world = DelegationUtil.MintGuildGold(world, _agentAddr, gg, height); + world = world.MintAsset(new ActionContext(), _agentAddr, ncgToStake); + world = world.TransferAsset( + new ActionContext(), _agentAddr, stakeStateAddr, ncgToStake); + } + + world = world.SetLegacyState(stakeStateAddr, stakeState.Serialize()); + + if (amount - previousAmount > 0) + { + var ncgToStake = _ncg * (amount - previousAmount); + world = world.MintAsset(new ActionContext(), _agentAddr, ncgToStake); + } + + var nextState = Execute( + height + (withoutInterval ? 1 : interval), + world, + new TestRandom(), + _agentAddr, + amount); + + if (amount > 0) + { + Assert.True(nextState.TryGetStakeState(_agentAddr, out StakeState nextStakeState)); + Assert.Equal(3, nextStakeState.StateVersion); + } + + world = DelegationUtil.EnsureStakeReleased( + nextState, height + LegacyStakeState.LockupInterval); + + var expectedBalance = _ncg * Math.Max(0, previousAmount - amount); + var actualBalance = world.GetBalance(_agentAddr, _ncg); + var nonValidatorDelegateeBalance = world.GetBalance( + Addresses.NonValidatorDelegatee, Currencies.GuildGold); + var stakeBalance = world.GetBalance(stakeStateAddr, Currencies.GuildGold); + Assert.Equal(expectedBalance, actualBalance); + Assert.Equal(Currencies.GuildGold * 0, nonValidatorDelegateeBalance); + Assert.Equal(Currencies.GuildGold * amount, stakeBalance); + } + private IWorld Execute( long blockIndex, IWorld previousState, 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/SetValidatorCommissionTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SetValidatorCommissionTest.cs index 6886fa952c..1b6ef02a35 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/SetValidatorCommissionTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/SetValidatorCommissionTest.cs @@ -4,6 +4,7 @@ namespace Lib9c.Tests.Action.ValidatorDelegation using System.Collections.Generic; using System.Numerics; using Libplanet.Crypto; + using Nekoyume.Action; using Nekoyume.Action.ValidatorDelegation; using Nekoyume.ValidatorDelegation; using Xunit; @@ -49,14 +50,12 @@ private static readonly long CommissionPercentageChangeCooldown [Fact] public void Serialization() { - var address = new PrivateKey().Address; BigInteger commissionPercentage = 10; - var action = new SetValidatorCommission(address, commissionPercentage); + var action = new SetValidatorCommission(commissionPercentage); var plainValue = action.PlainValue; var deserialized = new SetValidatorCommission(); deserialized.LoadPlainValue(plainValue); - Assert.Equal(address, deserialized.ValidatorDelegatee); Assert.Equal(commissionPercentage, deserialized.CommissionPercentage); } @@ -73,7 +72,7 @@ public void Execute() // When var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, commissionPercentage: 11); + commissionPercentage: 11); var actionContext = new ActionContext { PreviousState = world, @@ -109,7 +108,6 @@ public void Execute_Theory(int oldCommissionPercentage, int newCommissionPercent // When var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, newCommissionPercentage); var actionContext = new ActionContext { @@ -148,7 +146,6 @@ public void Execute_Theory_WithValueGreaterThanMaximum_Throw(int commissionPerce BlockIndex = height + CommissionPercentageChangeCooldown, }; var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, commissionPercentage); // Then @@ -178,7 +175,6 @@ public void Execute_Theory_WithNegative_Throw(int commissionPercentage) BlockIndex = height + CommissionPercentageChangeCooldown, }; var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, commissionPercentage); // Then @@ -206,8 +202,7 @@ public void Execute_Theory_WithInvalidValue_Throw(int cooldown) Signer = validatorKey.Address, BlockIndex = height + cooldown, }; - var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, commissionPercentage: 14); + var setValidatorCommission = new SetValidatorCommission(commissionPercentage: 14); // Then Assert.Throws( @@ -235,8 +230,7 @@ public void Execute_Theory_WitValue(int period) Signer = validatorKey.Address, BlockIndex = height + period, }; - var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, commissionPercentage: expectedCommission); + var setValidatorCommission = new SetValidatorCommission(expectedCommission); world = setValidatorCommission.Execute(actionContext); // Then @@ -246,5 +240,59 @@ public void Execute_Theory_WitValue(int period) Assert.Equal(expectedCommission, actualPercentage); } + + [Fact] + public void Execute_NotValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var agentAddress = new PrivateKey().Address; + var validatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height); + + // When + var setValidatorCommission = new SetValidatorCommission( + commissionPercentage: 11); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = height + CommissionPercentageChangeCooldown, + }; + + Assert.Throws( + () => setValidatorCommission.Execute(actionContext)); + } + + [Fact] + public void Execute_With_SameValue_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height); + + // When + var repository = new ValidatorRepository(world, new ActionContext()); + var delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + var commissionPercentage = delegatee.CommissionPercentage; + var setValidatorCommission = new SetValidatorCommission( + commissionPercentage: commissionPercentage); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + CommissionPercentageChangeCooldown, + }; + + Assert.Throws( + () => setValidatorCommission.Execute(actionContext)); + } } } diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs index 7cd66f9f5d..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 { @@ -452,8 +453,7 @@ protected static IWorld EnsureCommissionChangedValidator( BlockIndex = blockHeight, Signer = validatorKey.Address, }; - var setValidatorCommission = new SetValidatorCommission( - validatorKey.Address, currentCommission + increment); + var setValidatorCommission = new SetValidatorCommission(currentCommission + increment); world = setValidatorCommission.Execute(actionContext); currentCommission += increment; preferredHeight = blockHeight + cooldown; @@ -593,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/ClaimStakeReward.cs b/Lib9c/Action/ClaimStakeReward.cs index 98073aca4a..a027f35e2f 100644 --- a/Lib9c/Action/ClaimStakeReward.cs +++ b/Lib9c/Action/ClaimStakeReward.cs @@ -16,7 +16,9 @@ using Nekoyume.Model.Stake; using Nekoyume.Model.State; using Nekoyume.Module; +using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TableData; +using Nekoyume.ValidatorDelegation; using static Lib9c.SerializeKeys; namespace Nekoyume.Action @@ -58,6 +60,16 @@ public override IWorld Execute(IActionContext context) var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); var stakeStateAddr = LegacyStakeState.DeriveAddress(context.Signer); + + var validatorRepository = new ValidatorRepository(states, context); + var isValidator = validatorRepository.TryGetValidatorDelegatee( + context.Signer, out var _); + if (isValidator) + { + throw new InvalidOperationException( + "The validator cannot claim the stake reward."); + } + if (!states.TryGetStakeState(context.Signer, out var stakeStateV2)) { throw new FailedLoadStateException( diff --git a/Lib9c/Action/Stake.cs b/Lib9c/Action/Stake.cs index b87ce5a9c2..b59ead3e07 100644 --- a/Lib9c/Action/Stake.cs +++ b/Lib9c/Action/Stake.cs @@ -16,6 +16,7 @@ using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.Module.Guild; +using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TableData; using Nekoyume.TableData.Stake; using Nekoyume.TypedAddress; @@ -142,7 +143,13 @@ public override IWorld Execute(IActionContext context) // NOTE: Cannot anything if staking state is claimable. if (stakeStateV2.ClaimableBlockIndex <= context.BlockIndex) { - throw new StakeExistingClaimableException(); + var validatorRepository = new ValidatorRepository(states, context); + var isValidator = validatorRepository.TryGetValidatorDelegatee( + context.Signer, out var validatorDelegatee); + if (!isValidator) + { + throw new StakeExistingClaimableException(); + } } // NOTE: When the staking state is locked up. diff --git a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs index c68def6be6..29ca2abd83 100644 --- a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs +++ b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs @@ -46,24 +46,24 @@ public override IWorld Execute(IActionContext context) var unbondingSet = repository.GetUnbondingSet(); var unbondings = unbondingSet.UnbondingsToRelease(context.BlockIndex); - foreach (var unbonding in unbondings) + unbondings = unbondings.Select(unbonding => { 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; + return unbondLockIn; case RebondGrace rebondGrace: - rebondGrace.Release(context.BlockIndex, out _); + rebondGrace = rebondGrace.Release(context.BlockIndex, out _); repository.SetRebondGrace(rebondGrace); - break; + return rebondGrace; default: throw new InvalidOperationException("Invalid unbonding type."); } - } + }).ToImmutableArray(); repository.SetUnbondingSet(unbondingSet.SetUnbondings(unbondings)); diff --git a/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs b/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs index ebf99fbb4b..405d8a3666 100644 --- a/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs +++ b/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs @@ -3,11 +3,13 @@ using Bencodex.Types; using Libplanet.Action.State; using Libplanet.Action; -using Libplanet.Crypto; using Nekoyume.ValidatorDelegation; namespace Nekoyume.Action.ValidatorDelegation { + /// + /// Set the commission percentage of the validator. + /// [ActionType(TypeIdentifier)] public sealed class SetValidatorCommission : ActionBase { @@ -15,20 +17,16 @@ public sealed class SetValidatorCommission : ActionBase public SetValidatorCommission() { } - public SetValidatorCommission(Address validatorDelegatee, BigInteger commissionPercentage) + public SetValidatorCommission(BigInteger commissionPercentage) { - ValidatorDelegatee = validatorDelegatee; CommissionPercentage = commissionPercentage; } - public Address ValidatorDelegatee { get; private set; } - public BigInteger CommissionPercentage { get; private set; } public override IValue PlainValue => Dictionary.Empty .Add("type_id", TypeIdentifier) .Add("values", List.Empty - .Add(ValidatorDelegatee.Bencoded) .Add(CommissionPercentage)); public override void LoadPlainValue(IValue plainValue) @@ -41,17 +39,17 @@ public override void LoadPlainValue(IValue plainValue) throw new InvalidCastException(); } - ValidatorDelegatee = new Address(values[0]); - CommissionPercentage = (Integer)values[1]; + CommissionPercentage = (Integer)values[0]; } public override IWorld Execute(IActionContext context) { GasTracer.UseGas(1); + var validatorAddress = context.Signer; var world = context.PreviousState; var repository = new ValidatorRepository(world, context); - repository.SetCommissionPercentage(ValidatorDelegatee, CommissionPercentage, context.BlockIndex); + repository.SetCommissionPercentage(validatorAddress, CommissionPercentage, context.BlockIndex); return repository.World; } 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..8e16dedc10 100644 --- a/Lib9c/Delegation/Delegatee.cs +++ b/Lib9c/Delegation/Delegatee.cs @@ -165,6 +165,35 @@ public Address UnbondLockInAddress(Address delegatorAddress) public Address RebondGraceAddress(Address delegatorAddress) => Metadata.RebondGraceAddress(delegatorAddress); + /// + /// Get the of the distribution pool + /// where the rewards are distributed from. + /// + /// + /// of the distribution pool. + /// + public Address DistributionPoolAddress() + => Metadata.DistributionPoolAddress(); + + /// + /// Get the of the current . + /// + /// + /// of the current . + /// + public Address CurrentRewardBaseAddress() + => Metadata.CurrentRewardBaseAddress(); + + /// + /// Get the of the at the given height. + /// + /// + /// + /// of the at the given height. + /// + public Address RewardBaseAddress(long height) + => Metadata.RewardBaseAddress(height); + public Address CurrentLumpSumRewardsRecordAddress() => Metadata.CurrentLumpSumRewardsRecordAddress(); @@ -240,14 +269,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 +303,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, TotalShares); + + 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 +424,45 @@ ImmutableDictionary reward return reward; } - private void StartNewRewardPeriod(long height) + /// + /// Start a new reward period. + /// It generates a new and archives the current one. + /// + /// + /// The height of the block where the new reward period starts. + /// + 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.UpdateSigFig(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 +503,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 +543,77 @@ 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; + 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 + { + var newRewardBase = rewardBase.UpdateSigFig(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, recordEach.TotalShares); + 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..29dd536f07 100644 --- a/Lib9c/Delegation/DelegateeMetadata.cs +++ b/Lib9c/Delegation/DelegateeMetadata.cs @@ -20,6 +20,47 @@ public class DelegateeMetadata : IDelegateeMetadata private Address? _address; private readonly IComparer _currencyComparer = new CurrencyComparer(); + /// + /// Create a new instance of DelegateeMetadata. + /// + /// + /// The of the . + /// + /// + /// The of the account of the . + /// + /// + /// The used for delegation. + /// + /// + /// The enumerable of s used for reward. + /// + /// + /// The of the delegation pool that stores + /// delegated s. + /// + /// + /// The of the reward pool that gathers + /// rewards to be distributed. + /// + /// + /// The of the reward remainder pool to + /// sends the remainder of the rewards to. + /// + /// + /// The of the pool that sends the slashed + /// s to. + /// + /// + /// The period in blocks that the unbonded s + /// can be withdrawn. + /// + /// + /// The maximum number of entries that can be locked in for unbonding. + /// + /// + /// The maximum number of entries that can be locked in for rebonding. + /// public DelegateeMetadata( Address delegateeAddress, Address delegateeAccountAddress, @@ -62,12 +103,12 @@ public DelegateeMetadata( } public DelegateeMetadata( - Address address, - Address accountAddress, + Address delegateeAddress, + Address delegateeAccountAddress, List bencoded) { Currency delegationCurrency; - IEnumerable< Currency > rewardCurrencies; + IEnumerable rewardCurrencies; Address delegationPoolAddress; Address rewardPoolAddress; Address rewardRemainderPoolAddress; @@ -152,8 +193,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 +381,57 @@ public Address UnbondLockInAddress(Address delegatorAddress) public virtual Address RebondGraceAddress(Address delegatorAddress) => DelegationAddress.RebondGraceAddress(Address, delegatorAddress); + /// + /// Get the of the distribution pool + /// where the rewards are distributed from. + /// + /// + /// of the distribution pool. + /// + public virtual Address DistributionPoolAddress() + => DelegationAddress.DistributionPoolAddress(Address); + + /// + /// Get the of the current . + /// + /// + /// of the current . + /// + public virtual Address CurrentRewardBaseAddress() + => DelegationAddress.CurrentRewardBaseAddress(Address); + + /// + /// Get the of the at the given height. + /// + /// + /// + /// of the at the given height. + /// + public virtual Address RewardBaseAddress(long height) + => DelegationAddress.RewardBaseAddress(Address, height); + + /// + /// Get the of the current lump sum rewards record. + /// This will be removed after the migration is done. + /// + /// + /// of the current lump sum rewards record. + /// public virtual Address CurrentLumpSumRewardsRecordAddress() - => DelegationAddress.CurrentLumpSumRewardsRecordAddress(Address); - + => DelegationAddress.CurrentRewardBaseAddress(Address); + + /// + /// Get the of the lump sum rewards record at the given height. + /// This will be removed after the migration is done. + /// + /// + /// The height of the lump sum rewards record. + /// + /// + /// of the lump sum rewards record at the given height. + /// 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..4a850d65dd 100644 --- a/Lib9c/Delegation/DelegationAddress.cs +++ b/Lib9c/Delegation/DelegationAddress.cs @@ -65,29 +65,77 @@ public static Address RebondGraceAddress( delegateeMetadataAddress, delegatorAddress.ByteArray); - public static Address CurrentLumpSumRewardsRecordAddress( + /// + /// Get the of the current . + /// + /// + /// of the . + /// + /// + /// of the account of the . + /// + /// + /// of the current . + /// + public static Address CurrentRewardBaseAddress( Address delegateeAddress, Address delegateeAccountAddress) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); - public static Address CurrentLumpSumRewardsRecordAddress( + /// + /// Get the of the current . + /// + /// + /// of the . + /// + /// + /// of the current . + /// + public static Address CurrentRewardBaseAddress( Address delegateeMetadataAddress) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, delegateeMetadataAddress); - public static Address LumpSumRewardsRecordAddress( + /// + /// Get the of the at the given height. + /// + /// + /// of the . + /// + /// + /// of the account of the . + /// + /// + /// The height of the . + /// + /// + /// of the at the given height. + /// + 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( + /// + /// Get the of the at the given height. + /// + /// + /// of the . + /// + /// + /// The height of the . + /// + /// + /// of the at the given height. + /// + public static Address RewardBaseAddress( Address delegateeMetadataAddress, long height) => DeriveAddress( - DelegationElementType.LumpSumRewardsRecord, + DelegationElementType.RewardBase, delegateeMetadataAddress, BitConverter.GetBytes(height)); @@ -103,6 +151,41 @@ public static Address RewardPoolAddress( DelegationElementType.RewardPool, delegateeMetadataAddress); + /// + /// Get the of the distribution pool + /// where the rewards are distributed from. + /// + /// + /// of the . + /// + /// + /// of the account of the . + /// + /// + /// of the distribution pool. + /// + public static Address DistributionPoolAddress( + Address delegateeAddress, Address delegateeAccountAddress) + => DeriveAddress( + DelegationElementType.DistributionPool, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); + + /// + /// Get the of the distribution pool + /// where the rewards are distributed from. + /// + /// + /// of the . + /// + /// + /// of the distribution pool. + /// + public static Address DistributionPoolAddress( + Address delegateeMetadataAddress) + => DeriveAddress( + DelegationElementType.DistributionPool, + delegateeMetadataAddress); + public static Address DelegationPoolAddress( Address delegateeAddress, Address delegateeAccountAddress) => DeriveAddress( @@ -138,9 +221,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..26f33397b1 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,27 +71,63 @@ public DelegationRepository( .SetAccount(UnbondLockInAccountAddress, unbondLockInAccount) .SetAccount(RebondGraceAccountAddress, rebondGraceAccount) .SetAccount(UnbondingSetAccountAddress, unbondingSetAccount) + .SetAccount(RewardBaseAccountAddress, rewardBaseAccount) .SetAccount(LumpSumRewardsRecordAccountAddress, lumpSumRewardsRecordAccount); + /// + /// of the current action. + /// public IActionContext ActionContext { get; } + /// + /// of the account. + /// public Address DelegateeAccountAddress { get; } + /// + /// of the account. + /// public Address DelegatorAccountAddress { get; } - private Address DelegateeMetadataAccountAddress { get; } - - private Address DelegatorMetadataAccountAddress { get; } - - private Address BondAccountAddress { get; } - - private Address UnbondLockInAccountAddress { get; } - - private Address RebondGraceAccountAddress { get; } - - private Address UnbondingSetAccountAddress { get; } - - private Address LumpSumRewardsRecordAccountAddress { get; } + /// + /// of the account. + /// + public Address DelegateeMetadataAccountAddress { get; } + + /// + /// of the account. + /// + public Address DelegatorMetadataAccountAddress { get; } + + /// + /// of the account. + /// + public Address BondAccountAddress { get; } + + /// + /// of the account. + /// + public Address UnbondLockInAccountAddress { get; } + + /// + /// of the account + /// + public Address RebondGraceAccountAddress { get; } + + /// + /// of the account. + /// + public Address UnbondingSetAccountAddress { get; } + + /// + /// of the account. + /// + public Address RewardBaseAccountAddress { get; } + + /// + /// of the account. + /// + public Address LumpSumRewardsRecordAccountAddress { get; } public abstract IDelegatee GetDelegatee(Address address); @@ -162,6 +203,26 @@ 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 +286,24 @@ 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 +318,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..463f9aaeb0 100644 --- a/Lib9c/Delegation/IDelegatee.cs +++ b/Lib9c/Delegation/IDelegatee.cs @@ -65,6 +65,25 @@ public interface IDelegatee Address RebondGraceAddress(Address delegatorAddress); + /// + /// Get the of the current . + /// + /// + /// The of the current . + /// + Address CurrentRewardBaseAddress(); + + /// + /// Get the of the at the given height. + /// + /// + /// The height of the . + /// + /// + /// The of the at the given height. + /// + Address RewardBaseAddress(long height); + Address CurrentLumpSumRewardsRecordAddress(); Address LumpSumRewardsRecordAddress(long height); diff --git a/Lib9c/Delegation/IDelegationRepository.cs b/Lib9c/Delegation/IDelegationRepository.cs index 47c988a0b5..cec2148a04 100644 --- a/Lib9c/Delegation/IDelegationRepository.cs +++ b/Lib9c/Delegation/IDelegationRepository.cs @@ -36,10 +36,37 @@ public interface IDelegationRepository UnbondingSet GetUnbondingSet(); - LumpSumRewardsRecord? GetLumpSumRewardsRecord(IDelegatee delegatee, long height); + /// + /// Get the current of the . + /// + /// + /// The to get the current . + /// + /// + /// The current of the . + /// + RewardBase? GetCurrentRewardBase(IDelegatee delegatee); + + /// + /// Get the of the + /// at the given . + /// + /// + /// The to get the of. + /// + /// + /// The height to get the at. + /// + /// + /// The of the + /// at the given . + /// + 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 +85,26 @@ public interface IDelegationRepository void SetUnbondingSet(UnbondingSet unbondingSet); + /// + /// Set the of the . + /// + /// + /// The to set. + /// + void SetRewardBase(RewardBase rewardBase); + void SetLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord); + /// + /// Remove the from the . + /// This is used when the is no longer needed. + /// This can be removed when the migration for is done. + /// + /// + /// The to remove. + /// + 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..7b1eb491ac --- /dev/null +++ b/Lib9c/Delegation/RewardBase.cs @@ -0,0 +1,406 @@ +#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, + 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; + var bencodedRewardPortion = ((List)bencoded[2]).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[3]; + + try + { + StartHeight = (Integer)bencoded[4]; + } + catch (IndexOutOfRangeException) + { + StartHeight = null; + } + } + + /// + /// Constructor for new . + /// This constructor is used only for the initial reward base creation. + /// + /// + /// of . + /// + /// + /// of 's creation height. + /// It initializes the reward portion with 0. + /// + /// Significant figure of . + /// + private RewardBase( + Address address, + IEnumerable currencies, + int sigFig) + : this( + address, + currencies.Select(c => (c, BigInteger.Zero)), + sigFig, + null) + { + } + + /// + /// Constructor for new . + /// + /// + /// of . + /// + /// + /// 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, + IEnumerable<(Currency, BigInteger)> rewardPortion, + int sigFig, + long? startHeight = null) + { + Address = address; + + 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 . + /// + /// + /// Cumulative reward portion of 's creation height. + /// + /// + /// Significant figure of . + /// + /// + /// Start height of that attached when archived. + /// + private RewardBase( + Address address, + ImmutableSortedDictionary rewardPortion, + int sigFig, + long? startHeight = null) + { + Address = address; + RewardPortion = rewardPortion; + SigFig = sigFig; + StartHeight = startHeight; + } + + /// + /// of . + /// + public Address Address { 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(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. + /// + /// + /// used as denominator of the portion. + /// + /// New with added rewards. + /// + public RewardBase AddRewards(IEnumerable rewards, BigInteger totalShares) + => rewards.Aggregate(this, (accum, next) => AddReward(accum, next, totalShares)); + + /// + /// Add reward to the . + /// + /// + /// Reward to add. + /// + /// + /// used as denominator of the portion. + /// + /// + /// New with added reward. + /// + public RewardBase AddReward(FungibleAssetValue reward, BigInteger totalShares) + => AddReward(this, reward, totalShares); + + /// + /// Update the total shares of the . + /// + /// + /// used as denominator of the portion. + /// + /// + /// New with updated total shares. + /// + public RewardBase UpdateSigFig(BigInteger totalShares) + => UpdateSigFig(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, + 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, BigInteger totalShares) + { + 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 / totalShares); + + return new RewardBase( + rewardBase.Address, + rewardBase.RewardPortion.SetItem(reward.Currency, updatedPortion), + rewardBase.SigFig, + rewardBase.StartHeight); + } + + private static RewardBase UpdateSigFig(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, + newPortion, + newSigFig); + } + + public 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 + && 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..3e67bab5b3 100644 --- a/Lib9c/ValidatorDelegation/AbstainHistory.cs +++ b/Lib9c/ValidatorDelegation/AbstainHistory.cs @@ -42,7 +42,10 @@ public AbstainHistory(List bencoded) public static int WindowSize => 10; - public static int MaxAbstainAllowance => 3; + /// + /// Maximum abstain allowance to slash and jail. + /// + public static int MaxAbstainAllowance => 9; public static Address Address => new Address( ImmutableArray.Create( diff --git a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs index 7ebe20bea4..69513711e9 100644 --- a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs +++ b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs @@ -162,6 +162,12 @@ FungibleAssetValue commission public void SetCommissionPercentage(BigInteger percentage, long height) { + if (CommissionPercentage == percentage) + { + throw new InvalidOperationException( + "The commission percentage is already set to the requested value."); + } + if (height - CommissionPercentageLastUpdateHeight < CommissionPercentageUpdateCooldown) { throw new InvalidOperationException( 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);