diff --git a/.Lib9c.Plugin/PluginActionEvaluator.cs b/.Lib9c.Plugin/PluginActionEvaluator.cs index 3f92335f0b..0a9c3366de 100644 --- a/.Lib9c.Plugin/PluginActionEvaluator.cs +++ b/.Lib9c.Plugin/PluginActionEvaluator.cs @@ -29,7 +29,6 @@ public PluginActionEvaluator(IPluginKeyValueStore keyValueStore) new UpdateValidators(), new RecordProposer(), new RewardGold(), - new ReleaseValidatorUnbondings(), }.ToImmutableArray(), beginTxActions: new IAction[] { new Mortgage(), diff --git a/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs b/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs index 4746df15d5..e7f3f27b41 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs @@ -1089,7 +1089,7 @@ public void StressTest() private IWorld Stake(IWorld world, Address agentAddress) { - var action = new Stake(new BigInteger(500_000)); + var action = new Stake(new BigInteger(500_000), TesterAvatarAddress); var state = action.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/AdventureBoss/ExploreAdventureBossTest.cs b/.Lib9c.Tests/Action/AdventureBoss/ExploreAdventureBossTest.cs index c742acc4fd..51d42e329d 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/ExploreAdventureBossTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/ExploreAdventureBossTest.cs @@ -346,7 +346,7 @@ out var items private IWorld Stake(IWorld world, Address agentAddress) { - var action = new Stake(new BigInteger(500_000)); + var action = new Stake(new BigInteger(500_000), TesterAvatarAddress); var state = action.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs b/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs index ba9d4d05e9..5e39f65482 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs @@ -256,7 +256,7 @@ public void Execute( private IWorld Stake(IWorld world, Address agentAddress) { - var action = new Stake(new BigInteger(500_000)); + var action = new Stake(new BigInteger(500_000), TesterAvatarAddress); var state = action.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs b/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs index 75019d0372..814e380c0a 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs @@ -227,7 +227,7 @@ Type exc private IWorld Stake(IWorld world, Address agentAddress) { - var action = new Stake(new BigInteger(500_000)); + var action = new Stake(new BigInteger(500_000), TesterAvatarAddress); var state = action.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs b/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs index bf26cdef21..0a4298720a 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs @@ -596,7 +596,7 @@ private IWorld Stake(IWorld world, long amount = 0) world = DelegationUtil.EnsureValidatorPromotionReady(world, validatorKey, 0L); world = DelegationUtil.MakeGuild(world, AgentAddress, validatorKey.Address, 0L); - var action = new Stake(new BigInteger(amount)); + var action = new Stake(new BigInteger(amount), AvatarAddress); return action.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs b/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs index c12d4dc85a..54cd68d706 100644 --- a/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs +++ b/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs @@ -1,17 +1,49 @@ namespace Lib9c.Tests.Action.Guild; using System; +using System.Collections.Generic; +using System.Numerics; using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action.Guild; using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; using Xunit; public class BanGuildMemberTest : GuildTestBase { + private interface IBanGuildMemberFixture + { + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + [Fact] public void Serialization() { @@ -27,37 +59,27 @@ public void Serialization() [Fact] public void Execute() { - // Given - var world = World; - var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, targetGuildMemberAddress, GG * 100); - world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); - - // When - var banGuildMember = new BanGuildMember(targetGuildMemberAddress); - var actionContext = new ActionContext + var fixture = new StaticFixture { - PreviousState = world, - Signer = guildMasterAddress, - BlockIndex = 2L, + ValidatorNCG = NCG * 100, + SlashFactor = 0, + AgentNCG = NCG * 100, + MasterNCG = NCG * 100, }; - world = banGuildMember.Execute(actionContext); - - // Then - var guildRepository = new GuildRepository(world, actionContext); - var validatorRepository = new ValidatorRepository(world, actionContext); - var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); - var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + ExecuteWithFixture(fixture); + } - Assert.True(guildRepository.IsBanned(guildAddress, targetGuildMemberAddress)); - Assert.Null(guildRepository.GetJoinedGuild(targetGuildMemberAddress)); - Assert.Equal(0, guildDelegatee.TotalShares); - Assert.Equal(0, validatorDelegatee.TotalShares); + [Fact] + public void Execute_SlashValidator() + { + var fixture = new StaticFixture + { + ValidatorNCG = NCG * 100, + SlashFactor = 10, + AgentNCG = NCG * 100, + MasterNCG = NCG * 100, + }; + ExecuteWithFixture(fixture); } [Fact] @@ -69,8 +91,9 @@ public void Execute_NotMember() var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); // When var banGuildMember = new BanGuildMember(guildMemberAddress); @@ -78,7 +101,7 @@ public void Execute_NotMember() { PreviousState = world, Signer = guildMasterAddress, - BlockIndex = 2L, + BlockIndex = height, }; world = banGuildMember.Execute(actionContext); @@ -122,8 +145,9 @@ public void Execute_UnknownGuild_Throw() var guildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var unknownGuildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); world = EnsureToSetGuildParticipant(world, guildMasterAddress, unknownGuildAddress); // When @@ -141,7 +165,7 @@ public void Execute_UnknownGuild_Throw() } [Fact] - public void Execute_SignerIsNotGuildMaster_Throw() + public void Execute_SignerIsNotMaster_Throw() { // Given var world = World; @@ -149,9 +173,10 @@ public void Execute_SignerIsNotGuildMaster_Throw() var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, height++); // When var banGuildMember = new BanGuildMember(guildMemberAddress); @@ -159,6 +184,7 @@ public void Execute_SignerIsNotGuildMaster_Throw() { PreviousState = world, Signer = guildMemberAddress, + BlockIndex = height, }; // Then @@ -168,15 +194,16 @@ public void Execute_SignerIsNotGuildMaster_Throw() } [Fact] - public void Execute_TargetIsGuildMaster_Throw() + public void Execute_TargetIsMaster_Throw() { // Given var world = World; var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); // When var banGuildMember = new BanGuildMember(guildMasterAddress); @@ -194,22 +221,23 @@ public void Execute_TargetIsGuildMaster_Throw() // Expected use-case. [Fact] - public void Ban_By_GuildMaster() + public void Ban_By_Master() { var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var otherGuildMasterAddress = AddressUtil.CreateAgentAddress(); + var otherMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var otherGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var otherGuildAddress = AddressUtil.CreateGuildAddress(); + var height = 0L; IWorld world = World; - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); - world = EnsureToMakeGuild(world, otherGuildAddress, otherGuildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, otherGuildAddress, otherGuildMemberAddress, 1L); + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, height++); + world = EnsureToMakeGuild(world, otherGuildAddress, otherMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, otherGuildAddress, otherGuildMemberAddress, height++); var repository = new GuildRepository(world, new ActionContext()); // Guild @@ -218,8 +246,8 @@ public void Ban_By_GuildMaster() Assert.False(repository.IsBanned(guildAddress, guildMemberAddress)); Assert.Equal(guildAddress, repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.False(repository.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.False(repository.IsBanned(guildAddress, otherMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherMasterAddress)); Assert.False(repository.IsBanned(guildAddress, otherGuildMemberAddress)); Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); @@ -237,12 +265,12 @@ public void Ban_By_GuildMaster() Assert.True(repository.IsBanned(guildAddress, guildMemberAddress)); Assert.Null(repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.False(repository.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.False(repository.IsBanned(guildAddress, otherMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherMasterAddress)); Assert.False(repository.IsBanned(guildAddress, otherGuildMemberAddress)); Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); - action = new BanGuildMember(otherGuildMasterAddress); + action = new BanGuildMember(otherMasterAddress); world = action.Execute(new ActionContext { PreviousState = repository.World, @@ -256,8 +284,8 @@ public void Ban_By_GuildMaster() Assert.True(repository.IsBanned(guildAddress, guildMemberAddress)); Assert.Null(repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.True(repository.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.True(repository.IsBanned(guildAddress, otherMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherMasterAddress)); Assert.False(repository.IsBanned(guildAddress, otherGuildMemberAddress)); Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); @@ -275,13 +303,13 @@ public void Ban_By_GuildMaster() Assert.True(repository.IsBanned(guildAddress, guildMemberAddress)); Assert.Null(repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.True(repository.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.True(repository.IsBanned(guildAddress, otherMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherMasterAddress)); Assert.True(repository.IsBanned(guildAddress, otherGuildMemberAddress)); Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); action = new BanGuildMember(guildMasterAddress); - // GuildMaster cannot ban itself. + // Master cannot ban itself. Assert.Throws(() => action.Execute(new ActionContext { PreviousState = repository.World, @@ -298,14 +326,15 @@ public void Ban_By_GuildMember() var otherAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); + var height = 0L; var action = new BanGuildMember(targetGuildMemberAddress); IWorld world = World; - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); - world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, height++); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, height++); var repository = new GuildRepository(world, new ActionContext()); @@ -343,11 +372,12 @@ public void Ban_By_Other() var otherAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); + var height = 0L; IWorld world = World; - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, height++); var repository = new GuildRepository(world, new ActionContext()); @@ -359,7 +389,7 @@ public void Ban_By_Other() Signer = otherAddress, })); - // Other tries to ban GuildMaster. + // Other tries to ban Master. action = new BanGuildMember(guildMasterAddress); Assert.Throws( () => action.Execute( @@ -369,4 +399,135 @@ public void Ban_By_Other() Signer = otherAddress, })); } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private void ExecuteWithFixture(IBanGuildMemberFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorKey; + var validatorNCG = fixture.ValidatorNCG; + var validatorGG = NCGToGG(validatorNCG); + var masterAddress = fixture.MasterAddress; + var masterNCG = fixture.MasterNCG; + var masterGG = NCGToGG(masterNCG); + var agentAddress = fixture.AgentAddress; + var agentNCG = fixture.AgentNCG; + var agentGG = NCGToGG(agentNCG); + var guildAddress = fixture.GuildAddress; + var height = 0L; + var slashFactor = fixture.SlashFactor; + world = EnsureToInitializeValidator(world, validatorKey, validatorNCG, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToInitializeAgent(world, masterAddress, masterNCG, height++); + world = EnsureToInitializeAgent(world, agentAddress, agentNCG, height++); + world = EnsureToStake(world, masterAddress, masterNCG, height++); + world = EnsureToStake(world, agentAddress, agentNCG, height++); + world = EnsureToJoinGuild(world, guildAddress, agentAddress, height++); + if (slashFactor > 0) + { + world = EnsureToSlashValidator(world, validatorKey, slashFactor, height++); + } + + // When + var totalGG = validatorGG + masterGG + agentGG; + var slashedGG = SlashFAV(slashFactor, totalGG); + var totalShare = totalGG.RawValue; + var agentShare = totalShare * agentGG.RawValue / totalGG.RawValue; + var expectedAgengGG = (slashedGG * agentShare).DivRem(totalShare).Quotient; + var expectedTotalGG = slashedGG - expectedAgengGG; + var expectedTotalShares = totalShare - agentShare; + var banGuildMember = new BanGuildMember(agentAddress); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = masterAddress, + BlockIndex = height, + }; + world = banGuildMember.Execute(actionContext); + + // Then + var guildRepository = new GuildRepository(world, actionContext); + var validatorRepository = new ValidatorRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + + Assert.True(guildRepository.IsBanned(guildAddress, agentAddress)); + Assert.Null(guildRepository.GetJoinedGuild(agentAddress)); + Assert.Equal(expectedTotalGG, guildDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, guildDelegatee.TotalShares); + Assert.Equal(expectedTotalGG, validatorDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, validatorDelegatee.TotalShares); + } + + private class StaticFixture : IBanGuildMemberFixture + { + public PrivateKey ValidatorKey { get; set; } = new PrivateKey(); + + public FungibleAssetValue ValidatorNCG { get; set; } = NCG * 100; + + public BigInteger SlashFactor { get; set; } = 0; + + public AgentAddress AgentAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue AgentNCG { get; set; } = NCG * 100; + + public GuildAddress GuildAddress { get; set; } = AddressUtil.CreateGuildAddress(); + + public AgentAddress MasterAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue MasterNCG { get; set; } = NCG * 100; + } + + private class RandomFixture : IBanGuildMemberFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorKey = GetRandomKey(_random); + ValidatorNCG = GetRandomNCG(_random); + SlashFactor = GetRandomSlashFactor(_random); + AgentAddress = GetRandomAgentAddress(_random); + AgentNCG = GetRandomNCG(_random); + GuildAddress = GetRandomGuildAddress(_random); + MasterAddress = GetRandomAgentAddress(_random); + MasterNCG = GetRandomNCG(_random); + } + + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } } diff --git a/.Lib9c.Tests/Action/Guild/GuildTestBase.cs b/.Lib9c.Tests/Action/Guild/GuildTestBase.cs index 2604a9e8f2..7b6724f34f 100644 --- a/.Lib9c.Tests/Action/Guild/GuildTestBase.cs +++ b/.Lib9c.Tests/Action/Guild/GuildTestBase.cs @@ -1,91 +1,121 @@ namespace Lib9c.Tests.Action.Guild; using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; using Libplanet.Types.Assets; using Nekoyume; +using Nekoyume.Action; +using Nekoyume.Action.ValidatorDelegation; using Nekoyume.Model.Guild; using Nekoyume.Model.Stake; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.Module.Guild; -using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; +using Xunit; public abstract class GuildTestBase { protected static readonly Currency GG = Currencies.GuildGold; + protected static readonly FungibleAssetValue GGEpsilon = new FungibleAssetValue(GG, 0, 1); + protected static readonly FungibleAssetValue GGZero = GG * 0; protected static readonly Currency Mead = Currencies.Mead; protected static readonly Currency NCG = Currency.Uncapped("NCG", 2, null); + protected static readonly FungibleAssetValue NCGEpsilon = new FungibleAssetValue(NCG, 0, 1); protected static readonly BigInteger SharePerGG = BigInteger.Pow(10, Currencies.GuildGold.DecimalPlaces); + private static readonly int _maximumIntegerLength = 15; + private static readonly BigInteger _minimumNCGMajorUnit = 50; + public GuildTestBase() { var world = new World(MockUtil.MockModernWorldState); var goldCurrencyState = new GoldCurrencyState(NCG); World = world .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + World = InitializeUtil.InitializeTableSheets(World, isDevEx: false).states; } protected IWorld World { get; } - protected static IWorld EnsureToMintAsset( - IWorld world, Address address, FungibleAssetValue amount) + protected static IWorld EnsureToInitializeValidator( + IWorld world, PrivateKey validatorKey, FungibleAssetValue ncg, long blockHeight) { - var actionContext = new ActionContext + var validatorAddress = validatorKey.Address; + var validatorPublicKey = validatorKey.PublicKey; + world = EnsureToMintAsset(world, validatorAddress, ncg, blockHeight); + world = EnsureToStake(world, validatorAddress, ncg, blockHeight); + world = EnsureToCreateValidator(world, validatorPublicKey, NCGToGG(ncg)); + return world; + } + + protected static IWorld EnsureToInitializeAgent( + IWorld world, AgentAddress agentAddress, long blockHeight) + { + return EnsureToInitializeAgent(world, agentAddress, NCG * 0, blockHeight); + } + + protected static IWorld EnsureToInitializeAgent( + IWorld world, AgentAddress agentAddress, FungibleAssetValue ncg, long blockHeight) + { + var avatarIndex = 0; + if (ncg.RawValue > 0) { - PreviousState = world, - }; - return world.MintAsset(actionContext, address, amount); + world = EnsureToMintAsset(world, agentAddress, ncg, blockHeight); + } + + world = EnsureToCreateAvatar(world, agentAddress, avatarIndex, blockHeight); + return world; } - protected static IWorld EnsureToCreateValidator( - IWorld world, - PublicKey validatorPublicKey) + protected static IWorld EnsureToMintAsset( + IWorld world, Address address, FungibleAssetValue amount, long blockHeight) { - var validatorAddress = validatorPublicKey.Address; - var commissionPercentage = 10; var actionContext = new ActionContext { - Signer = validatorAddress, + PreviousState = world, + BlockIndex = blockHeight, }; - - var validatorRepository = new ValidatorRepository(world, actionContext); - validatorRepository.CreateDelegatee(validatorPublicKey, commissionPercentage); - - var guildRepository = new GuildRepository(validatorRepository); - guildRepository.CreateDelegatee(validatorAddress); - - return guildRepository.World; + return world.MintAsset(actionContext, address, amount); } protected static IWorld EnsureToTombstoneValidator( IWorld world, - Address validatorAddress) + PrivateKey validatorPrivateKey, + long blockHeight) { + var validatorAddress = validatorPrivateKey.Address; var actionContext = new ActionContext { Signer = validatorAddress, + BlockIndex = blockHeight, }; var validatorRepository = new ValidatorRepository(world, actionContext); var validatorDelegatee = validatorRepository.GetDelegatee(validatorAddress); validatorDelegatee.Tombstone(); - return validatorRepository.World; + var guildRepository = new GuildRepository( + validatorRepository.World, validatorRepository.ActionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorAddress); + guildDelegatee.Tombstone(); + + return guildRepository.World; } protected static IWorld EnsureToSlashValidator( - IWorld world, - Address validatorAddress, - BigInteger slashFactor, - long blockHeight) + IWorld world, PrivateKey validatorPrivateKey, BigInteger slashFactor, long blockHeight) { + var validatorAddress = validatorPrivateKey.Address; var actionContext = new ActionContext { Signer = validatorAddress, @@ -103,16 +133,32 @@ protected static IWorld EnsureToSlashValidator( return guildRepository.World; } + protected static IWorld EnsureToUndelegateValidator( + IWorld world, PrivateKey validatorPrivateKey, BigInteger share, long blockHeight) + { + var validatorAddress = validatorPrivateKey.Address; + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorAddress, + BlockIndex = blockHeight, + }; + var undelegateValidator = new UndelegateValidator(share); + return undelegateValidator.Execute(actionContext); + } + protected static IWorld EnsureToMakeGuild( IWorld world, GuildAddress guildAddress, - AgentAddress guildMasterAddress, - Address validatorAddress) + AgentAddress masterAddress, + PrivateKey validatorPrivateKey, + long blockHeight) { + var validatorAddress = validatorPrivateKey.Address; var actionContext = new ActionContext { - Signer = guildMasterAddress, - BlockIndex = 0L, + Signer = masterAddress, + BlockIndex = blockHeight, }; var repository = new GuildRepository(world, actionContext); repository.MakeGuild(guildAddress, validatorAddress); @@ -154,33 +200,22 @@ protected static IWorld EnsureToLeaveGuild( return repository.World; } - protected static IWorld EnsureToBanGuildMember( + protected static IWorld EnsureToBanMember( IWorld world, - AgentAddress guildMasterAddress, - AgentAddress agentAddress) + AgentAddress masterAddress, + AgentAddress agentAddress, + long blockHeight) { var actionContext = new ActionContext { Signer = agentAddress, + BlockIndex = blockHeight, }; var repository = new GuildRepository(world, actionContext); - repository.Ban(guildMasterAddress, agentAddress); + repository.Ban(masterAddress, agentAddress); return repository.World; } - protected static IWorld EnsureToPrepareGuildGold( - IWorld world, - Address address, - FungibleAssetValue amount) - { - if (!Equals(amount.Currency, Currencies.GuildGold)) - { - throw new ArgumentException("Currency must be GG.", nameof(amount)); - } - - return EnsureToMintAsset(world, StakeState.DeriveAddress(address), amount); - } - protected static IWorld EnsureToSetGuildParticipant( IWorld world, AgentAddress agentAddress, @@ -192,4 +227,269 @@ protected static IWorld EnsureToSetGuildParticipant( repository.SetGuildParticipant(guildParticipant); return repository.World; } + + protected static IWorld EnsureToStake( + IWorld world, + AgentAddress agentAddress, + int avatarIndex, + FungibleAssetValue ncg, + long blockHeight) + { + if (!ncg.Currency.Equals(NCG)) + { + throw new ArgumentException("Currency must be NCG.", nameof(ncg)); + } + + if (ncg.MinorUnit != 0) + { + throw new ArgumentException("Minor unit must be zero.", nameof(ncg)); + } + + var amount = ncg.MajorUnit; + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = blockHeight, + }; + var agentState = world.GetAgentState(agentAddress); + var avatarAddress = agentState.avatarAddresses[avatarIndex]; + var stake = new Stake(amount, avatarAddress); + return stake.Execute(actionContext); + } + + protected static IWorld EnsureToStakeValidator( + IWorld world, PrivateKey privateKey, FungibleAssetValue ncg, long blockHeight) + { + return EnsureToStake(world, privateKey.Address, ncg, blockHeight); + } + + protected static IWorld EnsureToStake( + IWorld world, Address address, FungibleAssetValue ncg, long blockHeight) + { + if (!ncg.Currency.Equals(NCG)) + { + throw new ArgumentException("Currency must be NCG.", nameof(ncg)); + } + + if (ncg.MinorUnit != 0) + { + throw new ArgumentException("Minor unit must be zero.", nameof(ncg)); + } + + return EnsureToStake(world, address, ncg.MajorUnit, blockHeight); + } + + protected static IWorld EnsureToReleaseUnbonding( + IWorld world, + AgentAddress agentAddress, + long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = agentAddress, + }; + + var guildRepository = new GuildRepository(world, actionContext); + var guildDelegator = guildRepository.GetDelegator(agentAddress); + guildDelegator.ReleaseUnbondings(blockHeight); + world = guildRepository.World; + + var validatorRepository = new ValidatorRepository(world, actionContext); + var validatorDelegator = validatorRepository.GetDelegator(agentAddress); + validatorDelegator.ReleaseUnbondings(blockHeight); + + return validatorRepository.World; + } + + protected static FungibleAssetValue GetRandomFAV(Currency currency) => GetRandomFAV(currency, Random.Shared); + + protected static FungibleAssetValue GetRandomFAV(Currency currency, Random random) + { + var decimalLength = random.Next(currency.DecimalPlaces); + var integerLength = random.Next(1, _maximumIntegerLength); + var decimalPart = Enumerable.Range(0, decimalLength) + .Aggregate(string.Empty, (s, i) => s + random.Next(10)); + var integerPart = Enumerable.Range(0, integerLength) + .Aggregate(string.Empty, (s, i) => s + (i != 0 ? random.Next(10) : random.Next(1, 10))); + var isDecimalZero = decimalLength == 0 || decimalPart.All(c => c == '0'); + var text = isDecimalZero is false ? $"{integerPart}.{decimalPart}" : integerPart; + + return FungibleAssetValue.Parse(currency, text); + } + + protected static FungibleAssetValue GetRandomGG(Random random) + { + return GetRandomFAV(GG, random); + } + + protected static FungibleAssetValue GetRandomNCG(Random random) + { + var ncg = GetRandomFAV(NCG, random); + var majorUnit = ncg.MajorUnit; + if (majorUnit < _minimumNCGMajorUnit) + { + majorUnit += _minimumNCGMajorUnit; + } + + return new FungibleAssetValue(NCG, majorUnit, 0); + } + + protected static PrivateKey GetRandomKey(Random random) + { + var bytes = Enumerable.Range(0, 32).Select(_ => (byte)random.Next(256)).ToArray(); + return new PrivateKey(bytes, informedConsent: true); + } + + protected static AgentAddress GetRandomAgentAddress(Random random) + { + return new AgentAddress(GetRandomKey(random).Address); + } + + protected static GuildAddress GetRandomGuildAddress(Random random) + { + return new GuildAddress(GetRandomKey(random).Address); + } + + protected static BigInteger GetRandomSlashFactor(Random random) + { + return random.Next(0, 100); + } + + protected static FungibleAssetValue SlashFAV( + BigInteger slashFactor, params FungibleAssetValue[] favs) + { + if (favs.Length == 0) + { + throw new ArgumentException("FAVs must not be empty.", nameof(favs)); + } + + if (slashFactor.Sign < 0) + { + throw new ArgumentOutOfRangeException(nameof(slashFactor), slashFactor, "Slash factor must be positive."); + } + + if (slashFactor == BigInteger.Zero) + { + return favs.Aggregate((a, b) => a + b); + } + + var currency = favs[0].Currency; + var sum = new FungibleAssetValue(currency, BigInteger.Zero, BigInteger.Zero); + foreach (var fav in favs) + { + if (!currency.Equals(fav.Currency)) + { + throw new ArgumentException("Currencies must be the same.", nameof(favs)); + } + + var value = fav.DivRem(slashFactor, out var remainder); + if (remainder.Sign > 0) + { + value += FungibleAssetValue.FromRawValue(currency, 1); + } + + sum += fav - value; + } + + return sum; + } + + protected static FungibleAssetValue NCGToGG(FungibleAssetValue ncg) + { + if (!ncg.Currency.Equals(NCG)) + { + throw new ArgumentException("Currency must be NCG.", nameof(ncg)); + } + + var (fav, remainder) = GuildModule.ConvertCurrency(ncg, GG); + if (remainder.Sign != 0) + { + throw new InvalidOperationException("Remainder must be zero."); + } + + return fav; + } + + private static IWorld EnsureToStake( + IWorld world, Address address, BigInteger amount, long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + Signer = address, + BlockIndex = blockHeight, + }; + var stake = new Stake(amount); + return stake.Execute(actionContext); + } + + private static IWorld EnsureToCreateAvatar( + IWorld world, + AgentAddress agentAddress, + int avatarIndex, + long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = blockHeight, + }; + var createAvatar = new CreateAvatar + { + index = avatarIndex, + name = $"avatar{avatarIndex}", + }; + return createAvatar.Execute(actionContext); + } + + private static IWorld EnsureToCreateValidator( + IWorld world, + PublicKey validatorPublicKey, + FungibleAssetValue gg) + { + if (!gg.Currency.Equals(GG)) + { + throw new ArgumentException("Currency must be GG.", nameof(gg)); + } + + var validatorAddress = validatorPublicKey.Address; + var commissionPercentage = 10; + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorAddress, + }; + var promoteValidator = new PromoteValidator( + publicKey: validatorPublicKey, + fav: gg, + commissionPercentage: commissionPercentage); + + return promoteValidator.Execute(actionContext); + } + + protected sealed class FungibleAssetValueEqualityComparer + : IEqualityComparer + { + private readonly FungibleAssetValue _difference; + + public FungibleAssetValueEqualityComparer(FungibleAssetValue difference) + { + _difference = difference; + } + + public bool Equals(FungibleAssetValue x, FungibleAssetValue y) + { + var diff = y - x; + return diff.RawValue == 0 || diff == _difference; + } + + public int GetHashCode([DisallowNull] FungibleAssetValue obj) + { + return obj.GetHashCode(); + } + } } diff --git a/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs b/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs index 6def3b369b..3c413d4e2f 100644 --- a/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs @@ -1,15 +1,47 @@ namespace Lib9c.Tests.Action.Guild; using System; +using System.Collections.Generic; +using System.Numerics; using Lib9c.Tests.Util; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action.Guild; using Nekoyume.Model.Guild; +using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; using Xunit; public class JoinGuildTest : GuildTestBase { + private interface IJoinGuildFixture + { + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + [Fact] public void Serialization() { @@ -25,35 +57,27 @@ public void Serialization() [Fact] public void Execute() { - // Given - var world = World; - var validatorKey = new PrivateKey(); - var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); - - // When - var joinGuild = new JoinGuild(guildAddress); - var actionContext = new ActionContext + var fixture = new StaticFixture { - PreviousState = world, - Signer = agentAddress, + ValidatorNCG = NCG * 100, + SlashFactor = 0, + AgentNCG = NCG * 100, + MasterNCG = NCG * 100, }; - world = joinGuild.Execute(actionContext); - - // Then - var guildRepository = new GuildRepository(world, new ActionContext()); - var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); - var validatorRepository = new ValidatorRepository(world, new ActionContext()); - var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); - var guildParticipant = guildRepository.GetGuildParticipant(agentAddress); + ExecuteWithFixture(fixture); + } - Assert.Equal(agentAddress, guildParticipant.Address); - Assert.Equal(guildDelegatee.TotalShares, 100 * SharePerGG); - Assert.Equal(validatorDelegatee.TotalShares, 100 * SharePerGG); + [Fact] + public void Execute_SlashedValidator() + { + var fixture = new StaticFixture + { + ValidatorNCG = NCG * 100, + SlashFactor = 10, + AgentNCG = NCG * 100, + MasterNCG = NCG * 100, + }; + ExecuteWithFixture(fixture); } [Fact] @@ -63,12 +87,13 @@ public void Execute_SignerIsAlreadyJoinedGuild_Throw() var world = World; var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); - world = EnsureToJoinGuild(world, guildAddress, agentAddress, 1L); + var height = 1L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToInitializeAgent(world, agentAddress, NCG * 100, height++); + world = EnsureToJoinGuild(world, guildAddress, agentAddress, height++); // When var joinGuild = new JoinGuild(guildAddress); @@ -76,7 +101,7 @@ public void Execute_SignerIsAlreadyJoinedGuild_Throw() { PreviousState = world, Signer = agentAddress, - BlockIndex = 2L, + BlockIndex = height, }; // Then @@ -92,12 +117,12 @@ public void Execute_SignerHasRejoinCooldown_Throw() var world = World; var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var height = 1L; - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToInitializeAgent(world, agentAddress, NCG * 100, height++); world = EnsureToJoinGuild(world, guildAddress, agentAddress, height++); world = EnsureToLeaveGuild(world, agentAddress, height); @@ -126,11 +151,12 @@ public void Execute_SignerIsValidator_Throw() var world = World; var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToInitializeAgent(world, agentAddress, NCG * 100, height++); // When var joinGuild = new JoinGuild(guildAddress); @@ -145,4 +171,132 @@ public void Execute_SignerIsValidator_Throw() () => joinGuild.Execute(actionContext)); Assert.Equal("Validator cannot join a guild.", exception.Message); } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private void ExecuteWithFixture(IJoinGuildFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorKey; + var validatorNCG = fixture.ValidatorNCG; + var validatorGG = NCGToGG(validatorNCG); + var slashFactor = fixture.SlashFactor; + var agentAddress = fixture.AgentAddress; + var agentNCG = fixture.AgentNCG; + var agentGG = NCGToGG(agentNCG); + var masterAddress = fixture.MasterAddress; + var masterNCG = fixture.MasterNCG; + var masterGG = NCGToGG(masterNCG); + var guildAddress = fixture.GuildAddress; + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, validatorNCG, height++); + world = EnsureToInitializeAgent(world, masterAddress, masterNCG, height++); + world = EnsureToInitializeAgent(world, agentAddress, agentNCG, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToStake(world, masterAddress, masterNCG, height++); + world = EnsureToStake(world, agentAddress, agentNCG, height++); + if (slashFactor > 1) + { + world = EnsureToSlashValidator(world, validatorKey, slashFactor, height++); + } + + // When + var totalGG = validatorGG + masterGG; + var slashedGG = SlashFAV(slashFactor, validatorGG + masterGG); + var totalShare = totalGG.RawValue; + var agentShare = totalShare * agentGG.RawValue / slashedGG.RawValue; + var expectedTotalGG = slashedGG + agentGG; + var expectedTotalShares = totalShare + agentShare; + var joinGuild = new JoinGuild(guildAddress); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + }; + world = joinGuild.Execute(actionContext); + + // Then + var guildRepository = new GuildRepository(world, new ActionContext()); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + var validatorRepository = new ValidatorRepository(world, new ActionContext()); + var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + var guildParticipant = guildRepository.GetGuildParticipant(agentAddress); + + Assert.Equal(agentAddress, guildParticipant.Address); + Assert.Equal(expectedTotalGG, guildDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, guildDelegatee.TotalShares); + Assert.Equal(expectedTotalGG, validatorDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, validatorDelegatee.TotalShares); + } + + private class StaticFixture : IJoinGuildFixture + { + public PrivateKey ValidatorKey { get; set; } = new PrivateKey(); + + public FungibleAssetValue ValidatorNCG { get; set; } = NCG * 100; + + public BigInteger SlashFactor { get; set; } + + public AgentAddress AgentAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue AgentNCG { get; set; } = NCG * 100; + + public GuildAddress GuildAddress { get; set; } = AddressUtil.CreateGuildAddress(); + + public AgentAddress MasterAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue MasterNCG { get; set; } = NCG * 100; + } + + private class RandomFixture : IJoinGuildFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorKey = GetRandomKey(_random); + ValidatorNCG = GetRandomNCG(_random); + SlashFactor = GetRandomSlashFactor(_random); + AgentAddress = GetRandomAgentAddress(_random); + AgentNCG = GetRandomNCG(_random); + GuildAddress = GetRandomGuildAddress(_random); + MasterAddress = GetRandomAgentAddress(_random); + MasterNCG = GetRandomNCG(_random); + } + + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } } diff --git a/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs b/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs index 356d817708..cb11b9c47f 100644 --- a/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs @@ -2,16 +2,41 @@ namespace Lib9c.Tests.Action.Guild; using System; using System.Collections.Generic; +using System.Numerics; using Lib9c.Tests.Util; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action.Guild; using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; using Xunit; public class MakeGuildTest : GuildTestBase { + private interface IMakeGuildFixture + { + public PrivateKey ValidatorKey { get; } + + public BigInteger SlashFactor { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + public static IEnumerable TestCases => new[] { @@ -41,33 +66,29 @@ public void Serialization() [Fact] public void Execute() { - // Given - var world = World; - var validatorPrivateKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - world = EnsureToCreateValidator(world, validatorPrivateKey.PublicKey); - world = EnsureToPrepareGuildGold(world, guildMasterAddress, GG * 100); - - // When - var makeGuild = new MakeGuild(validatorPrivateKey.Address); - var actionContext = new ActionContext + var fixture = new StaticFixture { - PreviousState = world, - Signer = guildMasterAddress, + ValidatorKey = new PrivateKey(), + ValidatorNCG = NCG * 100, + SlashFactor = 0, + MasterAddress = AddressUtil.CreateAgentAddress(), + MasterNCG = NCG * 100, }; - world = makeGuild.Execute(actionContext); + ExecuteWithFixture(fixture); + } - // Then - var guildRepository = new GuildRepository(world, new ActionContext()); - var validatorRepository = new ValidatorRepository(world, new ActionContext()); - var guildDelegatee = guildRepository.GetDelegatee(validatorPrivateKey.Address); - var validatorDelegatee = validatorRepository.GetDelegatee(validatorPrivateKey.Address); - var guildAddress = guildRepository.GetJoinedGuild(guildMasterAddress); - Assert.NotNull(guildAddress); - var guild = guildRepository.GetGuild(guildAddress.Value); - Assert.Equal(guildMasterAddress, guild.GuildMasterAddress); - Assert.Equal(SharePerGG * 100, guildDelegatee.TotalShares); - Assert.Equal(SharePerGG * 100, validatorDelegatee.TotalShares); + [Fact] + public void Execute_SlashedValidator() + { + var fixture = new StaticFixture + { + ValidatorKey = new PrivateKey(), + ValidatorNCG = NCG * 100, + SlashFactor = 10, + MasterAddress = AddressUtil.CreateAgentAddress(), + MasterNCG = NCG * 100, + }; + ExecuteWithFixture(fixture); } [Fact] @@ -76,17 +97,18 @@ public void Execute_AlreadyGuildOwner_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); // When var makeGuild = new MakeGuild(validatorKey.Address); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, }; // Then @@ -101,8 +123,9 @@ public void Execute_ByValidator_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); + var masterAddress = AddressUtil.CreateAgentAddress(); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); // When var makeGuild = new MakeGuild(validatorKey.Address); @@ -110,6 +133,7 @@ public void Execute_ByValidator_Throw() { PreviousState = world, Signer = validatorKey.Address, + BlockIndex = height, }; // Then @@ -124,14 +148,14 @@ public void Execute_WithUnknowValidator_Throw() // Given var world = World; var validatorPrivateKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); // When var makeGuild = new MakeGuild(validatorPrivateKey.Address); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, }; // Then @@ -139,4 +163,112 @@ public void Execute_WithUnknowValidator_Throw() () => makeGuild.Execute(actionContext)); Assert.Equal("The validator does not exist.", exception.Message); } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private void ExecuteWithFixture(IMakeGuildFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorKey; + var masterAddress = fixture.MasterAddress; + var validatorNCG = fixture.ValidatorNCG; + var validatorGG = NCGToGG(validatorNCG); + var masterNCG = fixture.MasterNCG; + var masterGG = NCGToGG(masterNCG); + var slashFactor = fixture.SlashFactor; + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, validatorNCG, height++); + world = EnsureToInitializeAgent(world, masterAddress, masterNCG, height++); + world = EnsureToStake(world, masterAddress, masterNCG, height++); + if (slashFactor > 0) + { + world = EnsureToSlashValidator(world, validatorKey, slashFactor, height++); + } + + // When + var totalGG = validatorGG; + var slashedGG = SlashFAV(slashFactor, validatorGG); + var totalShare = totalGG.RawValue; + var agentShare = totalShare * masterGG.RawValue / slashedGG.RawValue; + var expectedTotalGG = slashedGG + masterGG; + var expectedTotalShares = totalShare + agentShare; + var makeGuild = new MakeGuild(validatorKey.Address); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = masterAddress, + BlockIndex = height, + }; + world = makeGuild.Execute(actionContext); + + // Then + var guildRepository = new GuildRepository(world, new ActionContext()); + var validatorRepository = new ValidatorRepository(world, new ActionContext()); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + var guildAddress = guildRepository.GetJoinedGuild(masterAddress); + Assert.NotNull(guildAddress); + var guild = guildRepository.GetGuild(guildAddress.Value); + Assert.Equal(masterAddress, guild.GuildMasterAddress); + Assert.Equal(expectedTotalGG, guildDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, guildDelegatee.TotalShares); + Assert.Equal(expectedTotalGG, validatorDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, validatorDelegatee.TotalShares); + } + + private class StaticFixture : IMakeGuildFixture + { + public PrivateKey ValidatorKey { get; set; } = new PrivateKey(); + + public FungibleAssetValue ValidatorNCG { get; set; } = NCG * 100; + + public BigInteger SlashFactor { get; set; } + + public AgentAddress MasterAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue MasterNCG { get; set; } = GG * 100; + } + + private class RandomFixture : IMakeGuildFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorKey = GetRandomKey(_random); + ValidatorNCG = GetRandomNCG(_random); + SlashFactor = GetRandomSlashFactor(_random); + MasterAddress = GetRandomAgentAddress(_random); + MasterNCG = GetRandomNCG(_random); + } + + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } } diff --git a/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs b/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs index 3a707c2df4..1f836344d8 100644 --- a/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs @@ -1,15 +1,39 @@ namespace Lib9c.Tests.Action.Guild; using System; +using System.Collections.Generic; +using System.Numerics; using Lib9c.Tests.Util; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action.Guild; using Nekoyume.Model.Guild; +using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; using Xunit; public class MoveGuildTest : GuildTestBase { + private interface IMoveGuildFixture + { + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildInfo GuildInfo1 { get; } + + public GuildInfo GuildInfo2 { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + [Fact] public void Serialization() { @@ -24,23 +48,68 @@ public void Serialization() [Fact] public void Execute() + { + var fixture = new StaticFixture + { + GuildInfo1 = new GuildInfo + { + ValidatorNCG = NCG * 100, + SlashFactor = 0, + MasterNCG = NCG * 100, + }, + GuildInfo2 = new GuildInfo + { + ValidatorNCG = NCG * 100, + SlashFactor = 0, + MasterNCG = NCG * 100, + }, + AgentNCG = NCG * 100, + }; + ExecuteWithFixture(fixture); + } + + [Fact] + public void Execute_SlashedValidator() + { + var fixture = new StaticFixture + { + GuildInfo1 = new GuildInfo + { + ValidatorNCG = NCG * 100, + SlashFactor = 10, + MasterNCG = NCG * 100, + }, + GuildInfo2 = new GuildInfo + { + ValidatorNCG = NCG * 100, + SlashFactor = 10, + MasterNCG = NCG * 100, + }, + AgentNCG = NCG * 100, + }; + ExecuteWithFixture(fixture); + } + + [Fact] + public void Execute_ToGuildDelegatingToTombstonedValidator_Throw() { // Given var world = World; var validatorKey1 = new PrivateKey(); var validatorKey2 = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress1 = AddressUtil.CreateAgentAddress(); - var guildMasterAddress2 = AddressUtil.CreateAgentAddress(); + var masterAddress1 = AddressUtil.CreateAgentAddress(); + var masterAddress2 = AddressUtil.CreateAgentAddress(); var guildAddress1 = AddressUtil.CreateGuildAddress(); var guildAddress2 = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey1.PublicKey); - world = EnsureToCreateValidator(world, validatorKey2.PublicKey); - world = EnsureToPrepareGuildGold(world, guildMasterAddress1, GG * 100); - world = EnsureToMakeGuild(world, guildAddress1, guildMasterAddress1, validatorKey1.Address); - world = EnsureToMakeGuild(world, guildAddress2, guildMasterAddress2, validatorKey2.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); - world = EnsureToJoinGuild(world, guildAddress1, agentAddress, 1L); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey1, NCG * 100, height++); + world = EnsureToInitializeValidator(world, validatorKey2, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress1, masterAddress1, validatorKey1, height++); + world = EnsureToMakeGuild(world, guildAddress2, masterAddress2, validatorKey2, height++); + world = EnsureToInitializeAgent(world, agentAddress, NCG * 100, height++); + world = EnsureToJoinGuild(world, guildAddress1, agentAddress, height++); + world = EnsureToTombstoneValidator(world, validatorKey2, height++); // When var moveGuild = new MoveGuild(guildAddress2); @@ -50,58 +119,182 @@ public void Execute() Signer = agentAddress, BlockIndex = 2L, }; - world = moveGuild.Execute(actionContext); // Then - var guildRepository = new GuildRepository(world, actionContext); - var validatorRepository = new ValidatorRepository(world, actionContext); - var guildDelegatee1 = guildRepository.GetDelegatee(validatorKey1.Address); - var guildDelegatee2 = guildRepository.GetDelegatee(validatorKey2.Address); - var validatorDelegatee1 = validatorRepository.GetDelegatee(validatorKey1.Address); - var validatorDelegatee2 = validatorRepository.GetDelegatee(validatorKey2.Address); + var exception = Assert.Throws( + () => moveGuild.Execute(actionContext)); + Assert.Equal( + "The validator of the guild to move to has been tombstoned.", exception.Message); + } - var guildParticipant = guildRepository.GetGuildParticipant(agentAddress); + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + [InlineData(1893396102)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } - Assert.Equal(guildAddress2, guildParticipant.GuildAddress); - Assert.Equal(100 * SharePerGG, guildDelegatee1.TotalShares); - Assert.Equal(100 * SharePerGG, validatorDelegatee1.TotalShares); - Assert.Equal(100 * SharePerGG, guildDelegatee2.TotalShares); - Assert.Equal(100 * SharePerGG, validatorDelegatee2.TotalShares); + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); } - [Fact] - public void Execute_ToGuildDelegatingToTombstonedValidator_Throw() + private void ExecuteWithFixture(IMoveGuildFixture fixture) { // Given var world = World; - var validatorKey1 = new PrivateKey(); - var validatorKey2 = new PrivateKey(); - var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress1 = AddressUtil.CreateAgentAddress(); - var guildMasterAddress2 = AddressUtil.CreateAgentAddress(); - var guildAddress1 = AddressUtil.CreateGuildAddress(); - var guildAddress2 = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey1.PublicKey); - world = EnsureToCreateValidator(world, validatorKey2.PublicKey); - world = EnsureToMakeGuild(world, guildAddress1, guildMasterAddress1, validatorKey1.Address); - world = EnsureToMakeGuild(world, guildAddress2, guildMasterAddress2, validatorKey2.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); - world = EnsureToJoinGuild(world, guildAddress1, agentAddress, 1L); - world = EnsureToTombstoneValidator(world, validatorKey2.Address); + var validatorKey1 = fixture.GuildInfo1.ValidatorKey; + var validatorKey2 = fixture.GuildInfo2.ValidatorKey; + var validatorNCG1 = fixture.GuildInfo1.ValidatorNCG; + var validatorNCG2 = fixture.GuildInfo2.ValidatorNCG; + var validatorGG1 = NCGToGG(validatorNCG1); + var validatorGG2 = NCGToGG(validatorNCG2); + var agentAddress = fixture.AgentAddress; + var agentNCG = fixture.AgentNCG; + var agentGG = NCGToGG(agentNCG); + var masterAddress1 = fixture.GuildInfo1.MasterAddress; + var masterAddress2 = fixture.GuildInfo2.MasterAddress; + var masterNCG1 = fixture.GuildInfo1.MasterNCG; + var masterNCG2 = fixture.GuildInfo2.MasterNCG; + var masterGG1 = NCGToGG(masterNCG1); + var masterGG2 = NCGToGG(masterNCG2); + var guildAddress1 = fixture.GuildInfo1.GuildAddress; + var guildAddress2 = fixture.GuildInfo2.GuildAddress; + var height = 0L; + var slashFactor1 = fixture.GuildInfo1.SlashFactor; + var slashFactor2 = fixture.GuildInfo2.SlashFactor; + world = EnsureToInitializeValidator(world, validatorKey1, validatorNCG1, height++); + world = EnsureToInitializeValidator(world, validatorKey2, validatorNCG2, height++); + world = EnsureToMakeGuild(world, guildAddress1, masterAddress1, validatorKey1, height++); + world = EnsureToMakeGuild(world, guildAddress2, masterAddress2, validatorKey2, height++); + world = EnsureToInitializeAgent(world, masterAddress1, masterNCG1, height++); + world = EnsureToInitializeAgent(world, masterAddress2, masterNCG2, height++); + world = EnsureToInitializeAgent(world, agentAddress, agentNCG, height++); + world = EnsureToStake(world, masterAddress1, masterNCG1, height++); + world = EnsureToStake(world, masterAddress2, masterNCG2, height++); + world = EnsureToJoinGuild(world, guildAddress1, agentAddress, height++); + world = EnsureToStake(world, agentAddress, agentNCG, height++); + if (slashFactor1 > 0) + { + world = EnsureToSlashValidator(world, validatorKey1, slashFactor1, height++); + } + + if (slashFactor2 > 0) + { + world = EnsureToSlashValidator(world, validatorKey2, slashFactor2, height++); + } // When + var totalGG1 = validatorGG1 + masterGG1 + agentGG; + var totalGG2 = validatorGG2 + masterGG2; + var slashedGG1 = SlashFAV(slashFactor1, totalGG1); + var slashedGG2 = SlashFAV(slashFactor2, totalGG2); + var totalShare1 = totalGG1.RawValue; + var totalShare2 = totalGG2.RawValue; + var agentShare1 = totalShare1 * agentGG.RawValue / totalGG1.RawValue; + var expectedAgengGG1 = (slashedGG1 * agentShare1).DivRem(totalShare1).Quotient; + var agentShare2 = totalShare2 * expectedAgengGG1.RawValue / slashedGG2.RawValue; + var expectedTotalGG1 = slashedGG1 - expectedAgengGG1; + var expectedTotalGG2 = slashedGG2 + expectedAgengGG1; + var expectedTotalShares1 = totalShare1 - agentShare1; + var expectedTotalShares2 = totalShare2 + agentShare2; var moveGuild = new MoveGuild(guildAddress2); var actionContext = new ActionContext { PreviousState = world, Signer = agentAddress, - BlockIndex = 2L, + BlockIndex = height++, }; + world = moveGuild.Execute(actionContext); // Then - var exception = Assert.Throws( - () => moveGuild.Execute(actionContext)); - Assert.Equal( - "The validator of the guild to move to has been tombstoned.", exception.Message); + var guildRepository = new GuildRepository(world, actionContext); + var validatorRepository = new ValidatorRepository(world, actionContext); + var guildDelegatee1 = guildRepository.GetDelegatee(validatorKey1.Address); + var guildDelegatee2 = guildRepository.GetDelegatee(validatorKey2.Address); + var validatorDelegatee1 = validatorRepository.GetDelegatee(validatorKey1.Address); + var validatorDelegatee2 = validatorRepository.GetDelegatee(validatorKey2.Address); + + var guildParticipant = guildRepository.GetGuildParticipant(agentAddress); + + Assert.Equal(guildAddress2, guildParticipant.GuildAddress); + Assert.Equal(expectedTotalGG1, guildDelegatee1.TotalDelegated); + Assert.Equal(expectedTotalGG1, validatorDelegatee1.TotalDelegated); + Assert.Equal(expectedTotalShares1, guildDelegatee1.TotalShares); + Assert.Equal(expectedTotalShares1, validatorDelegatee1.TotalShares); + Assert.Equal(expectedTotalGG2, guildDelegatee2.TotalDelegated); + Assert.Equal(expectedTotalGG2, validatorDelegatee2.TotalDelegated); + Assert.Equal(expectedTotalShares2, guildDelegatee2.TotalShares); + Assert.Equal(expectedTotalShares2, validatorDelegatee2.TotalShares); + } + + private struct GuildInfo + { + public GuildInfo() + { + } + + public GuildInfo(Random random) + { + ValidatorKey = GetRandomKey(random); + ValidatorNCG = GetRandomNCG(random); + SlashFactor = GetRandomSlashFactor(random); + GuildAddress = GetRandomGuildAddress(random); + MasterAddress = GetRandomAgentAddress(random); + MasterNCG = GetRandomNCG(random); + } + + public PrivateKey ValidatorKey { get; set; } = new PrivateKey(); + + public FungibleAssetValue ValidatorNCG { get; set; } = NCG * 100; + + public BigInteger SlashFactor { get; set; } = 0; + + public GuildAddress GuildAddress { get; set; } = AddressUtil.CreateGuildAddress(); + + public AgentAddress MasterAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue MasterNCG { get; set; } = NCG * 100; + } + + private class StaticFixture : IMoveGuildFixture + { + public AgentAddress AgentAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue AgentNCG { get; set; } = NCG * 100; + + public GuildInfo GuildInfo1 { get; set; } + + public GuildInfo GuildInfo2 { get; set; } + } + + private class RandomFixture : IMoveGuildFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + AgentAddress = GetRandomAgentAddress(_random); + AgentNCG = GetRandomNCG(_random); + GuildInfo1 = new GuildInfo(_random); + GuildInfo2 = new GuildInfo(_random); + } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildInfo GuildInfo1 { get; } + + public GuildInfo GuildInfo2 { get; } } } diff --git a/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs b/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs index 4600712c6c..21bebcc2a0 100644 --- a/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs @@ -1,8 +1,11 @@ namespace Lib9c.Tests.Action.Guild; using System; +using System.Collections.Generic; +using System.Numerics; using Lib9c.Tests.Util; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action; using Nekoyume.Action.Guild; using Nekoyume.Model.Guild; @@ -12,6 +15,34 @@ namespace Lib9c.Tests.Action.Guild; public class QuitGuildTest : GuildTestBase { + private interface IQuitGuildFixture + { + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + [Fact] public void Serialization() { @@ -25,36 +56,27 @@ public void Serialization() [Fact] public void Execute() { - // Given - var world = World; - var validatorKey = new PrivateKey(); - var agentAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, agentAddress, GG * 100); - world = EnsureToJoinGuild(world, guildAddress, agentAddress, 1L); - - // When - var quitGuild = new QuitGuild(); - var actionContext = new ActionContext + var fixture = new StaticFixture { - PreviousState = world, - Signer = agentAddress, - BlockIndex = 2L, + ValidatorNCG = NCG * 100, + SlashFactor = 0, + AgentNCG = NCG * 100, + MasterNCG = NCG * 100, }; - world = quitGuild.Execute(actionContext); + ExecuteWithFixture(fixture); + } - // Then - var guildRepository = new GuildRepository(world, actionContext); - var validatorRepository = new ValidatorRepository(world, actionContext); - var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); - var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); - Assert.Throws( - () => guildRepository.GetGuildParticipant(agentAddress)); - Assert.Equal(0, guildDelegatee.TotalShares); - Assert.Equal(0, validatorDelegatee.TotalShares); + [Fact] + public void Execute_SlashedValidator() + { + var fixture = new StaticFixture + { + ValidatorNCG = NCG * 100, + SlashFactor = 10, + AgentNCG = NCG * 100, + MasterNCG = NCG * 100, + }; + ExecuteWithFixture(fixture); } [Fact] @@ -65,9 +87,10 @@ public void Execute_SignerDoesNotHaveGuild_Throw() var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var masterAddress = AddressUtil.CreateAgentAddress(); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); // When var quitGuild = new QuitGuild(); @@ -75,7 +98,7 @@ public void Execute_SignerDoesNotHaveGuild_Throw() { PreviousState = world, Signer = agentAddress, - BlockIndex = 2L, + BlockIndex = height, }; // Then @@ -92,7 +115,7 @@ public void Execute_FromUnknownGuild_Throw() var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); world = EnsureToSetGuildParticipant(world, agentAddress, guildAddress); // When @@ -118,19 +141,20 @@ public void Execute_SignerIsGuildMaster_Throw() var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToPrepareGuildGold(world, guildMasterAddress, GG * 100); - world = EnsureToJoinGuild(world, guildAddress, agentAddress, 1L); + var masterAddress = AddressUtil.CreateAgentAddress(); + var height = 1L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToInitializeAgent(world, masterAddress, NCG * 100, height++); + world = EnsureToJoinGuild(world, guildAddress, agentAddress, height++); // When var quitGuild = new QuitGuild(); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, - BlockIndex = 2L, + Signer = masterAddress, + BlockIndex = height, }; // Then @@ -140,4 +164,138 @@ public void Execute_SignerIsGuildMaster_Throw() expected: "The signer is a guild master. Guild master cannot quit the guild.", actual: exception.Message); } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + [InlineData(1746916991)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private void ExecuteWithFixture(IQuitGuildFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorKey; + var validatorNCG = fixture.ValidatorNCG; + var validatorGG = NCGToGG(validatorNCG); + var slashFactor = fixture.SlashFactor; + var agentAddress = fixture.AgentAddress; + var agentNCG = fixture.AgentNCG; + var agentGG = NCGToGG(agentNCG); + var masterAddress = fixture.MasterAddress; + var masterNCG = fixture.MasterNCG; + var masterGG = NCGToGG(masterNCG); + var guildAddress = fixture.GuildAddress; + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, validatorNCG, height++); + world = EnsureToInitializeAgent(world, masterAddress, masterNCG, height++); + world = EnsureToInitializeAgent(world, agentAddress, agentNCG, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToStake(world, masterAddress, masterNCG, height++); + world = EnsureToStake(world, agentAddress, agentNCG, height++); + world = EnsureToJoinGuild(world, guildAddress, agentAddress, height++); + if (slashFactor > 0) + { + world = EnsureToSlashValidator(world, validatorKey, slashFactor, height++); + } + + // When + var totalGG = validatorGG + masterGG + agentGG; + var slashedGG = SlashFAV(slashFactor, totalGG); + var totalShare = totalGG.RawValue; + var agentShare = totalShare * agentGG.RawValue / totalGG.RawValue; + var expectedAgengGG = (slashedGG * agentShare).DivRem(totalShare).Quotient; + var expectedTotalGG = slashedGG - expectedAgengGG; + var expectedTotalShares = totalShare - agentShare; + var quitGuild = new QuitGuild(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = height++, + }; + world = quitGuild.Execute(actionContext); + world = EnsureToReleaseUnbonding( + world, agentAddress, height + ValidatorDelegatee.ValidatorUnbondingPeriod); + + // Then + var guildRepository = new GuildRepository(world, actionContext); + var validatorRepository = new ValidatorRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + + Assert.Throws( + () => guildRepository.GetGuildParticipant(agentAddress)); + Assert.Equal(expectedTotalGG, guildDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, guildDelegatee.TotalShares); + Assert.Equal(expectedTotalGG, validatorDelegatee.TotalDelegated); + Assert.Equal(expectedTotalShares, validatorDelegatee.TotalShares); + } + + private class StaticFixture : IQuitGuildFixture + { + public PrivateKey ValidatorKey { get; set; } = new PrivateKey(); + + public FungibleAssetValue ValidatorNCG { get; set; } = NCG * 100; + + public BigInteger SlashFactor { get; set; } = 0; + + public AgentAddress AgentAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue AgentNCG { get; set; } = NCG * 100; + + public GuildAddress GuildAddress { get; set; } = AddressUtil.CreateGuildAddress(); + + public AgentAddress MasterAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue MasterNCG { get; set; } = NCG * 100; + } + + private class RandomFixture : IQuitGuildFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorKey = GetRandomKey(_random); + ValidatorNCG = GetRandomNCG(_random); + SlashFactor = GetRandomSlashFactor(_random); + AgentAddress = GetRandomAgentAddress(_random); + AgentNCG = GetRandomNCG(_random); + GuildAddress = GetRandomGuildAddress(_random); + MasterAddress = GetRandomAgentAddress(_random); + MasterNCG = GetRandomNCG(_random); + } + + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public AgentAddress AgentAddress { get; } + + public FungibleAssetValue AgentNCG { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } } diff --git a/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs b/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs index 4184104ba1..2313eb920f 100644 --- a/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs @@ -1,17 +1,45 @@ namespace Lib9c.Tests.Action.Guild; using System; +using System.Collections.Generic; +using System.Numerics; using Lib9c.Tests.Util; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action; using Nekoyume.Action.Guild; using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; using Xunit; public class RemoveGuildTest : GuildTestBase { + private interface IRemoveGuildFixture + { + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + [Fact] public void Serialization() { @@ -25,33 +53,27 @@ public void Serialization() [Fact] public void Execute() { - // Given - var world = World; - var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - - // When - var removeGuild = new RemoveGuild(); - var actionContext = new ActionContext + var fixture = new StaticFixture { - PreviousState = world, - Signer = guildMasterAddress, + ValidatorNCG = NCG * 100, + SlashFactor = 0, + MasterNCG = NCG * 100, }; - world = removeGuild.Execute(actionContext); - // Then - var guildRepository = new GuildRepository(world, actionContext); - var validatorRepository = new ValidatorRepository(world, actionContext); - var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); - var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + ExecuteWithFixture(fixture); + } - Assert.Throws(() => guildRepository.GetGuild(guildAddress)); - Assert.Equal(0, guildDelegatee.TotalShares); - Assert.Equal(0, validatorDelegatee.TotalShares); + [Fact] + public void Execute_SlashedValidator() + { + var fixture = new StaticFixture + { + ValidatorNCG = NCG * 100, + SlashFactor = 10, + MasterNCG = NCG * 100, + }; + + ExecuteWithFixture(fixture); } [Fact] @@ -79,7 +101,7 @@ public void Execute_UnknownGuild_Throw() } [Fact] - public void Execute_ByGuildMember_Throw() + public void Execute_ByMember_Throw() { // Given var world = World; @@ -87,9 +109,10 @@ public void Execute_ByGuildMember_Throw() var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, height++); // When var removeGuild = new RemoveGuild(); @@ -97,6 +120,7 @@ public void Execute_ByGuildMember_Throw() { PreviousState = world, Signer = guildMemberAddress, + BlockIndex = height, }; // Then @@ -114,10 +138,11 @@ public void Execute_GuildHasMember_Throw() var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildParticipantAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToPrepareGuildGold(world, guildMasterAddress, GG * 100); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildParticipantAddress, 1L); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToInitializeAgent(world, guildMasterAddress, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, guildParticipantAddress, height++); // When var removeGuild = new RemoveGuild(); @@ -125,6 +150,7 @@ public void Execute_GuildHasMember_Throw() { PreviousState = world, Signer = guildMasterAddress, + BlockIndex = height, }; // Then @@ -139,18 +165,21 @@ public void Execute_GuildHasBond_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToPrepareGuildGold(world, guildMasterAddress, GG * 100); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToInitializeAgent(world, masterAddress, NCG * 100, height++); + world = EnsureToStake(world, masterAddress, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); // When var removeGuild = new RemoveGuild(); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, + BlockIndex = height, }; // Then @@ -160,16 +189,17 @@ public void Execute_GuildHasBond_Throw() } [Fact] - public void Execute_ByNonGuildMember_Throw() + public void Execute_ByNonMember_Throw() { // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var otherAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); // When var removeGuild = new RemoveGuild(); @@ -177,6 +207,7 @@ public void Execute_ByNonGuildMember_Throw() { PreviousState = world, Signer = otherAddress, + BlockIndex = height, }; // Then @@ -191,20 +222,22 @@ public void Execute_ResetBannedAddresses() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var bannedAddress = AddressUtil.CreateAgentAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, bannedAddress, 1L); - world = EnsureToBanGuildMember(world, guildMasterAddress, bannedAddress); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, bannedAddress, height++); + world = EnsureToBanMember(world, masterAddress, bannedAddress, height++); // When var removeGuild = new RemoveGuild(); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, + BlockIndex = height, }; world = removeGuild.Execute(actionContext); @@ -212,4 +245,120 @@ public void Execute_ResetBannedAddresses() var repository = new GuildRepository(world, actionContext); Assert.False(repository.IsBanned(guildAddress, bannedAddress)); } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private void ExecuteWithFixture(IRemoveGuildFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorKey; + var validatorNCG = fixture.ValidatorNCG; + var validatorAmount = validatorNCG.MajorUnit; + var validatorGG = NCGToGG(validatorNCG); + var masterAddress = fixture.MasterAddress; + var masterNCG = fixture.MasterNCG; + var masterAmount = masterNCG.MajorUnit; + var guildAddress = fixture.GuildAddress; + var height = 0L; + var slashFactor = fixture.SlashFactor; + world = EnsureToInitializeValidator(world, validatorKey, validatorNCG, height++); + if (slashFactor > 0) + { + world = EnsureToSlashValidator(world, validatorKey, slashFactor, height++); + } + + world = EnsureToInitializeAgent(world, masterAddress, masterNCG, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToStake(world, masterAddress, masterNCG, height++); + world = EnsureToStake(world, masterAddress, NCG * 0, height++); + + // When + var totalGG = validatorGG; + var slashedGG = SlashFAV(slashFactor, totalGG); + var totalShare = totalGG.RawValue; + var expectedTotalGG = slashedGG; + var expectedTotalShares = totalGG.RawValue; + var removeGuild = new RemoveGuild(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = masterAddress, + BlockIndex = height, + }; + world = removeGuild.Execute(actionContext); + + // Then + var guildRepository = new GuildRepository(world, actionContext); + var validatorRepository = new ValidatorRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + var validatorDelegatee = validatorRepository.GetDelegatee(validatorKey.Address); + var comparer = new FungibleAssetValueEqualityComparer(GGEpsilon); + + Assert.Throws(() => guildRepository.GetGuild(guildAddress)); + Assert.Equal(expectedTotalGG, guildDelegatee.TotalDelegated, comparer); + Assert.Equal(expectedTotalGG, validatorDelegatee.TotalDelegated, comparer); + Assert.Equal(expectedTotalShares, guildDelegatee.TotalShares); + Assert.Equal(expectedTotalShares, validatorDelegatee.TotalShares); + } + + private class StaticFixture : IRemoveGuildFixture + { + public PrivateKey ValidatorKey { get; set; } = new PrivateKey(); + + public FungibleAssetValue ValidatorNCG { get; set; } = GG * 100; + + public BigInteger SlashFactor { get; set; } + + public GuildAddress GuildAddress { get; set; } = AddressUtil.CreateGuildAddress(); + + public AgentAddress MasterAddress { get; set; } = AddressUtil.CreateAgentAddress(); + + public FungibleAssetValue MasterNCG { get; set; } = NCG * 100; + } + + private class RandomFixture : IRemoveGuildFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorKey = GetRandomKey(_random); + ValidatorNCG = GetRandomNCG(_random); + SlashFactor = GetRandomSlashFactor(_random); + GuildAddress = GetRandomGuildAddress(_random); + MasterAddress = GetRandomAgentAddress(_random); + MasterNCG = GetRandomNCG(_random); + } + + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorNCG { get; } + + public BigInteger SlashFactor { get; } + + public GuildAddress GuildAddress { get; } + + public AgentAddress MasterAddress { get; } + + public FungibleAssetValue MasterNCG { get; } + } } diff --git a/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs b/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs index 932d77f00d..fcc56b54c5 100644 --- a/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs +++ b/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs @@ -14,13 +14,13 @@ public class UnbanGuildMemberTest : GuildTestBase [Fact] public void Serialization() { - var guildMemberAddress = new PrivateKey().Address; - var action = new UnbanGuildMember(guildMemberAddress); + var memberAddress = new PrivateKey().Address; + var action = new UnbanGuildMember(memberAddress); var plainValue = action.PlainValue; var deserialized = new UnbanGuildMember(); deserialized.LoadPlainValue(plainValue); - Assert.Equal(guildMemberAddress, deserialized.Target); + Assert.Equal(memberAddress, deserialized.Target); } [Fact] @@ -29,20 +29,21 @@ public void Execute() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); - world = EnsureToBanGuildMember(world, guildMasterAddress, targetGuildMemberAddress); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, height++); + world = EnsureToBanMember(world, masterAddress, targetGuildMemberAddress, height++); // When var unbanGuildMember = new UnbanGuildMember(targetGuildMemberAddress); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, }; world = unbanGuildMember.Execute(actionContext); @@ -58,16 +59,16 @@ public void Execute_SignerDoesNotHaveGuild_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildMemberAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); + var memberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); // When - var unbanGuildMember = new UnbanGuildMember(guildMemberAddress); + var unbanGuildMember = new UnbanGuildMember(memberAddress); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, }; // Then @@ -82,20 +83,21 @@ public void Execute_UnknownGuild_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildMemberAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); + var memberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var unknownGuildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToSetGuildParticipant(world, guildMasterAddress, unknownGuildAddress); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToSetGuildParticipant(world, masterAddress, unknownGuildAddress); // When - var unbanGuildMember = new UnbanGuildMember(guildMemberAddress); + var unbanGuildMember = new UnbanGuildMember(memberAddress); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, }; // Then @@ -110,19 +112,21 @@ public void Execute_SignerIsNotGuildMaster_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildMemberAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); + var memberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, memberAddress, height++); // When - var unbanGuildMember = new UnbanGuildMember(guildMemberAddress); + var unbanGuildMember = new UnbanGuildMember(memberAddress); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMemberAddress, + Signer = memberAddress, + BlockIndex = height, }; // Then @@ -137,17 +141,19 @@ public void Execute_TargetIsNotBanned_Throw() // Given var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + var height = 0L; + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); // When - var unbanGuildMember = new UnbanGuildMember(guildMasterAddress); + var unbanGuildMember = new UnbanGuildMember(masterAddress); var actionContext = new ActionContext { PreviousState = world, - Signer = guildMasterAddress, + Signer = masterAddress, + BlockIndex = height, }; // Then @@ -161,17 +167,18 @@ public void Unban_By_GuildMember() { var world = World; var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildMemberAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); + var memberAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); + var height = 0L; var action = new UnbanGuildMember(targetGuildMemberAddress); - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); - world = EnsureToBanGuildMember(world, guildMasterAddress, targetGuildMemberAddress); + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, memberAddress, height++); + world = EnsureToBanMember(world, masterAddress, targetGuildMemberAddress, height++); var repository = new GuildRepository(world, new ActionContext()); @@ -179,7 +186,7 @@ public void Unban_By_GuildMember() Assert.Throws(() => action.Execute(new ActionContext { PreviousState = repository.World, - Signer = guildMemberAddress, + Signer = memberAddress, })); // GuildMember tries to ban itself. @@ -194,17 +201,18 @@ public void Unban_By_GuildMember() public void Unban_By_GuildMaster() { var validatorKey = new PrivateKey(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var masterAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); + var height = 0L; var action = new UnbanGuildMember(targetGuildMemberAddress); IWorld world = World; - world = EnsureToCreateValidator(world, validatorKey.PublicKey); - world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); - world = EnsureToBanGuildMember(world, guildMasterAddress, targetGuildMemberAddress); + world = EnsureToInitializeValidator(world, validatorKey, NCG * 100, height++); + world = EnsureToMakeGuild(world, guildAddress, masterAddress, validatorKey, height++); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, height++); + world = EnsureToBanMember(world, masterAddress, targetGuildMemberAddress, height++); var repository = new GuildRepository(world, new ActionContext()); @@ -214,7 +222,8 @@ public void Unban_By_GuildMaster() world = action.Execute(new ActionContext { PreviousState = repository.World, - Signer = guildMasterAddress, + Signer = masterAddress, + BlockIndex = height, }); repository.UpdateWorld(world); diff --git a/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs b/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs index ae1538eb19..b281a5661d 100644 --- a/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs +++ b/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs @@ -113,16 +113,20 @@ public void Test() state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); state = DelegationUtil.MakeGuild(state, _agentAddr, validatorKey.Address, 0L); + var withdrawHeight = stake2BlockIndex + LegacyStakeState.LockupInterval + 1; // Withdraw stake via stake3. - state = Stake3(state, _agentAddr, 0, stake2BlockIndex + LegacyStakeState.LockupInterval + 1); + state = Stake3(state, _agentAddr, _avatarAddr, 0, withdrawHeight); - state = DelegationUtil.EnsureStakeReleased(state, stake2BlockIndex + ValidatorDelegatee.ValidatorUnbondingPeriod); + var unbondedHeight = withdrawHeight + ValidatorDelegatee.ValidatorUnbondingPeriod; + state = DelegationUtil.EnsureUnbondedClaimed( + state, _agentAddr, unbondedHeight); // Stake 50 NCG via stake3 before patching. - const long firstStake3BlockIndex = stake2BlockIndex + LegacyStakeState.LockupInterval + 1; + var firstStake3BlockIndex = unbondedHeight; state = Stake3( state, _agentAddr, + _avatarAddr, stakedAmount, firstStake3BlockIndex); @@ -146,6 +150,7 @@ public void Test() state = Stake3( state, _agentAddr, + _avatarAddr, stakedAmount, firstStake3BlockIndex + 1); @@ -197,10 +202,11 @@ private static IWorld Stake2( private static IWorld Stake3( IWorld state, Address agentAddr, + Address avatarAddr, long stakingAmount, long blockIndex) { - var stake3 = new Stake(stakingAmount); + var stake3 = new Stake(stakingAmount, avatarAddr); return stake3.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/StakeTest.cs b/.Lib9c.Tests/Action/StakeTest.cs index 58f8c4898c..4fc62ec98a 100644 --- a/.Lib9c.Tests/Action/StakeTest.cs +++ b/.Lib9c.Tests/Action/StakeTest.cs @@ -30,6 +30,7 @@ public class StakeTest private readonly Currency _ncg; private readonly PublicKey _agentPublicKey = new PrivateKey().PublicKey; private readonly Address _agentAddr; + private readonly Address _avatarAddr; private readonly StakePolicySheet _stakePolicySheet; public StakeTest(ITestOutputHelper outputHelper) @@ -65,7 +66,7 @@ public StakeTest(ITestOutputHelper outputHelper) ( _, _agentAddr, - _, + _avatarAddr, _initialState ) = InitializeUtil.InitializeStates( sheetsOverride: sheetsOverride, @@ -80,13 +81,15 @@ public StakeTest(ITestOutputHelper outputHelper) [InlineData(long.MaxValue, true)] public void Constructor(long amount, bool success) { + var avatarAddress = new PrivateKey().Address; + if (success) { - var stake = new Stake(amount); + var stake = new Stake(amount, _avatarAddr); } else { - Assert.Throws(() => new Stake(amount)); + Assert.Throws(() => new Stake(amount, _avatarAddr)); } } @@ -95,7 +98,7 @@ public void Constructor(long amount, bool success) [InlineData(long.MaxValue)] public void Serialization(long amount) { - var action = new Stake(amount); + var action = new Stake(amount, _avatarAddr); var ser = action.PlainValue; var de = new Stake(); de.LoadPlainValue(ser); @@ -236,11 +239,7 @@ public void Execute_Throw_StateNullException_Via_0_Amount() [Theory] // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. [InlineData(0, 50, LegacyStakeState.RewardInterval)] - [InlineData( - long.MaxValue - LegacyStakeState.RewardInterval, - long.MaxValue, - long.MaxValue)] - public void Execute_Throw_StakeExistingClaimableException_With_StakeState( + public void Execute_Success_When_Claimable_With_StakeState( long previousStartedBlockIndex, long previousAmount, long blockIndex) @@ -256,24 +255,22 @@ public void Execute_Throw_StakeExistingClaimableException_With_StakeState( stakeStateAddr, _ncg * previousAmount) .SetLegacyState(stakeStateAddr, stakeState.Serialize()); - Assert.Throws( - () => - Execute( - blockIndex, - previousState, - new TestRandom(), - _agentAddr, - previousAmount)); + + var nextState = Execute( + blockIndex, + previousState, + new TestRandom(), + _agentAddr, + previousAmount); + + Assert.True(nextState.TryGetStakeState(_agentAddr, out var newStakeState)); + Assert.Equal(blockIndex, newStakeState.ClaimedBlockIndex); } [Theory] // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. // NOTE: RewardInterval of StakePolicySheetFixtures.V2 is 50,400. [InlineData(0, 50, 50400)] - [InlineData( - long.MaxValue - 50400, - long.MaxValue, - long.MaxValue)] public void Execute_Throw_StakeExistingClaimableException_With_StakeStateV2( long previousStartedBlockIndex, long previousAmount, @@ -289,14 +286,15 @@ public void Execute_Throw_StakeExistingClaimableException_With_StakeStateV2( stakeStateAddr, _ncg * previousAmount) .SetLegacyState(stakeStateAddr, stakeStateV2.Serialize()); - Assert.Throws( - () => - Execute( - blockIndex, - previousState, - new TestRandom(), - _agentAddr, - previousAmount)); + var nextState = Execute( + blockIndex, + previousState, + new TestRandom(), + _agentAddr, + previousAmount); + + Assert.True(nextState.TryGetStakeState(_agentAddr, out var newStakeState)); + Assert.Equal(blockIndex, newStakeState.ClaimedBlockIndex); } [Theory] @@ -322,8 +320,8 @@ public void Execute_Success_When_Staking_State_Null(long amount) _agentAddr, amount); - world = DelegationUtil.EnsureStakeReleased( - world, height + ValidatorDelegatee.ValidatorUnbondingPeriod); + world = DelegationUtil.EnsureUnbondedClaimed( + world, _agentAddr, height + ValidatorDelegatee.ValidatorUnbondingPeriod); } [Theory] @@ -394,8 +392,8 @@ public void Execute_Success_When_Exist_StakeStateV3( Assert.Equal(3, nextStakeState.StateVersion); } - world = DelegationUtil.EnsureStakeReleased( - nextState, height + LegacyStakeState.LockupInterval); + world = DelegationUtil.EnsureUnbondedClaimed( + nextState, _agentAddr, height + interval + ValidatorDelegatee.ValidatorUnbondingPeriod); var expectedBalance = _ncg * Math.Max(0, previousAmount - amount); var actualBalance = world.GetBalance(_agentAddr, _ncg); @@ -461,16 +459,10 @@ public void Execute_Success_When_Exist_StakeStateV3_Without_Guild( 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); + var actualBalance = nextState.GetBalance(_agentAddr, _ncg); + var stakeBalance = nextState.GetBalance(stakeStateAddr, Currencies.GuildGold); Assert.Equal(expectedBalance, actualBalance); - Assert.Equal(Currencies.GuildGold * 0, nonValidatorDelegateeBalance); Assert.Equal(Currencies.GuildGold * amount, stakeBalance); } @@ -535,8 +527,8 @@ public void Execute_Success_When_Validator_Tries_To_Increase_Amount_Without_Clai Assert.Equal(3, nextStakeState.StateVersion); } - world = DelegationUtil.EnsureStakeReleased( - nextState, height + LegacyStakeState.LockupInterval); + world = DelegationUtil.EnsureUnbondedClaimed( + nextState, _agentAddr, height + interval + ValidatorDelegatee.ValidatorUnbondingPeriod); var expectedBalance = _ncg * Math.Max(0, previousAmount - amount); var actualBalance = world.GetBalance(_agentAddr, _ncg); @@ -548,6 +540,32 @@ public void Execute_Success_When_Validator_Tries_To_Increase_Amount_Without_Clai Assert.Equal(Currencies.GuildGold * amount, stakeBalance); } + [Fact] + public void Execute_Success_When_Validator_Slashed() + { + var world = _initialState; + var height = 0L; + var validatorKey = new PrivateKey(); + var validatorAddress = validatorKey.PublicKey.Address; + var guildMasterKey = new PrivateKey(); + var guildMasterAddress = new GuildAddress(guildMasterKey.Address); + world = DelegationUtil.EnsureValidatorPromotionReady( + world, validatorKey.PublicKey, height++); + world = DelegationUtil.MakeGuild( + world, guildMasterAddress, validatorAddress, height++, out var guildAddress); + world = world.MintAsset(new ActionContext { }, _agentAddr, _ncg * 100); + world = DelegationUtil.Stake(world, _agentAddr, _avatarAddr, 100, height++); + world = DelegationUtil.JoinGuild(world, _agentAddr, guildAddress, height++); + world = DelegationUtil.SlashValidator(world, validatorAddress, 10, height++); + + Assert.True(world.TryGetStakeState(_agentAddr, out var stakeState)); + height += stakeState.CancellableBlockIndex; + world = DelegationUtil.Stake(world, _agentAddr, _avatarAddr, 0, height++); + + var actualNCG = world.GetBalance(_agentAddr, _ncg); + Assert.Equal(_ncg * 90, actualNCG); + } + private IWorld Execute( long blockIndex, IWorld previousState, @@ -555,7 +573,7 @@ private IWorld Execute( Address signer, long amount) { - var action = new Stake(amount); + var action = new Stake(amount, _avatarAddr); var nextState = action.Execute( new ActionContext { diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs deleted file mode 100644 index 5b961f5790..0000000000 --- a/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -#nullable enable -namespace Lib9c.Tests.Action.ValidatorDelegation; - -using System.Numerics; -using Libplanet.Crypto; -using Nekoyume.Action.ValidatorDelegation; -using Nekoyume.Model.Guild; -using Nekoyume.ValidatorDelegation; -using Xunit; - -public class ReleaseValidatorUnbondingsTest : ValidatorDelegationTestBase -{ - [Fact] - public void Serialization() - { - var action = new ReleaseValidatorUnbondings(); - var plainValue = action.PlainValue; - - var deserialized = new ReleaseValidatorUnbondings(); - deserialized.LoadPlainValue(plainValue); - } - - [Fact] - public void Execute() - { - // Given - var world = World; - var validatorKey = new PrivateKey(); - var validatorGold = DelegationCurrency * 50; - var validatorBalance = DelegationCurrency * 100; - var share = new BigInteger(10); - var height = 1L; - var actionContext = new ActionContext { }; - - world = EnsureToMintAsset(world, validatorKey, validatorBalance, height++); - world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); - world = EnsureUnbondingValidator(world, validatorKey.Address, share, height); - - // When - var expectedRepository = new GuildRepository(world, actionContext); - var expectedDelegatee = expectedRepository.GetDelegatee(validatorKey.Address); - var expectedUnbondingSet = expectedRepository.GetUnbondingSet(); - var expectedReleaseCount = expectedUnbondingSet.UnbondingRefs.Count; - var expectedDepositGold = expectedDelegatee.FAVFromShare(share); - var expectedBalance = GetBalance(world, validatorKey.Address) + expectedDepositGold; - - var releaseValidatorUnbondings = new ReleaseValidatorUnbondings(validatorKey.Address); - actionContext = new ActionContext - { - PreviousState = world, - Signer = validatorKey.Address, - BlockIndex = height + ValidatorDelegatee.ValidatorUnbondingPeriod, - }; - world = releaseValidatorUnbondings.Execute(actionContext); - - // Then - var actualRepository = new ValidatorRepository(world, actionContext); - var actualBalance = GetBalance(world, validatorKey.Address); - var actualUnbondingSet = actualRepository.GetUnbondingSet(); - var actualReleaseCount = actualUnbondingSet.UnbondingRefs.Count; - - Assert.Equal(expectedBalance, actualBalance); - Assert.NotEqual(expectedUnbondingSet.IsEmpty, actualUnbondingSet.IsEmpty); - Assert.True(actualUnbondingSet.IsEmpty); - Assert.Equal(expectedReleaseCount - 1, actualReleaseCount); - } - - [Fact(Skip = "Skip due to zero unbonding period before migration")] - public void Execute_ThereIsNoUnbonding_AtEarlyHeight() - { - // Given - var world = World; - var validatorKey = new PrivateKey(); - var height = 1L; - var actionContext = new ActionContext { }; - var share = new BigInteger(10); - - world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); - world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 50, height++); - world = EnsureUnbondingValidator(world, validatorKey.Address, share, height); - - // When - var expectedRepository = new GuildRepository(world, actionContext); - var expectedUnbondingSet = expectedRepository.GetUnbondingSet(); - var expectedReleaseCount = expectedUnbondingSet.UnbondingRefs.Count; - - var releaseValidatorUnbondings = new ReleaseValidatorUnbondings(validatorKey.Address); - actionContext = new ActionContext - { - PreviousState = world, - Signer = validatorKey.Address, - BlockIndex = height, - }; - world = releaseValidatorUnbondings.Execute(actionContext); - - // Then - var actualRepository = new GuildRepository(world, actionContext); - var actualUnbondingSet = actualRepository.GetUnbondingSet(); - var actualReleaseCount = actualUnbondingSet.UnbondingRefs.Count; - - Assert.Equal(expectedUnbondingSet.IsEmpty, actualUnbondingSet.IsEmpty); - Assert.False(actualUnbondingSet.IsEmpty); - Assert.Equal(expectedReleaseCount, actualReleaseCount); - } -} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs index 437ac8016c..5c46ef130a 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs @@ -13,6 +13,7 @@ namespace Lib9c.Tests.Action.ValidatorDelegation; using Libplanet.Types.Evidence; using Nekoyume.Action; using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.Guild; using Nekoyume.ValidatorDelegation; using Xunit; @@ -102,6 +103,13 @@ public void Execute() Assert.True(actualDelegatee.Jailed); Assert.Equal(long.MaxValue, actualDelegatee.JailedUntil); Assert.True(actualDelegatee.Tombstoned); + + // guild + var guildRepository = new GuildRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + Assert.True(guildDelegatee.Jailed); + Assert.Equal(long.MaxValue, guildDelegatee.JailedUntil); + Assert.True(guildDelegatee.Tombstoned); } [Fact] @@ -168,6 +176,12 @@ public void Execute_ByAbstain() var delegatee = repository.GetDelegatee(validatorKey.Address); Assert.True(delegatee.Jailed); Assert.False(delegatee.Tombstoned); + + // guild + var guildRepository = new GuildRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + Assert.True(guildDelegatee.Jailed); + Assert.False(guildDelegatee.Tombstoned); } [Fact] @@ -206,9 +220,13 @@ public void Execute_ToJailedValidator_ThenNothingHappens() // Then var actualRepository = new ValidatorRepository(world, actionContext); var actualDelegatee = actualRepository.GetDelegatee(validatorKey.Address); - var actualJailed = actualDelegatee.Jailed; - Assert.Equal(expectedJailed, actualJailed); + Assert.Equal(expectedJailed, actualDelegatee.Jailed); + + // guild + var guildRepository = new GuildRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + Assert.Equal(expectedJailed, guildDelegatee.Jailed); } [Fact] @@ -250,10 +268,16 @@ public void Execute_ByAbstain_ToJailedValidator_ThenNothingHappens() // Then var actualRepisitory = new ValidatorRepository(world, actionContext); var actualDelegatee = actualRepisitory.GetDelegatee(validatorKey.Address); - var actualTotalDelegated = actualDelegatee.TotalDelegated; Assert.True(actualDelegatee.Jailed); Assert.False(actualDelegatee.Tombstoned); - Assert.Equal(expectedTotalDelegated, actualTotalDelegated); + Assert.Equal(expectedTotalDelegated, actualDelegatee.TotalDelegated); + + // guild + var guildRepository = new GuildRepository(world, actionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorKey.Address); + Assert.True(guildDelegatee.Jailed); + Assert.False(guildDelegatee.Tombstoned); + Assert.Equal(expectedTotalDelegated, guildDelegatee.TotalDelegated); } } diff --git a/.Lib9c.Tests/Delegation/DelegationFixture.cs b/.Lib9c.Tests/Delegation/DelegationFixture.cs index ceab140717..d662118758 100644 --- a/.Lib9c.Tests/Delegation/DelegationFixture.cs +++ b/.Lib9c.Tests/Delegation/DelegationFixture.cs @@ -69,28 +69,5 @@ public DelegationFixture() public DummyDelegatee DummyDelegatee1 { get; } public DummyDelegator DummyDelegator1 { get; } - - public static FungibleAssetValue[] TotalRewardsOfRecords(IDelegatee delegatee, IDelegationRepository repo) - { - var rewards = delegatee.RewardCurrencies.Select(r => r * 0).ToArray(); - var record = repo.GetCurrentLumpSumRewardsRecord(delegatee); - - while (true) - { - foreach (var reward in rewards.Select((v, i) => (v, i))) - { - rewards[reward.i] += repo.World.GetBalance(record.Address, reward.v.Currency); - } - - if (record.LastStartHeight is null) - { - break; - } - - record = repo.GetLumpSumRewardsRecord(delegatee, record.LastStartHeight.Value); - } - - return rewards; - } } } diff --git a/.Lib9c.Tests/Delegation/DelegatorTest.cs b/.Lib9c.Tests/Delegation/DelegatorTest.cs index 65bc9df9df..eb0ab180d8 100644 --- a/.Lib9c.Tests/Delegation/DelegatorTest.cs +++ b/.Lib9c.Tests/Delegation/DelegatorTest.cs @@ -104,14 +104,12 @@ public void Undelegate() var delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); var share1 = repo.GetBond(delegatee, delegator.Address).Share; var unbondLockIn = repo.GetUnbondLockIn(delegatee, delegator.Address); - var unbondingSet = repo.GetUnbondingSet(); Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); Assert.Equal(delegatingFAV, delegateeBalance); Assert.Equal(initialShare - undelegatingShare, share1); Assert.Equal(delegatingFAV - undelegatingFAV, delegatee.TotalDelegated); Assert.Equal(initialShare - undelegatingShare, delegatee.TotalShares); Assert.Equal(delegatee.Address, Assert.Single(delegator.Delegatees)); - Assert.Equal(unbondLockIn.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); var entriesByExpireHeight = Assert.Single(unbondLockIn.Entries); Assert.Equal(10L + delegatee.UnbondingPeriod, entriesByExpireHeight.Key); var entry = Assert.Single(entriesByExpireHeight.Value); @@ -127,14 +125,12 @@ public void Undelegate() delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); var share2 = repo.GetBond(delegatee, delegator.Address).Share; unbondLockIn = repo.GetUnbondLockIn(delegatee, delegator.Address); - unbondingSet = repo.GetUnbondingSet(); Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); Assert.Equal(delegatingFAV, delegateeBalance); Assert.Equal(share1 - undelegatingShare, share2); Assert.Equal(delegatee.DelegationCurrency * 0, delegatee.TotalDelegated); Assert.Equal(System.Numerics.BigInteger.Zero, delegatee.TotalShares); Assert.Empty(delegator.Delegatees); - Assert.Equal(unbondLockIn.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); Assert.Equal(2, unbondLockIn.Entries.Count); unbondLockIn = unbondLockIn.Release(10L + delegatee.UnbondingPeriod - 1, out _); @@ -202,9 +198,8 @@ public void Redelegate() var share1 = repo.GetBond(delegatee1, delegator.Address).Share; var share2 = repo.GetBond(delegatee2, delegator.Address).Share; var rebondGrace = repo.GetRebondGrace(delegatee1, delegator.Address); - var unbondingSet = repo.GetUnbondingSet(); Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); - Assert.Equal(delegatingFAV, delegatee1Balance); + Assert.Equal(delegatingFAV - redelegatingFAV, delegatee1Balance); Assert.Equal(initialShare - redelegatingShare, share1); Assert.Equal(initialShare - redelegatingShare, delegatee1.TotalShares); Assert.Equal(redelegatedDstShare, share2); @@ -212,7 +207,6 @@ public void Redelegate() Assert.Equal(delegatingFAV - redelegatingFAV, delegatee1.TotalDelegated); Assert.Equal(redelegatingFAV, delegatee2.TotalDelegated); Assert.Equal(2, delegator.Delegatees.Count); - Assert.Equal(rebondGrace.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); var entriesByExpireHeight = Assert.Single(rebondGrace.Entries); Assert.Equal(10L + delegatee1.UnbondingPeriod, entriesByExpireHeight.Key); var entry = Assert.Single(entriesByExpireHeight.Value); @@ -232,9 +226,8 @@ public void Redelegate() share1 = repo.GetBond(delegatee1, delegator.Address).Share; share2 = repo.GetBond(delegatee2, delegator.Address).Share; rebondGrace = repo.GetRebondGrace(delegatee1, delegator.Address); - unbondingSet = repo.GetUnbondingSet(); Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); - Assert.Equal(delegatingFAV, delegatee1Balance); + Assert.Equal(delegatingFAV - redelegatingFAV - redelegatingFAV2, delegatee1Balance); Assert.Equal(initialShare - redelegatingShare - redelegatingShare2, share1); Assert.Equal(initialShare - redelegatingShare - redelegatingShare2, delegatee1.TotalShares); Assert.Equal(redelegatedDstShare + redelegatedDstShare2, share2); @@ -242,7 +235,6 @@ public void Redelegate() Assert.Equal(delegatingFAV - redelegatingFAV - redelegatingFAV2, delegatee1.TotalDelegated); Assert.Equal(redelegatingFAV + redelegatingFAV2, delegatee2.TotalDelegated); Assert.Equal(delegatee2.Address, Assert.Single(delegator.Delegatees)); - Assert.Equal(rebondGrace.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); Assert.Equal(2, rebondGrace.Entries.Count); rebondGrace = rebondGrace.Release(10L + delegatee1.UnbondingPeriod - 1, out _); @@ -290,13 +282,6 @@ public void RewardOnDelegate() repo.MintAsset(delegator2.Address, delegatorInitialBalance); var rewards = delegatee.RewardCurrencies.Select(r => r * 100); - foreach (var reward in rewards) - { - repo.MintAsset(delegatee.RewardPoolAddress, reward); - } - - // EndBlock after delegatee's reward - delegatee.CollectRewards(10L); var delegatingFAV1 = delegatee.DelegationCurrency * 10; delegator1.Delegate(delegatee, delegatingFAV1, 10L); @@ -330,14 +315,12 @@ public void RewardOnDelegate() c => repo.World.GetBalance(delegator1.Address, c)); 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 - s), collectedRewards); - Assert.Equal(rewards, legacyRewards); delegator2.Delegate(delegatee, delegatingFAV2, 11L); delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); @@ -347,7 +330,6 @@ public void RewardOnDelegate() c => repo.World.GetBalance(delegator2.Address, c)); 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); @@ -360,7 +342,6 @@ public void RewardOnDelegate() Assert.Equal( rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); - Assert.Equal(rewards, legacyRewards); } [Fact] @@ -375,13 +356,6 @@ public void RewardOnUndelegate() repo.MintAsset(delegator2.Address, delegatorInitialBalance); var rewards = delegatee.RewardCurrencies.Select(r => r * 100); - foreach (var reward in rewards) - { - repo.MintAsset(delegatee.RewardPoolAddress, reward); - } - - // BeginBlock after delegatee's reward - delegatee.CollectRewards(10L); var delegatingFAV1 = delegatee.DelegationCurrency * 10; delegator1.Delegate(delegatee, delegatingFAV1, 10L); @@ -414,14 +388,12 @@ public void RewardOnUndelegate() c => repo.World.GetBalance(delegator1.Address, c)); 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 - s), collectedRewards); - Assert.Equal(rewards, legacyRewards); shareToUndelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; delegator2.Undelegate(delegatee, shareToUndelegate, 11L); @@ -432,7 +404,6 @@ public void RewardOnUndelegate() c => repo.World.GetBalance(delegator2.Address, c)); 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); @@ -444,9 +415,6 @@ public void RewardOnUndelegate() Assert.Equal( rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); - Assert.Equal( - rewards, - legacyRewards); } [Fact] @@ -462,13 +430,6 @@ public void RewardOnRedelegate() repo.MintAsset(delegator2.Address, delegatorInitialBalance); var rewards = delegatee.RewardCurrencies.Select(r => r * 100); - foreach (var reward in rewards) - { - repo.MintAsset(delegatee.RewardPoolAddress, reward); - } - - // EndBlock after delegatee's reward - delegatee.CollectRewards(10L); var delegatingFAV1 = delegatee.DelegationCurrency * 10; delegator1.Delegate(delegatee, delegatingFAV1, 10L); @@ -501,14 +462,12 @@ public void RewardOnRedelegate() c => repo.World.GetBalance(delegator1.Address, c)); 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 - s), collectedRewards); - Assert.Equal(rewards, legacyRewards); shareToRedelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; delegator2.Redelegate(delegatee, dstDelegatee, shareToRedelegate, 11L); @@ -519,7 +478,6 @@ public void RewardOnRedelegate() c => repo.World.GetBalance(delegator2.Address, c)); 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); @@ -532,7 +490,6 @@ public void RewardOnRedelegate() Assert.Equal( rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); - Assert.Equal(rewards, legacyRewards); } [Fact] @@ -548,13 +505,6 @@ public void RewardOnClaim() repo.MintAsset(delegator2.Address, delegatorInitialBalance); var rewards = delegatee.RewardCurrencies.Select(r => r * 100); - foreach (var reward in rewards) - { - repo.MintAsset(delegatee.RewardPoolAddress, reward); - } - - // EndBlock after delegatee's reward - delegatee.CollectRewards(10L); var delegatingFAV1 = delegatee.DelegationCurrency * 10; delegator1.Delegate(delegatee, delegatingFAV1, 10L); @@ -587,14 +537,12 @@ public void RewardOnClaim() c => repo.World.GetBalance(delegator1.Address, c)); 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 - s), collectedRewards); - Assert.Equal(rewards, legacyRewards); shareToRedelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; delegator2.ClaimReward(delegatee, 11L); @@ -605,7 +553,6 @@ public void RewardOnClaim() c => repo.World.GetBalance(delegator2.Address, c)); 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); @@ -618,7 +565,6 @@ public void RewardOnClaim() Assert.Equal( rewards.Zip(rewards1.Zip(rewards2, (f, s) => f + s), (f, s) => f - s).ToArray(), collectedRewards); - Assert.Equal(rewards, legacyRewards); } } } diff --git a/.Lib9c.Tests/Util/DelegationUtil.cs b/.Lib9c.Tests/Util/DelegationUtil.cs index 9899495839..2918ef5b44 100644 --- a/.Lib9c.Tests/Util/DelegationUtil.cs +++ b/.Lib9c.Tests/Util/DelegationUtil.cs @@ -1,14 +1,20 @@ namespace Lib9c.Tests.Util { using System; + using System.Numerics; using Lib9c.Tests.Action; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; + using Nekoyume.Action; using Nekoyume.Action.Guild; using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Model.Guild; using Nekoyume.Model.Stake; + using Nekoyume.Module; using Nekoyume.TableData.Stake; + using Nekoyume.TypedAddress; + using Nekoyume.ValidatorDelegation; public static class DelegationUtil { @@ -49,6 +55,35 @@ public static IWorld PromoteValidator( return promoteValidator.ExecutePublic(actionContext); } + public static IWorld SlashValidator( + IWorld world, + Address validatorAddress, + BigInteger slashFactor, + long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + }; + + var repository = new ValidatorRepository(world, actionContext); + var validatorDelegatee = repository.GetDelegatee(validatorAddress); + if (validatorDelegatee.Jailed) + { + return world; + } + + validatorDelegatee.Slash(slashFactor, blockHeight, blockHeight); + + var guildRepository = new GuildRepository(repository.World, repository.ActionContext); + var guildDelegatee = guildRepository.GetDelegatee(validatorAddress); + guildDelegatee.Slash(slashFactor, blockHeight, blockHeight); + repository.UpdateWorld(guildRepository.World); + + return repository.World; + } + public static IWorld MakeGuild( IWorld world, Address guildMasterAddress, Address validatorAddress, long blockHeight) { @@ -68,6 +103,39 @@ public static IWorld MakeGuild( return makeGuild.Execute(actionContext); } + public static IWorld MakeGuild( + IWorld world, + Address guildMasterAddress, + Address validatorAddress, + long blockHeight, + out GuildAddress guildAddress) + { + world = MakeGuild(world, guildMasterAddress, validatorAddress, blockHeight); + var guildRepository = new GuildRepository(world, new ActionContext()); + var guildParticipant = guildRepository.GetGuildParticipant(guildMasterAddress); + guildAddress = guildParticipant.GuildAddress; + return world; + } + + public static IWorld JoinGuild( + IWorld world, Address agentAddress, GuildAddress guildAddress, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = agentAddress, + RandomSeed = Random.Shared.Next(), + }; + var joinGuild = new JoinGuild(guildAddress); + return joinGuild.Execute(actionContext); + } + public static FungibleAssetValue GetGuildCoinFromNCG(FungibleAssetValue balance) { return FungibleAssetValue.Parse(Currencies.GuildGold, balance.GetQuantityString(true)); @@ -91,23 +159,77 @@ public static IWorld EnsureGuildParticipentIsStaked( return world; } - public static IWorld EnsureStakeReleased( - IWorld world, long blockHeight) + public static IWorld EnsureUnbondedClaimed( + IWorld world, Address agentAddress, long blockHeight) { if (blockHeight < 0) { throw new ArgumentOutOfRangeException(nameof(blockHeight)); } - // TODO : [GuildMigration] Revive below code when the migration is done. - // var actionContext = new ActionContext - // { - // PreviousState = world, - // BlockIndex = blockHeight, - // }; - // var releaseValidatorUnbondings = new ReleaseValidatorUnbondings(); - // return releaseValidatorUnbondings.Execute(actionContext); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = blockHeight, + }; + var claimUnbonded = new ClaimUnbonded(); + return claimUnbonded.Execute(actionContext); + return world; + } + + public static IWorld CreateAvatar(IWorld world, Address agentAddress, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = blockHeight, + }; + var createAvatar = new CreateAvatar + { + name = $"avatar{Random.Shared.Next()}", + }; + + return createAvatar.Execute(actionContext); + } + + public static IWorld CreateAvatar( + IWorld world, Address agentAddress, long blockHeight, out Address avatarAddress) + { + var agentState1 = world.GetAgentState(agentAddress); + var index = agentState1?.avatarAddresses.Count ?? 0; + world = CreateAvatar(world, agentAddress, blockHeight); + var agentState2 = world.GetAgentState(agentAddress); + avatarAddress = agentState2.avatarAddresses[index]; return world; } + + public static IWorld Stake( + IWorld world, + Address agentAddress, + Address avatarAddress, + BigInteger amount, + long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + BlockIndex = blockHeight, + }; + var stake = new Stake(amount, avatarAddress); + return stake.Execute(actionContext); + } } } diff --git a/Lib9c.Policy/Policy/BlockPolicySource.cs b/Lib9c.Policy/Policy/BlockPolicySource.cs index 5fead8e122..3c508dc71c 100644 --- a/Lib9c.Policy/Policy/BlockPolicySource.cs +++ b/Lib9c.Policy/Policy/BlockPolicySource.cs @@ -156,7 +156,6 @@ internal IBlockPolicy GetPolicy( new UpdateValidators(), new RecordProposer(), new RewardGold(), - new ReleaseValidatorUnbondings(), }.ToImmutableArray(), beginTxActions: new IAction[] { new Mortgage(), diff --git a/Lib9c.Policy/Policy/DebugPolicy.cs b/Lib9c.Policy/Policy/DebugPolicy.cs index 8a1f5d1fbd..f44390b19a 100644 --- a/Lib9c.Policy/Policy/DebugPolicy.cs +++ b/Lib9c.Policy/Policy/DebugPolicy.cs @@ -26,7 +26,6 @@ public DebugPolicy() new UpdateValidators(), new RecordProposer(), new RewardGold(), - new ReleaseValidatorUnbondings(), }.ToImmutableArray(), beginTxActions: new IAction[] { new Mortgage(), diff --git a/Lib9c/Action/ClaimStakeReward.cs b/Lib9c/Action/ClaimStakeReward.cs index 1bb4cbbfab..84ca81c20a 100644 --- a/Lib9c/Action/ClaimStakeReward.cs +++ b/Lib9c/Action/ClaimStakeReward.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Immutable; -using System.Globalization; using System.Linq; using Bencodex.Types; using Lib9c; @@ -8,14 +7,11 @@ using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; -using Libplanet.Types.Assets; -using Nekoyume.Action.Garages; using Nekoyume.Extensions; -using Nekoyume.Helper; -using Nekoyume.Model.Item; using Nekoyume.Model.Stake; using Nekoyume.Model.State; using Nekoyume.Module; +using Nekoyume.Module.Guild; using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TableData; using Nekoyume.ValidatorDelegation; @@ -115,6 +111,25 @@ public override IWorld Execute(IActionContext context) AvatarAddress); } + states = Claim( + states, + context, + AvatarAddress, + stakeStateAddr, + avatarState, + stakeStateV2); + + return states; + } + + public static IWorld Claim( + IWorld states, + IActionContext context, + Address avatarAddress, + Address stakeStateAddress, + AvatarState avatarState, + StakeState stakeStateV2) + { var sheets = states.GetSheets(sheetTuples: new[] { ( @@ -134,7 +149,7 @@ public override IWorld Execute(IActionContext context) var stakeRegularRewardSheet = sheets.GetSheet(); // NOTE: var ncg = states.GetGoldCurrency(); - var stakedNcg = states.GetBalance(stakeStateAddr, ncg); + var stakedNcg = states.GetStaked(context.Signer); var stakingLevel = Math.Min( stakeRegularRewardSheet.FindLevelByStakedAmount( context.Signer, @@ -171,7 +186,7 @@ public override IWorld Execute(IActionContext context) foreach (var fav in favResult) { var rewardCurrency = fav.Currency; - var recipient = Currencies.PickAddress(rewardCurrency, context.Signer, AvatarAddress); + var recipient = Currencies.PickAddress(rewardCurrency, context.Signer, avatarAddress); states = states.MintAsset(context, recipient, fav); } @@ -182,8 +197,8 @@ public override IWorld Execute(IActionContext context) context.BlockIndex); return states - .SetLegacyState(stakeStateAddr, stakeStateV2.Serialize()) - .SetAvatarState(AvatarAddress, avatarState); + .SetLegacyState(stakeStateAddress, stakeStateV2.Serialize()) + .SetAvatarState(avatarAddress, avatarState); } } } diff --git a/Lib9c/Action/Guild/ClaimUnbonded.cs b/Lib9c/Action/Guild/ClaimUnbonded.cs new file mode 100644 index 0000000000..7db809549a --- /dev/null +++ b/Lib9c/Action/Guild/ClaimUnbonded.cs @@ -0,0 +1,47 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Nekoyume.Model.Guild; + +namespace Nekoyume.Action.Guild +{ + /// + /// An action to claim unbonded assets. + /// This action can be executed only when the unbonding period is over. + /// + [ActionType(TypeIdentifier)] + public sealed class ClaimUnbonded : ActionBase + { + public const string TypeIdentifier = "claim_unbonded"; + + public ClaimUnbonded() { } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Null.Value); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Null) + { + throw new InvalidCastException(); + } + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var repository = new GuildRepository(world, context); + var guildDelegator = repository.GetDelegator(context.Signer); + guildDelegator.ReleaseUnbondings(context.BlockIndex); + + return repository.World; + } + } +} diff --git a/Lib9c/Action/Stake.cs b/Lib9c/Action/Stake.cs index 33b68e579a..5421b11f91 100644 --- a/Lib9c/Action/Stake.cs +++ b/Lib9c/Action/Stake.cs @@ -26,11 +26,15 @@ namespace Nekoyume.Action { - [ActionType("stake3")] + [ActionType(ActionTypeText)] public class Stake : GameAction, IStakeV1 { + private const string ActionTypeText = "stake3"; + internal BigInteger Amount { get; set; } + internal Address? AvatarAddress { get; private set; } + BigInteger IStakeV1.Amount => Amount; public Stake(BigInteger amount) @@ -38,19 +42,45 @@ public Stake(BigInteger amount) Amount = amount >= 0 ? amount : throw new ArgumentOutOfRangeException(nameof(amount)); + AvatarAddress = null; + } + + public Stake(BigInteger amount, Address avatarAddress) + { + Amount = amount >= 0 + ? amount + : throw new ArgumentOutOfRangeException(nameof(amount)); + AvatarAddress = avatarAddress; } public Stake() { } - protected override IImmutableDictionary PlainValueInternal => - ImmutableDictionary.Empty.Add(AmountKey, (IValue)(Integer)Amount); + protected override IImmutableDictionary PlainValueInternal + { + get + { + var plainValue = ImmutableDictionary.Empty + .Add(AmountKey, (IValue)(Integer)Amount); + if (AvatarAddress.HasValue) + { + plainValue = plainValue.Add(StakeAvatarAddressKey, AvatarAddress.Value.Bencoded); + } + + return plainValue; + } + } protected override void LoadPlainValueInternal( IImmutableDictionary plainValue) { Amount = plainValue[AmountKey].ToBigInteger(); + AvatarAddress = null; + if (plainValue.TryGetValue(StakeAvatarAddressKey, out var avatarAddress)) + { + AvatarAddress = new Address(avatarAddress); + } } public override IWorld Execute(IActionContext context) @@ -104,7 +134,7 @@ public override IWorld Execute(IActionContext context) var stakeStateAddress = LegacyStakeState.DeriveAddress(context.Signer); var currency = states.GetGoldCurrency(); var currentBalance = states.GetBalance(context.Signer, currency); - var stakedBalance = states.GetBalance(stakeStateAddress, currency); + var stakedBalance = states.GetStaked(context.Signer); var targetStakeBalance = currency * Amount; // NOTE: When the total balance is less than the target balance. if (currentBalance + stakedBalance < targetStakeBalance) @@ -140,26 +170,35 @@ public override IWorld Execute(IActionContext context) return states; } - // NOTE: Cannot anything if staking state is claimable. + // NOTE: Claim silently if there are any. if (stakeStateV2.ClaimableBlockIndex <= context.BlockIndex) { var validatorRepository = new ValidatorRepository(states, context); var isValidator = validatorRepository.TryGetDelegatee( context.Signer, out var validatorDelegatee); - if (!isValidator) + if (!isValidator && AvatarAddress.HasValue) { - throw new StakeExistingClaimableException(); + if (!states.TryGetAvatarState( + context.Signer, + AvatarAddress.Value, + out var avatarState)) + { + throw new FailedLoadStateException( + ActionTypeText, + addressesHex, + typeof(AvatarState), + AvatarAddress.Value); + } + + states = ClaimStakeReward.Claim(states, context, AvatarAddress.Value, stakeStateAddress, avatarState, stakeStateV2); } - } - - // NOTE: When the staking state is locked up. - // TODO: Remove this condition after the migration is done. - if (stakeStateV2.CancellableBlockIndex > context.BlockIndex) - { - // NOTE: Cannot re-contract with less balance. - if (targetStakeBalance < stakedBalance) + else if (isValidator) + { + } + else { - throw new RequiredBlockIndexException(); + throw new InvalidOperationException( + $"For non-validators, {nameof(AvatarAddress)} is required."); } } @@ -181,6 +220,7 @@ public override IWorld Execute(IActionContext context) stakedBalance, targetStakeBalance, latestStakeContract); + Log.Debug( "{AddressesHex}Stake Total Executed Time: {Elapsed}", addressesHex, @@ -222,57 +262,26 @@ private static IWorld ContractNewStake( var guildRepository = new GuildRepository(state, context); - // TODO : [GuildMigration] Remove below code when the migration is done. if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) { var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); var guildDelegatee = guildRepository.GetDelegatee(guild.ValidatorAddress); var share = guildDelegatee.ShareFromFAV(gg); + if (targetStakeBalance.Sign == 0) + { + share = guildRepository.GetBond(guildDelegatee, agentAddress).Share; + } - var guildDelegator = guildRepository.GetDelegator(agentAddress); - guildDelegatee.Unbond(guildDelegator, share, height); - - var validatorRepository = new ValidatorRepository(guildRepository); - var validatorDelegatee = validatorRepository.GetDelegatee(guild.ValidatorAddress); - var validatorDelegator = validatorRepository.GetDelegator(guild.Address); - validatorDelegatee.Unbond(validatorDelegator, share, height); - - state = validatorRepository.World; - state = state.BurnAsset(context, guildDelegatee.DelegationPoolAddress, gg); + guildParticipant.Undelegate(guild, share, height); + state = guildRepository.World; } else { - state = state.BurnAsset(context, stakeStateAddr, gg); + state = state + .TransferAsset(context, stakeStateAddr, context.Signer, -additionalBalance) + .BurnAsset(context, stakeStateAddr, gg); } - state = state - .TransferAsset(context, stakeStateAddr, context.Signer, -additionalBalance); - - // TODO : [GuildMigration] Revive below code when the migration is done. - // if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) - // { - // var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); - // var guildDelegatee = guildRepository.GetGuildDelegatee(guild.ValidatorAddress); - // var share = guildDelegatee.ShareFromFAV(gg); - // guildParticipant.Undelegate(guild, share, height); - // state = guildRepository.World; - // } - // else - // { - // var delegateeAddress = Addresses.NonValidatorDelegatee; - // var delegatorAddress = context.Signer; - // var repository = new GuildRepository(state, context); - // var unbondLockInAddress = DelegationAddress.UnbondLockInAddress(delegateeAddress, repository.DelegateeAccountAddress, delegatorAddress); - // var unbondLockIn = new UnbondLockIn( - // unbondLockInAddress, ValidatorDelegatee.ValidatorMaxUnbondLockInEntries, delegateeAddress, delegatorAddress, null); - // unbondLockIn = unbondLockIn.LockIn( - // gg, height, height + ValidatorDelegatee.ValidatorUnbondingPeriod); - // repository.SetUnbondLockIn(unbondLockIn); - // repository.SetUnbondingSet( - // repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); - // state = repository.World; - // } - if ((stakedBalance + additionalBalance).Sign == 0) { return state.MutateAccount( diff --git a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs deleted file mode 100644 index aabba19fcd..0000000000 --- a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs +++ /dev/null @@ -1,145 +0,0 @@ -using Bencodex.Types; -using Libplanet.Action.State; -using Libplanet.Action; -using Libplanet.Crypto; -using Nekoyume.ValidatorDelegation; -using Nekoyume.Delegation; -using System; -using System.Linq; -using System.Collections.Immutable; -using Libplanet.Types.Assets; -using System.Numerics; -using Nekoyume.Model.Guild; -using Nekoyume.Module.Guild; -using Nekoyume.TypedAddress; -using Nekoyume.Module; -using Nekoyume.Model.Stake; -using Lib9c; -using Nekoyume.Action.Guild.Migration.LegacyModels; - -namespace Nekoyume.Action.ValidatorDelegation -{ - public sealed class ReleaseValidatorUnbondings : ActionBase - { - public ReleaseValidatorUnbondings() { } - - public ReleaseValidatorUnbondings(Address validatorDelegatee) - { - } - - public override IValue PlainValue => Null.Value; - - public override void LoadPlainValue(IValue plainValue) - { - } - - public override IWorld Execute(IActionContext context) - { - var world = context.PreviousState; - - if (world.GetDelegationMigrationHeight() is null) - { - return world; - } - - var repository = new GuildRepository(world, context); - var unbondingSet = repository.GetUnbondingSet(); - var unbondings = unbondingSet.UnbondingsToRelease(context.BlockIndex); - - unbondings = unbondings.Select(unbonding => - { - switch (unbonding) - { - case UnbondLockIn unbondLockIn: - unbondLockIn = unbondLockIn.Release(context.BlockIndex, out var releasedFAV); - repository.SetUnbondLockIn(unbondLockIn); - repository.UpdateWorld( - Unstake(repository.World, context, unbondLockIn, releasedFAV)); - return unbondLockIn; - case RebondGrace rebondGrace: - rebondGrace = rebondGrace.Release(context.BlockIndex, out _); - repository.SetRebondGrace(rebondGrace); - return rebondGrace; - default: - throw new InvalidOperationException("Invalid unbonding type."); - } - }).ToImmutableArray(); - - repository.SetUnbondingSet(unbondingSet.SetUnbondings(unbondings)); - - return repository.World; - } - - private IWorld Unstake( - IWorld world, IActionContext context, UnbondLockIn unbondLockIn, FungibleAssetValue? releasedFAV) - { - var agentAddress = new AgentAddress(unbondLockIn.DelegatorAddress); - var guildRepository = new GuildRepository(world, context); - var goldCurrency = world.GetGoldCurrency(); - if (!IsValidator(world, context, unbondLockIn.DelegateeAddress)) - { - if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) - { - var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); - var stakeStateAddress = guildParticipant.DelegationPoolAddress; - var gg = world.GetBalance(stakeStateAddress, ValidatorDelegatee.ValidatorDelegationCurrency); - if (gg.Sign > 0) - { - var (ncg, _) = ConvertToGoldCurrency(gg, goldCurrency); - world = world.BurnAsset(context, stakeStateAddress, gg); - world = world.TransferAsset( - context, stakeStateAddress, agentAddress, ncg); - } - } - else - { - if (releasedFAV is not FungibleAssetValue gg || gg.Sign < 1) - { - return world; - } - - var stakeStateAddress = StakeState.DeriveAddress(agentAddress); - var (ncg, _) = ConvertToGoldCurrency(gg, goldCurrency); - world = world - .TransferAsset(context, stakeStateAddress, agentAddress, ncg) - .BurnAsset(context, stakeStateAddress, gg); - } - } - - return world; - } - - private static bool IsValidator(IWorld world, IActionContext context, Address address) - { - var repository = new ValidatorRepository(world, context); - try - { - repository.GetDelegatee(address); - return true; - } - catch (FailedLoadStateException) - { - return false; - } - } - - private static (FungibleAssetValue Gold, FungibleAssetValue Remainder) - ConvertToGoldCurrency(FungibleAssetValue fav, Currency targetCurrency) - { - var sourceCurrency = fav.Currency; - if (targetCurrency.DecimalPlaces < sourceCurrency.DecimalPlaces) - { - var d = BigInteger.Pow(10, sourceCurrency.DecimalPlaces - targetCurrency.DecimalPlaces); - var value = FungibleAssetValue.FromRawValue(targetCurrency, fav.RawValue / d); - var fav2 = FungibleAssetValue.FromRawValue(sourceCurrency, value.RawValue * d); - return (value, fav - fav2); - } - else - { - var d = BigInteger.Pow(10, targetCurrency.DecimalPlaces - sourceCurrency.DecimalPlaces); - var value = FungibleAssetValue.FromRawValue(targetCurrency, fav.RawValue * d); - return (value, targetCurrency * 0); - } - } - } -} diff --git a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs index 1b7014cbe6..f130ddd0f6 100644 --- a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs +++ b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs @@ -63,6 +63,7 @@ public override IWorld Execute(IActionContext context) var guildRepository = new GuildRepository(repository.World, repository.ActionContext); var guildDelegatee = guildRepository.GetDelegatee(abstain.Address); guildDelegatee.Slash(LivenessSlashFactor, context.BlockIndex, context.BlockIndex); + guildDelegatee.Jail(context.BlockIndex + AbstainJailTime); repository.UpdateWorld(guildRepository.World); } @@ -83,6 +84,7 @@ public override IWorld Execute(IActionContext context) var guildRepository = new GuildRepository(repository.World, repository.ActionContext); var guildDelegatee = guildRepository.GetDelegatee(e.TargetAddress); guildDelegatee.Slash(DuplicateVoteSlashFactor, e.Height, context.BlockIndex); + guildDelegatee.Tombstone(); repository.UpdateWorld(guildRepository.World); break; default: diff --git a/Lib9c/Action/ValidatorDelegation/UnjailValidator.cs b/Lib9c/Action/ValidatorDelegation/UnjailValidator.cs index bc060f7805..92dcca3e1e 100644 --- a/Lib9c/Action/ValidatorDelegation/UnjailValidator.cs +++ b/Lib9c/Action/ValidatorDelegation/UnjailValidator.cs @@ -3,6 +3,7 @@ using Libplanet.Action.State; using Libplanet.Action; using Nekoyume.ValidatorDelegation; +using Nekoyume.Model.Guild; namespace Nekoyume.Action.ValidatorDelegation { @@ -32,11 +33,16 @@ public override IWorld Execute(IActionContext context) GasTracer.UseGas(1); var world = context.PreviousState; - var repository = new ValidatorRepository(world, context); - var delegatee = repository.GetDelegatee(context.Signer); - delegatee.Unjail(context.BlockIndex); + var validatorRepository = new ValidatorRepository(world, context); + var validatorDelegatee = validatorRepository.GetDelegatee(context.Signer); + validatorDelegatee.Unjail(context.BlockIndex); - return repository.World; + var guildRepository = new GuildRepository( + validatorRepository.World, validatorRepository.ActionContext); + var guildDelegatee = guildRepository.GetDelegatee(context.Signer); + guildDelegatee.Unjail(context.BlockIndex); + + return guildRepository.World; } } } diff --git a/Lib9c/Delegation/Delegatee.cs b/Lib9c/Delegation/Delegatee.cs index d78b40f2d2..a158372450 100644 --- a/Lib9c/Delegation/Delegatee.cs +++ b/Lib9c/Delegation/Delegatee.cs @@ -58,12 +58,6 @@ private Delegatee(DelegateeMetadata metadata, TRepository repository) Repository = repository; } - public event EventHandler? DelegationChanged; - - public event EventHandler? Enjailed; - - public event EventHandler? Unjailed; - public DelegateeMetadata Metadata { get; } public TRepository Repository { get; } @@ -124,7 +118,7 @@ public void Jail(long releaseHeight) Metadata.JailedUntil = releaseHeight; Metadata.Jailed = true; Repository.SetDelegateeMetadata(Metadata); - Enjailed?.Invoke(this, EventArgs.Empty); + OnEnjailed(); } public void Unjail(long height) @@ -147,7 +141,7 @@ public void Unjail(long height) Metadata.JailedUntil = -1L; Metadata.Jailed = false; Repository.SetDelegateeMetadata(Metadata); - Unjailed?.Invoke(this, EventArgs.Empty); + OnUnjailed(); } public void Tombstone() @@ -225,7 +219,7 @@ public virtual BigInteger Bond(TDelegator delegator, FungibleAssetValue fav, lon Repository.SetBond(bond); StartNewRewardPeriod(height); Repository.SetDelegateeMetadata(Metadata); - DelegationChanged?.Invoke(this, height); + OnDelegationChanged(height); return share; } @@ -233,7 +227,7 @@ public virtual BigInteger Bond(TDelegator delegator, FungibleAssetValue fav, lon public FungibleAssetValue Unbond(TDelegator delegator, BigInteger share, long height) { DistributeReward(delegator, height); - if (TotalShares.IsZero || TotalDelegated.RawValue.IsZero) + if (TotalShares.IsZero) { throw new InvalidOperationException( "Cannot unbond without bonding."); @@ -252,7 +246,7 @@ public FungibleAssetValue Unbond(TDelegator delegator, BigInteger share, long he Repository.SetBond(bond); StartNewRewardPeriod(height); Repository.SetDelegateeMetadata(Metadata); - DelegationChanged?.Invoke(this, height); + OnDelegationChanged(height); return fav; } @@ -267,21 +261,10 @@ public void DistributeReward(TDelegator delegator, long height) if (Repository.GetCurrentRewardBase((TDelegatee)this) is RewardBase rewardBase) { var lastRewardBase = Repository.GetRewardBase((TDelegatee)this, bond.LastDistributeHeight.Value); - TransferReward(delegator, share, rewardBase, lastRewardBase); + var rewards = CalculateRewards(share, rewardBase, lastRewardBase); + TransferRewards(delegator, rewards); // TransferRemainders(newRecord); } - else - { - IEnumerable lumpSumRewardsRecords - = GetLumpSumRewardsRecords(bond.LastDistributeHeight); - - foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) - { - TransferReward(delegator, share, record); - // TransferRemainders(newRecord); - Repository.SetLumpSumRewardsRecord(record); - } - } } if (bond.LastDistributeHeight != height) @@ -309,26 +292,6 @@ public void CollectRewards(long height) Repository.SetRewardBase(rewardBase); } - else - { - LumpSumRewardsRecord record = Repository.GetCurrentLumpSumRewardsRecord((TDelegatee)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) @@ -350,12 +313,7 @@ public virtual void Slash(BigInteger slashFactor, long infractionHeight, long he { var unbonding = UnbondingFactory.GetUnbondingFromRef(item, Repository); - unbonding = unbonding.Slash(slashFactor, infractionHeight, height, out var slashedFAV); - - if (slashedFAV.HasValue) - { - slashed += slashedFAV.Value; - } + unbonding = unbonding.Slash(slashFactor, infractionHeight, height, SlashedPoolAddress); if (unbonding.IsEmpty) { @@ -387,35 +345,12 @@ public virtual void Slash(BigInteger slashFactor, long infractionHeight, long he } Repository.SetDelegateeMetadata(Metadata); - DelegationChanged?.Invoke(this, height); + OnDelegationChanged(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; - } - /// /// Start a new reward period. /// It generates a new and archives the current one. @@ -425,8 +360,6 @@ ImmutableDictionary reward /// public void StartNewRewardPeriod(long height) { - MigrateLumpSumRewardsRecords(); - RewardBase newRewardBase; if (Repository.GetCurrentRewardBase((TDelegatee)this) is RewardBase rewardBase) { @@ -457,46 +390,7 @@ public void StartNewRewardPeriod(long height) Repository.SetRewardBase(newRewardBase); } - private List GetLumpSumRewardsRecords(long? lastRewardHeight) - { - List records = new(); - if (lastRewardHeight is null - || !(Repository.GetCurrentLumpSumRewardsRecord((TDelegatee)this) is LumpSumRewardsRecord record)) - { - return records; - } - - while (record.StartHeight >= lastRewardHeight) - { - records.Add(record); - - if (!(record.LastStartHeight is long lastStartHeight)) - { - break; - } - - record = Repository.GetLumpSumRewardsRecord((TDelegatee)this, lastStartHeight) - ?? throw new InvalidOperationException( - $"Lump sum rewards record for #{lastStartHeight} is missing"); - } - - return records; - } - - private void TransferReward(TDelegator 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 TransferReward( - TDelegator delegator, + public IEnumerable CalculateRewards( BigInteger share, RewardBase currentRewardBase, RewardBase? lastRewardBase) @@ -515,96 +409,36 @@ private void TransferReward( } var reward = c.Value - lastCumulativeEach; - - if (reward.Sign > 0) - { - Repository.TransferAsset(DistributionPoolAddress(), delegator.RewardAddress, reward); - } + yield return reward; } } - private void TransferRemainders(LumpSumRewardsRecord record) + protected virtual void OnDelegationChanged(long height) { - foreach (var rewardCurrency in RewardCurrencies) - { - FungibleAssetValue remainder = Repository.GetBalance(record.Address, rewardCurrency); + } - if (remainder.Sign > 0) - { - Repository.TransferAsset(record.Address, RewardRemainderPoolAddress, remainder); - } - } + protected virtual void OnEnjailed() + { } - private void MigrateLumpSumRewardsRecords() + protected virtual void OnUnjailed() { - var growSize = 100; - var capacity = 5000; - List records = new(capacity); - if (!(Repository.GetCurrentLumpSumRewardsRecord((TDelegatee)this) is LumpSumRewardsRecord record)) - { - return; - } + } - while (record.LastStartHeight is long lastStartHeight) - { - if (records.Count == capacity) - { - capacity += growSize; - records.Capacity = capacity; - } + internal void AddUnbondingRef(UnbondingRef reference) + => Metadata.AddUnbondingRef(reference); - records.Add(record); - record = Repository.GetLumpSumRewardsRecord((TDelegatee)this, lastStartHeight) - ?? throw new InvalidOperationException( - $"Lump sum rewards record for #{lastStartHeight} is missing"); - } + internal void RemoveUnbondingRef(UnbondingRef reference) + => Metadata.RemoveUnbondingRef(reference); - RewardBase? rewardBase = null; - for (var i = records.Count - 1; i >= 0; i--) + private void TransferRewards(TDelegator delegator, IEnumerable rewards) + { + foreach (var reward in rewards) { - 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((TDelegatee)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) + if (reward.Sign > 0) { - var toTransfer = Repository.GetBalance(recordEach.Address, r.Key); - if (toTransfer.Sign > 0) - { - Repository.TransferAsset(recordEach.Address, DistributionPoolAddress(), toTransfer); - } + Repository.TransferAsset(DistributionPoolAddress(), delegator.RewardAddress, reward); } - - Repository.RemoveLumpSumRewardsRecord(recordEach); - } - - if (rewardBase is RewardBase rewardBaseToSet) - { - Repository.SetRewardBase(rewardBaseToSet); } } } diff --git a/Lib9c/Delegation/DelegateeMetadata.cs b/Lib9c/Delegation/DelegateeMetadata.cs index 29dd536f07..8a94d69097 100644 --- a/Lib9c/Delegation/DelegateeMetadata.cs +++ b/Lib9c/Delegation/DelegateeMetadata.cs @@ -91,7 +91,7 @@ public DelegateeMetadata( -1L, false, ImmutableSortedSet.Empty) - { + { } public DelegateeMetadata( @@ -107,109 +107,50 @@ public DelegateeMetadata( Address delegateeAccountAddress, List bencoded) { - Currency delegationCurrency; - IEnumerable rewardCurrencies; - Address delegationPoolAddress; - Address rewardPoolAddress; - Address rewardRemainderPoolAddress; - Address slashedPoolAddress; - long unbondingPeriod; - int maxUnbondLockInEntries; - int maxRebondGraceEntries; - FungibleAssetValue totalDelegated; - BigInteger totalShares; - bool jailed; - long jailedUntil; - bool tombstoned; - IEnumerable unbondingRefs; - - // TODO: Remove this if block after migration to state version 1 is done. - if (bencoded[0] is not Text) + if (bencoded[0] is not Text text || text != StateTypeName || bencoded[1] is not Integer integer) { - // Assume state version 0 - delegationCurrency = new Currency(bencoded[0]); - rewardCurrencies = ((List)bencoded[1]).Select(v => new Currency(v)); - delegationPoolAddress = new Address(bencoded[2]); - rewardPoolAddress = new Address(bencoded[3]); - rewardRemainderPoolAddress = new Address(bencoded[4]); - slashedPoolAddress = new Address(bencoded[5]); - unbondingPeriod = (Integer)bencoded[6]; - maxUnbondLockInEntries = (Integer)bencoded[7]; - maxRebondGraceEntries = (Integer)bencoded[8]; - totalDelegated = new FungibleAssetValue(bencoded[10]); - totalShares = (Integer)bencoded[11]; - jailed = (Bencodex.Types.Boolean)bencoded[12]; - jailedUntil = (Integer)bencoded[13]; - tombstoned = (Bencodex.Types.Boolean)bencoded[14]; - unbondingRefs = ((List)bencoded[15]).Select(item => new UnbondingRef(item)); + throw new InvalidCastException(); } - else + + if (integer > StateVersion) { - 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."); - } - - delegationCurrency = new Currency(bencoded[2]); - rewardCurrencies = ((List)bencoded[3]).Select(v => new Currency(v)); - delegationPoolAddress = new Address(bencoded[4]); - rewardPoolAddress = new Address(bencoded[5]); - rewardRemainderPoolAddress = new Address(bencoded[6]); - slashedPoolAddress = new Address(bencoded[7]); - unbondingPeriod = (Integer)bencoded[8]; - maxUnbondLockInEntries = (Integer)bencoded[9]; - maxRebondGraceEntries = (Integer)bencoded[10]; - totalDelegated = new FungibleAssetValue(bencoded[11]); - totalShares = (Integer)bencoded[12]; - jailed = (Bencodex.Types.Boolean)bencoded[13]; - jailedUntil = (Integer)bencoded[14]; - tombstoned = (Bencodex.Types.Boolean)bencoded[15]; - unbondingRefs = ((List)bencoded[16]).Select(item => new UnbondingRef(item)); + throw new FailedLoadStateException("Un-deserializable state."); } - if (!totalDelegated.Currency.Equals(delegationCurrency)) + DelegateeAddress = delegateeAddress; + DelegateeAccountAddress = delegateeAccountAddress; + DelegationCurrency = new Currency(bencoded[2]); + RewardCurrencies = ((List)bencoded[3]).Select(v => new Currency(v)).ToImmutableSortedSet(_currencyComparer); + DelegationPoolAddress = new Address(bencoded[4]); + RewardPoolAddress = new Address(bencoded[5]); + RewardRemainderPoolAddress = new Address(bencoded[6]); + SlashedPoolAddress = new Address(bencoded[7]); + UnbondingPeriod = (Integer)bencoded[8]; + MaxUnbondLockInEntries = (Integer)bencoded[9]; + MaxRebondGraceEntries = (Integer)bencoded[10]; + TotalDelegatedFAV = new FungibleAssetValue(bencoded[11]); + TotalShares = (Integer)bencoded[12]; + Jailed = (Bencodex.Types.Boolean)bencoded[13]; + JailedUntil = (Integer)bencoded[14]; + Tombstoned = (Bencodex.Types.Boolean)bencoded[15]; + UnbondingRefs = ((List)bencoded[16]).Select(item => new UnbondingRef(item)).ToImmutableSortedSet(); + + if (!TotalDelegatedFAV.Currency.Equals(DelegationCurrency)) { throw new InvalidOperationException("Invalid currency."); } - if (totalDelegated.Sign < 0) + if (TotalDelegatedFAV.Sign < 0) { - throw new ArgumentOutOfRangeException( - nameof(totalDelegated), - totalDelegated, + throw new InvalidOperationException( "Total delegated must be non-negative."); } - if (totalShares.Sign < 0) + if (TotalShares.Sign < 0) { - throw new ArgumentOutOfRangeException( - nameof(totalShares), - totalShares, + throw new InvalidOperationException( "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; - TotalDelegatedFAV = totalDelegated; - TotalShares = totalShares; - Jailed = jailed; - JailedUntil = jailedUntil; - Tombstoned = tombstoned; - UnbondingRefs = unbondingRefs.ToImmutableSortedSet(); } private DelegateeMetadata( @@ -292,11 +233,11 @@ public Address Address public Address SlashedPoolAddress { get; } - public long UnbondingPeriod { get; private set; } + public long UnbondingPeriod { get; internal set; } - public int MaxUnbondLockInEntries { get; } + public int MaxUnbondLockInEntries { get; internal set; } - public int MaxRebondGraceEntries { get; } + public int MaxRebondGraceEntries { get; internal set; } public FungibleAssetValue TotalDelegatedFAV { get; private set; } @@ -464,6 +405,5 @@ public void UpdateUnbondingPeriod(long unbondingPeriod) { UnbondingPeriod = unbondingPeriod; } - } } diff --git a/Lib9c/Delegation/DelegationRepository.cs b/Lib9c/Delegation/DelegationRepository.cs index 0a8167e737..c7beb0887e 100644 --- a/Lib9c/Delegation/DelegationRepository.cs +++ b/Lib9c/Delegation/DelegationRepository.cs @@ -190,7 +190,7 @@ public RebondGrace GetRebondGrace(TDelegatee delegatee, Address delegatorAddress IValue? value = rebondGraceAccount.GetState(address); return value is IValue bencoded ? new RebondGrace(address, delegatee.MaxRebondGraceEntries, bencoded, this) - : new RebondGrace(address, delegatee.MaxRebondGraceEntries, this); + : new RebondGrace(address, delegatee.MaxRebondGraceEntries, delegatee.Address, delegatorAddress, this); } public RebondGrace GetUnlimitedRebondGrace(Address address) @@ -201,11 +201,6 @@ public RebondGrace GetUnlimitedRebondGrace(Address address) : throw new FailedLoadStateException("RebondGrace not found."); } - public UnbondingSet GetUnbondingSet() - => unbondingSetAccount.GetState(UnbondingSet.Address) is IValue bencoded - ? new UnbondingSet(bencoded, this) - : new UnbondingSet(this); - /// public RewardBase? GetCurrentRewardBase(TDelegatee delegatee) { @@ -282,13 +277,6 @@ public void SetRebondGrace(RebondGrace rebondGrace) : rebondGraceAccount.SetState(rebondGrace.Address, rebondGrace.Bencoded); } - public void SetUnbondingSet(UnbondingSet unbondingSet) - { - unbondingSetAccount = unbondingSet.IsEmpty - ? unbondingSetAccount.RemoveState(UnbondingSet.Address) - : unbondingSetAccount.SetState(UnbondingSet.Address, unbondingSet.Bencoded); - } - /// public void SetRewardBase(RewardBase rewardBase) { diff --git a/Lib9c/Delegation/Delegator.cs b/Lib9c/Delegation/Delegator.cs index a526f860db..84e2edd1ab 100644 --- a/Lib9c/Delegation/Delegator.cs +++ b/Lib9c/Delegation/Delegator.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Numerics; using Bencodex.Types; using Libplanet.Crypto; @@ -74,6 +76,8 @@ public virtual void Delegate( throw new InvalidOperationException("Delegatee is tombstoned."); } + ReleaseUnbondings(height); + delegatee.Bond((TDelegator)this, fav, height); Metadata.AddDelegatee(delegatee.Address); Repository.TransferAsset(DelegationPoolAddress, delegatee.DelegationPoolAddress, fav); @@ -99,6 +103,8 @@ public virtual void Undelegate( nameof(height), height, "Height must be positive."); } + ReleaseUnbondings(height); + UnbondLockIn unbondLockIn = Repository.GetUnbondLockIn(delegatee, Address); if (unbondLockIn.IsFull) @@ -107,19 +113,20 @@ public virtual void Undelegate( } FungibleAssetValue fav = delegatee.Unbond((TDelegator)this, share, height); - unbondLockIn = unbondLockIn.LockIn( - fav, height, height + delegatee.UnbondingPeriod); + + if (fav.Sign > 0) + { + unbondLockIn = unbondLockIn.LockIn( + fav, height, height + delegatee.UnbondingPeriod); + AddUnbondingRef(delegatee, UnbondingFactory.ToReference(unbondLockIn)); + Repository.SetUnbondLockIn(unbondLockIn); + } 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.SetDelegator((TDelegator)this); } @@ -143,16 +150,31 @@ public virtual void Redelegate( nameof(height), height, "Height must be positive."); } + if (srcDelegatee.Equals(dstDelegatee)) + { + throw new InvalidOperationException("Source and destination delegatees are the same."); + } + if (dstDelegatee.Tombstoned) { throw new InvalidOperationException("Destination delegatee is tombstoned."); } + ReleaseUnbondings(height); + + RebondGrace srcRebondGrace = Repository.GetRebondGrace(srcDelegatee, Address); + + if (srcRebondGrace.IsFull) + { + throw new InvalidOperationException("Rebonding is full."); + } + FungibleAssetValue fav = srcDelegatee.Unbond( (TDelegator)this, share, height); dstDelegatee.Bond( (TDelegator)this, fav, height); - RebondGrace srcRebondGrace = Repository.GetRebondGrace(srcDelegatee, Address).Grace( + + srcRebondGrace = srcRebondGrace.Grace( dstDelegatee.Address, fav, height, @@ -165,11 +187,10 @@ public virtual void Redelegate( Metadata.AddDelegatee(dstDelegatee.Address); - srcDelegatee.AddUnbondingRef(UnbondingFactory.ToReference(srcRebondGrace)); + AddUnbondingRef(srcDelegatee, UnbondingFactory.ToReference(srcRebondGrace)); Repository.SetRebondGrace(srcRebondGrace); - Repository.SetUnbondingSet( - Repository.GetUnbondingSet().SetUnbonding(srcRebondGrace)); + Repository.TransferAsset(srcDelegatee.DelegationPoolAddress, dstDelegatee.DelegationPoolAddress, fav); Repository.SetDelegator((TDelegator)this); } @@ -192,6 +213,8 @@ public void CancelUndelegate( nameof(height), height, "Height must be positive."); } + ReleaseUnbondings(height); + UnbondLockIn unbondLockIn = Repository.GetUnbondLockIn(delegatee, Address); if (unbondLockIn.IsFull) @@ -205,12 +228,10 @@ public void CancelUndelegate( if (unbondLockIn.IsEmpty) { - delegatee.RemoveUnbondingRef(UnbondingFactory.ToReference(unbondLockIn)); + RemoveUnbondingRef(delegatee, UnbondingFactory.ToReference(unbondLockIn)); } Repository.SetUnbondLockIn(unbondLockIn); - Repository.SetUnbondingSet( - Repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); Repository.SetDelegator((TDelegator)this); } @@ -228,5 +249,74 @@ public void ClaimReward( void IDelegator.ClaimReward(IDelegatee delegatee, long height) => ClaimReward((TDelegatee)delegatee, height); + + public void ReleaseUnbondings(long height) + { + var unbondings = Metadata.UnbondingRefs.Select( + unbondingRef => UnbondingFactory.GetUnbondingFromRef(unbondingRef, Repository)); + ReleaseUnbondings(unbondings, height); + Repository.SetDelegator((TDelegator)this); + } + + protected virtual void OnUnbondingReleased( + long height, IUnbonding releasedUnbonding, FungibleAssetValue? releasedFAV) + { + } + + private void ReleaseUnbondings(IEnumerable unbondings, long height) + { + foreach (var unbonding in unbondings) + { + ReleaseUnbonding(unbonding, height); + } + } + + private void ReleaseUnbonding(IUnbonding unbonding, long height) + { + FungibleAssetValue? releasedFAV; + switch (unbonding) + { + case UnbondLockIn unbondLockIn: + unbondLockIn = unbondLockIn.Release(height, out releasedFAV); + Repository.SetUnbondLockIn(unbondLockIn); + unbonding = unbondLockIn; + break; + case RebondGrace rebondGrace: + rebondGrace = rebondGrace.Release(height, out releasedFAV); + Repository.SetRebondGrace(rebondGrace); + unbonding = rebondGrace; + break; + default: + throw new InvalidOperationException("Invalid unbonding type."); + } + + if (unbonding.IsEmpty) + { + TDelegatee delegatee = Repository.GetDelegatee(unbonding.DelegateeAddress); + RemoveUnbondingRef(delegatee, UnbondingFactory.ToReference(unbonding)); + } + + OnUnbondingReleased(height, unbonding, releasedFAV); + } + + private void AddUnbondingRef(TDelegatee delegatee, UnbondingRef reference) + { + AddUnbondingRef(reference); + delegatee.AddUnbondingRef(reference); + Repository.SetDelegatee(delegatee); + } + + private void RemoveUnbondingRef(TDelegatee delegatee, UnbondingRef reference) + { + RemoveUnbondingRef(reference); + delegatee.RemoveUnbondingRef(reference); + Repository.SetDelegatee(delegatee); + } + + private void AddUnbondingRef(UnbondingRef reference) + => Metadata.AddUnbondingRef(reference); + + private void RemoveUnbondingRef(UnbondingRef reference) + => Metadata.RemoveUnbondingRef(reference); } } diff --git a/Lib9c/Delegation/DelegatorMetadata.cs b/Lib9c/Delegation/DelegatorMetadata.cs index 9fe2be2088..22232b5f01 100644 --- a/Lib9c/Delegation/DelegatorMetadata.cs +++ b/Lib9c/Delegation/DelegatorMetadata.cs @@ -1,15 +1,20 @@ #nullable enable using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Bencodex; using Bencodex.Types; using Libplanet.Crypto; +using Nekoyume.Action; namespace Nekoyume.Delegation { public class DelegatorMetadata : IDelegatorMetadata { + private const string StateTypeName = "delegator_metadata"; + private const long StateVersion = 1; + private Address? _address; public DelegatorMetadata( @@ -22,7 +27,8 @@ public DelegatorMetadata( accountAddress, delegationPoolAddress, rewardAddress, - ImmutableSortedSet
.Empty) + ImmutableSortedSet
.Empty, + ImmutableSortedSet.Empty) { } @@ -35,16 +41,48 @@ public DelegatorMetadata( } public DelegatorMetadata( - Address address, - Address accountAddress, + Address delegatorAddress, + Address delegatorAccountAddress, List bencoded) - : this( - address, - accountAddress, - new Address(bencoded[0]), - new Address(bencoded[1]), - ((List)bencoded[2]).Select(item => new Address(item)).ToImmutableSortedSet()) { + Address delegationPoolAddress; + Address rewardAddress; + IEnumerable
delegatees; + IEnumerable unbondingRefs; + + // TODO: Remove this if block after migration to state version 1 is done. + if (bencoded[0] is not Text) + { + // Assume state version 0 + delegationPoolAddress = new Address(bencoded[0]); + rewardAddress = new Address(bencoded[1]); + delegatees = ((List)bencoded[2]).Select(item => new Address(item)); + unbondingRefs = ImmutableSortedSet.Empty; + } + else + { + 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."); + } + + delegationPoolAddress = new Address(bencoded[2]); + rewardAddress = new Address(bencoded[3]); + delegatees = ((List)bencoded[4]).Select(item => new Address(item)); + unbondingRefs = ((List)bencoded[5]).Select(item => new UnbondingRef(item)); + } + + DelegatorAddress = delegatorAddress; + DelegatorAccountAddress = delegatorAccountAddress; + DelegationPoolAddress = delegationPoolAddress; + RewardAddress = rewardAddress; + Delegatees = delegatees.ToImmutableSortedSet(); + UnbondingRefs = unbondingRefs.ToImmutableSortedSet(); } private DelegatorMetadata( @@ -52,13 +90,15 @@ private DelegatorMetadata( Address accountAddress, Address delegationPoolAddress, Address rewardAddress, - ImmutableSortedSet
delegatees) + IEnumerable
delegatees, + IEnumerable unbondingRefs) { DelegatorAddress = address; DelegatorAccountAddress = accountAddress; DelegationPoolAddress = delegationPoolAddress; RewardAddress = rewardAddress; - Delegatees = delegatees; + Delegatees = delegatees.ToImmutableSortedSet(); + UnbondingRefs = unbondingRefs.ToImmutableSortedSet(); } public Address DelegatorAddress { get; } @@ -76,11 +116,16 @@ public Address Address public ImmutableSortedSet
Delegatees { get; private set; } + public ImmutableSortedSet UnbondingRefs { get; private set; } + public List Bencoded => List.Empty + .Add(StateTypeName) + .Add(StateVersion) .Add(DelegationPoolAddress.Bencoded) .Add(RewardAddress.Bencoded) - .Add(new List(Delegatees.Select(a => a.Bencoded))); + .Add(new List(Delegatees.Select(a => a.Bencoded))) + .Add(new List(UnbondingRefs.Select(unbondingRef => unbondingRef.Bencoded))); IValue IBencodable.Bencoded => Bencoded; @@ -94,6 +139,16 @@ public void RemoveDelegatee(Address delegatee) Delegatees = Delegatees.Remove(delegatee); } + public void AddUnbondingRef(UnbondingRef unbondingRef) + { + UnbondingRefs = UnbondingRefs.Add(unbondingRef); + } + + public void RemoveUnbondingRef(UnbondingRef unbondingRef) + { + UnbondingRefs = UnbondingRefs.Remove(unbondingRef); + } + public override bool Equals(object? obj) => obj is IDelegator other && Equals(other); @@ -105,7 +160,8 @@ public virtual bool Equals(IDelegator? other) && DelegatorAccountAddress.Equals(delegator.DelegatorAccountAddress) && DelegationPoolAddress.Equals(delegator.DelegationPoolAddress) && RewardAddress.Equals(delegator.RewardAddress) - && Delegatees.SequenceEqual(delegator.Delegatees)); + && Delegatees.SequenceEqual(delegator.Delegatees) + && UnbondingRefs.SequenceEqual(delegator.UnbondingRefs)); public override int GetHashCode() => DelegatorAddress.GetHashCode(); diff --git a/Lib9c/Delegation/IDelegatee.cs b/Lib9c/Delegation/IDelegatee.cs index 463f9aaeb0..d0da6acfff 100644 --- a/Lib9c/Delegation/IDelegatee.cs +++ b/Lib9c/Delegation/IDelegatee.cs @@ -87,7 +87,5 @@ public interface IDelegatee Address CurrentLumpSumRewardsRecordAddress(); Address LumpSumRewardsRecordAddress(long height); - - event EventHandler? DelegationChanged; } } diff --git a/Lib9c/Delegation/IDelegationRepository.cs b/Lib9c/Delegation/IDelegationRepository.cs index cec2148a04..022ede2841 100644 --- a/Lib9c/Delegation/IDelegationRepository.cs +++ b/Lib9c/Delegation/IDelegationRepository.cs @@ -34,8 +34,6 @@ public interface IDelegationRepository RebondGrace GetUnlimitedRebondGrace(Address address); - UnbondingSet GetUnbondingSet(); - /// /// Get the current of the . /// @@ -83,8 +81,6 @@ public interface IDelegationRepository void SetRebondGrace(RebondGrace rebondGrace); - void SetUnbondingSet(UnbondingSet unbondingSet); - /// /// Set the of the . /// diff --git a/Lib9c/Delegation/IUnbonding.cs b/Lib9c/Delegation/IUnbonding.cs index fae5adbea5..f0bf059746 100644 --- a/Lib9c/Delegation/IUnbonding.cs +++ b/Lib9c/Delegation/IUnbonding.cs @@ -8,6 +8,10 @@ public interface IUnbonding { Address Address { get; } + Address DelegateeAddress { get; } + + Address DelegatorAddress { get; } + long LowestExpireHeight { get; } bool IsFull { get; } @@ -20,6 +24,6 @@ IUnbonding Slash( BigInteger slashFactor, long infractionHeight, long height, - out FungibleAssetValue? slashedFAV); + Address slashedPoolAddress); } } diff --git a/Lib9c/Delegation/RebondGrace.cs b/Lib9c/Delegation/RebondGrace.cs index ac158207d1..01a6a74fa4 100644 --- a/Lib9c/Delegation/RebondGrace.cs +++ b/Lib9c/Delegation/RebondGrace.cs @@ -18,10 +18,17 @@ private static readonly IComparer _entryComparer private readonly IDelegationRepository? _repository; - public RebondGrace(Address address, int maxEntries, IDelegationRepository? repository = null) + public RebondGrace( + Address address, + int maxEntries, + Address delegateeAddress, + Address delegatorAddress, + IDelegationRepository? repository = null) : this( address, maxEntries, + delegateeAddress, + delegatorAddress, ImmutableSortedDictionary>.Empty, repository) { @@ -36,7 +43,9 @@ public RebondGrace(Address address, int maxEntries, List bencoded, IDelegationRe : this( address, maxEntries, - bencoded.Select(kv => kv is List list + new Address(bencoded[0]), + new Address(bencoded[1]), + ((List)bencoded[2]).Select(kv => kv is List list ? new KeyValuePair>( (Integer)list[0], ((List)list[1]).Select(e => new UnbondingEntry(e)).ToImmutableList()) @@ -50,9 +59,16 @@ public RebondGrace(Address address, int maxEntries, List bencoded, IDelegationRe public RebondGrace( Address address, int maxEntries, + Address delegateeAddress, + Address delegatorAddress, IEnumerable entries, IDelegationRepository? repository = null) - : this(address, maxEntries, repository) + : this( + address, + maxEntries, + delegateeAddress, + delegatorAddress, + repository) { foreach (var entry in entries) { @@ -63,6 +79,8 @@ public RebondGrace( private RebondGrace( Address address, int maxEntries, + Address delegateeAddress, + Address delegatorAddress, ImmutableSortedDictionary> entries, IDelegationRepository? repository) { @@ -77,6 +95,8 @@ private RebondGrace( Address = address; MaxEntries = maxEntries; Entries = entries; + DelegateeAddress = delegateeAddress; + DelegatorAddress = delegatorAddress; _repository = repository; } @@ -90,7 +110,9 @@ private RebondGrace( public IDelegationRepository? Repository => _repository; - public long LowestExpireHeight => Entries.First().Key; + public long LowestExpireHeight => IsEmpty + ? -1 + : Entries.First().Key; public bool IsFull => Entries.Values.Sum(e => e.Count) >= MaxEntries; @@ -103,11 +125,14 @@ public ImmutableArray FlattenedEntries => Entries.Values.SelectMany(e => e).ToImmutableArray(); public List Bencoded - => new List( - Entries.Select( - sortedDict => new List( - (Integer)sortedDict.Key, - new List(sortedDict.Value.Select(e => e.Bencoded))))); + => List.Empty + .Add(DelegateeAddress.Bencoded) + .Add(DelegatorAddress.Bencoded) + .Add(new List( + Entries.Select( + sortedDict => new List( + (Integer)sortedDict.Key, + new List(sortedDict.Value.Select(e => e.Bencoded)))))); IValue IBencodable.Bencoded => Bencoded; @@ -145,8 +170,9 @@ public RebondGrace Slash( BigInteger slashFactor, long infractionHeight, long height, - out FungibleAssetValue? slashedFAV) + Address slashedPoolAddress) { + // TODO: Extract common logic to abstract class CannotMutateRelationsWithoutRepository(); var slashed = new SortedDictionary(); @@ -173,13 +199,23 @@ public RebondGrace Slash( updatedEntries = Entries.SetItem(expireHeight, slashedEntries); } - slashedFAV = null; foreach (var (address, slashedEach) in slashed) { - var delegatee = _repository!.GetDelegatee(address); - var delegator = _repository!.GetDelegator(DelegatorAddress); + var delegatee = Repository!.GetDelegatee(address); + var delegator = Repository!.GetDelegator(DelegatorAddress); delegatee.Unbond(delegator, delegatee.ShareFromFAV(slashedEach), height); - slashedFAV = slashedFAV.HasValue ? slashedFAV + slashedEach : slashedEach; + + var delegationBalance = Repository!.GetBalance(delegatee.DelegationPoolAddress, slashedEach.Currency); + var slashAmount = slashedEach; + if (delegationBalance < slashedEach) + { + slashAmount = delegationBalance; + } + + if (slashAmount > slashedEach.Currency * 0) + { + Repository.TransferAsset(delegatee.DelegationPoolAddress, slashedPoolAddress, slashAmount); + } } @@ -190,8 +226,8 @@ IUnbonding IUnbonding.Slash( BigInteger slashFactor, long infractionHeight, long height, - out FungibleAssetValue? slashedFAV) - => Slash(slashFactor, infractionHeight, height, out slashedFAV); + Address slashedPoolAddress) + => Slash(slashFactor, infractionHeight, height, slashedPoolAddress); public override bool Equals(object? obj) => obj is RebondGrace other && Equals(other); @@ -246,7 +282,7 @@ private RebondGrace AddEntry(UnbondingEntry entry) private RebondGrace UpdateEntries( ImmutableSortedDictionary> entries) - => new RebondGrace(Address, MaxEntries, entries, _repository); + => new RebondGrace(Address, MaxEntries, DelegateeAddress, DelegatorAddress, entries, _repository); private void CannotMutateRelationsWithoutRepository() { diff --git a/Lib9c/Delegation/UnbondLockIn.cs b/Lib9c/Delegation/UnbondLockIn.cs index 22987f5cac..c337f8da78 100644 --- a/Lib9c/Delegation/UnbondLockIn.cs +++ b/Lib9c/Delegation/UnbondLockIn.cs @@ -8,7 +8,6 @@ using Bencodex.Types; using Libplanet.Crypto; using Libplanet.Types.Assets; -using Nekoyume.Model.Stake; namespace Nekoyume.Delegation { @@ -113,10 +112,14 @@ private UnbondLockIn( public Address DelegatorAddress { get; } + public IDelegationRepository? Repository => _repository; + // TODO: Use better custom collection type public ImmutableSortedDictionary> Entries { get; } - public long LowestExpireHeight => Entries.First().Key; + public long LowestExpireHeight => IsEmpty + ? -1 + : Entries.First().Key; public bool IsFull => Entries.Values.Sum(e => e.Count) >= MaxEntries; @@ -170,15 +173,12 @@ public UnbondLockIn Release(long height, out FungibleAssetValue? releasedFAV) if (releasedFAV.HasValue) { - if (DelegateeAddress != Addresses.NonValidatorDelegatee) - { - var delegateeMetadata = _repository!.GetDelegateeMetadata(DelegateeAddress); - var delegatorMetadata = _repository.GetDelegatorMetadata(DelegatorAddress); - _repository!.TransferAsset( - delegateeMetadata.DelegationPoolAddress, - delegatorMetadata.DelegationPoolAddress, - releasedFAV.Value); - } + var delegateeMetadata = _repository!.GetDelegateeMetadata(DelegateeAddress); + var delegatorMetadata = _repository.GetDelegatorMetadata(DelegatorAddress); + _repository!.TransferAsset( + delegateeMetadata.DelegationPoolAddress, + delegatorMetadata.DelegationPoolAddress, + releasedFAV.Value); } return UpdateEntries(updatedEntries); @@ -190,9 +190,12 @@ public UnbondLockIn Slash( BigInteger slashFactor, long infractionHeight, long height, - out FungibleAssetValue? slashedFAV) + Address slashedPoolAddress) { - slashedFAV = null; + // TODO: Extract common logic to abstract class + CannotMutateRelationsWithoutRepository(); + + var slashed = new SortedDictionary(); var updatedEntries = Entries; var entriesToSlash = Entries.TakeWhile(e => e.Key >= infractionHeight); foreach (var (expireHeight, entries) in entriesToSlash) @@ -203,14 +206,37 @@ public UnbondLockIn Slash( var slashedEntry = entry.Slash(slashFactor, infractionHeight, out var slashedSingle); int index = slashedEntries.BinarySearch(slashedEntry, _entryComparer); slashedEntries = slashedEntries.Insert(index < 0 ? ~index : index, slashedEntry); - slashedFAV = slashedFAV.HasValue - ? slashedFAV.Value + slashedSingle - : slashedSingle; + if (slashed.TryGetValue(entry.UnbondeeAddress, out var value)) + { + slashed[entry.UnbondeeAddress] = value + slashedSingle; + } + else + { + slashed[entry.UnbondeeAddress] = slashedSingle; + } } updatedEntries = Entries.SetItem(expireHeight, slashedEntries); } + foreach (var (address, slashedEach) in slashed) + { + var delegatee = Repository!.GetDelegatee(address); + var delegator = Repository!.GetDelegator(DelegatorAddress); + + var delegationBalance = Repository!.GetBalance(delegatee.DelegationPoolAddress, slashedEach.Currency); + var slashAmount = slashedEach; + if (delegationBalance < slashedEach) + { + slashAmount = delegationBalance; + } + + if (slashAmount > slashedEach.Currency * 0) + { + Repository.TransferAsset(delegatee.DelegationPoolAddress, slashedPoolAddress, slashAmount); + } + } + return UpdateEntries(updatedEntries); } @@ -218,8 +244,8 @@ IUnbonding IUnbonding.Slash( BigInteger slashFactor, long infractionHeight, long height, - out FungibleAssetValue? slashedFAV) - => Slash(slashFactor, infractionHeight, height, out slashedFAV); + Address slashedPoolAddress) + => Slash(slashFactor, infractionHeight, height, slashedPoolAddress); public override bool Equals(object? obj) => obj is UnbondLockIn other && Equals(other); diff --git a/Lib9c/Delegation/UnbondingSet.cs b/Lib9c/Delegation/UnbondingSet.cs deleted file mode 100644 index 98a4b53fd5..0000000000 --- a/Lib9c/Delegation/UnbondingSet.cs +++ /dev/null @@ -1,173 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Bencodex; -using Bencodex.Types; -using Libplanet.Crypto; - -namespace Nekoyume.Delegation -{ - public sealed class UnbondingSet : IBencodable - { - private readonly IDelegationRepository _repository; - private ImmutableSortedDictionary _lowestExpireHeights; - - public UnbondingSet(IDelegationRepository repository) - : this( - ImmutableSortedDictionary>.Empty, - ImmutableSortedDictionary.Empty, - repository) - { - } - - public UnbondingSet(IValue bencoded, IDelegationRepository repository) - : this((List)bencoded, repository) - { - } - - public UnbondingSet(List bencoded, IDelegationRepository repository) - : this( - ((List)bencoded[0]).Select( - kv => new KeyValuePair>( - (Integer)((List)kv)[0], - ((List)((List)kv)[1]).Select(a => new UnbondingRef(a)).ToImmutableSortedSet())) - .ToImmutableSortedDictionary(), - ((List)bencoded[1]).Select( - kv => new KeyValuePair( - new UnbondingRef(((List)kv)[0]), - (Integer)((List)kv)[1])) - .ToImmutableSortedDictionary(), - repository) - { - } - - private UnbondingSet( - ImmutableSortedDictionary> unbondings, - ImmutableSortedDictionary lowestExpireHeights, - IDelegationRepository repository) - { - UnbondingRefs = unbondings; - _lowestExpireHeights = lowestExpireHeights; - _repository = repository; - } - - public static Address Address => new Address( - ImmutableArray.Create( - 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55)); - - public ImmutableSortedDictionary> UnbondingRefs { get; } - - public ImmutableArray FlattenedUnbondingRefs - => UnbondingRefs.Values.SelectMany(e => e).ToImmutableArray(); - - public IDelegationRepository Repository => _repository; - - public List Bencoded - => List.Empty - .Add(new List( - UnbondingRefs.Select( - sortedDict => new List( - (Integer)sortedDict.Key, - new List(sortedDict.Value.Select(a => a.Bencoded)))))) - .Add(new List( - _lowestExpireHeights.Select( - sortedDict => new List( - sortedDict.Key.Bencoded, - (Integer)sortedDict.Value)))); - - IValue IBencodable.Bencoded => Bencoded; - - public bool IsEmpty => UnbondingRefs.IsEmpty; - - public ImmutableArray UnbondingsToRelease(long height) - => UnbondingRefs - .TakeWhile(kv => kv.Key <= height) - .SelectMany(kv => kv.Value) - .Select(unbondingRef => UnbondingFactory.GetUnbondingFromRef(unbondingRef, _repository)) - .ToImmutableArray(); - - public UnbondingSet SetUnbondings(IEnumerable unbondings) - { - UnbondingSet result = this; - foreach (var unbonding in unbondings) - { - result = SetUnbonding(unbonding); - } - - return result; - } - - public UnbondingSet SetUnbonding(IUnbonding unbonding) - { - if (unbonding.IsEmpty) - { - try - { - return RemoveUnbonding(unbonding); - } - catch (ArgumentException) - { - return this; - } - } - - UnbondingRef unbondigRef = UnbondingFactory.ToReference(unbonding); - - if (_lowestExpireHeights.TryGetValue(unbondigRef, out var lowestExpireHeight)) - { - if (lowestExpireHeight == unbonding.LowestExpireHeight) - { - return this; - } - - var refs = UnbondingRefs[lowestExpireHeight]; - return new UnbondingSet( - UnbondingRefs.SetItem( - unbonding.LowestExpireHeight, - refs.Add(unbondigRef)), - _lowestExpireHeights.SetItem( - unbondigRef, unbonding.LowestExpireHeight), - _repository); - } - - return new UnbondingSet( - UnbondingRefs.SetItem( - unbonding.LowestExpireHeight, - ImmutableSortedSet.Empty.Add(unbondigRef)), - _lowestExpireHeights.SetItem( - unbondigRef, unbonding.LowestExpireHeight), - _repository); - } - - private UnbondingSet RemoveUnbonding(IUnbonding unbonding) - { - UnbondingRef unbondigRef = UnbondingFactory.ToReference(unbonding); - - if (_lowestExpireHeights.TryGetValue(unbondigRef, out var expireHeight) - && UnbondingRefs.TryGetValue(expireHeight, out var refs)) - { - refs = refs.Remove(unbondigRef); - - if (refs.IsEmpty) - { - return new UnbondingSet( - UnbondingRefs.Remove(expireHeight), - _lowestExpireHeights.Remove(unbondigRef), - _repository); - } - - return new UnbondingSet( - UnbondingRefs.SetItem(expireHeight, refs), - _lowestExpireHeights.Remove(unbondigRef), - _repository); - } - else - { - throw new ArgumentException("The address is not in the unbonding set."); - } - } - } -} diff --git a/Lib9c/Model/Guild/GuildDelegatee.cs b/Lib9c/Model/Guild/GuildDelegatee.cs index 1a852fec5a..bd3c78e08e 100644 --- a/Lib9c/Model/Guild/GuildDelegatee.cs +++ b/Lib9c/Model/Guild/GuildDelegatee.cs @@ -26,8 +26,8 @@ public GuildDelegatee( rewardRemainderPoolAddress: Addresses.CommunityPool, slashedPoolAddress: Addresses.CommunityPool, unbondingPeriod: ValidatorDelegatee.ValidatorUnbondingPeriod, - maxUnbondLockInEntries: ValidatorDelegatee.ValidatorMaxUnbondLockInEntries, - maxRebondGraceEntries: ValidatorDelegatee.ValidatorMaxRebondGraceEntries, + maxUnbondLockInEntries: GuildMaxUnbondLockInEntries, + maxRebondGraceEntries: GuildMaxRebondGraceEntries, repository: repository) { } @@ -39,8 +39,15 @@ public GuildDelegatee( address: address, repository: repository) { + Metadata.UnbondingPeriod = ValidatorDelegatee.ValidatorUnbondingPeriod; + Metadata.MaxUnbondLockInEntries = GuildMaxUnbondLockInEntries; + Metadata.MaxRebondGraceEntries = GuildMaxRebondGraceEntries; } + public static int GuildMaxUnbondLockInEntries => 1; + + public static int GuildMaxRebondGraceEntries => 1; + public override void Slash(BigInteger slashFactor, long infractionHeight, long height) { FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem); diff --git a/Lib9c/Model/Guild/GuildDelegator.cs b/Lib9c/Model/Guild/GuildDelegator.cs index d36107a240..ee24ec6998 100644 --- a/Lib9c/Model/Guild/GuildDelegator.cs +++ b/Lib9c/Model/Guild/GuildDelegator.cs @@ -1,7 +1,14 @@ #nullable enable using System; +using Lib9c; +using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Delegation; +using Nekoyume.Model.Stake; +using Nekoyume.Module; +using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; namespace Nekoyume.Model.Guild { @@ -28,6 +35,45 @@ public GuildDelegator( { } + protected override void OnUnbondingReleased(long height, IUnbonding releasedUnbonding, FungibleAssetValue? releasedFAV) + { + if (releasedUnbonding is UnbondLockIn unbondLockIn) + { + if (Repository.GetJoinedGuild(new AgentAddress(unbondLockIn.DelegatorAddress)) is null) + { + return; + } + + Unstake(unbondLockIn, releasedFAV); + } + } + + private void Unstake(UnbondLockIn unbondLockIn, FungibleAssetValue? releasedFAV) + { + if (releasedFAV is not FungibleAssetValue gg + || !gg.Currency.Equals(Currencies.GuildGold) + || gg.Sign < 1) + { + return; + } + + var agentAddress = new AgentAddress(unbondLockIn.DelegatorAddress); + var goldCurrency = Repository.World.GetGoldCurrency(); + var stakeStateAddress = StakeState.DeriveAddress(agentAddress); + var (ncg, _) = GuildModule.ConvertCurrency(gg, goldCurrency); + Repository.TransferAsset( + stakeStateAddress, agentAddress, ncg); + Repository.UpdateWorld( + Repository.World.BurnAsset(Repository.ActionContext, stakeStateAddress, gg)); + + var balanceGap = Repository.World.GetBalance(stakeStateAddress, goldCurrency) + - (Repository.World.GetStaked(agentAddress) + FungibleAssetValue.FromRawValue(goldCurrency, 1)); + if (balanceGap.Sign > 0) + { + Repository.TransferAsset(stakeStateAddress, Addresses.CommunityPool, balanceGap); + } + } + public bool Equals(GuildDelegator? other) => Metadata.Equals(other?.Metadata); } diff --git a/Lib9c/Module/Guild/GuildModule.cs b/Lib9c/Module/Guild/GuildModule.cs index ec1687a2e7..7b636810ec 100644 --- a/Lib9c/Module/Guild/GuildModule.cs +++ b/Lib9c/Module/Guild/GuildModule.cs @@ -1,11 +1,20 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using Lib9c; using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Evidence; +using Libplanet.Types.Tx; using Nekoyume.Extensions; using Nekoyume.Model.Guild; +using Nekoyume.Model.Stake; using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TypedAddress; using Nekoyume.ValidatorDelegation; @@ -94,9 +103,8 @@ public static GuildRepository RemoveGuild( throw new InvalidOperationException("There are remained participants in the guild."); } - var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); - var validatorDelegatee = validatorRepository.GetDelegatee(guild.ValidatorAddress); - var bond = validatorRepository.GetBond(validatorDelegatee, guild.Address); + var delegatee = repository.GetDelegatee(guild.ValidatorAddress); + var bond = repository.GetBond(delegatee, signer); if (bond.Share > 0) { throw new InvalidOperationException("The signer has a bond with the validator."); @@ -111,5 +119,85 @@ public static GuildRepository RemoveGuild( return repository; } + + public static (BigInteger Share, BigInteger TotalShare, FungibleAssetValue TotalDelegated) GetDelegationInfo( + this GuildRepository repository, Address agentAddress) + { + var guildDelegator = repository.GetDelegator(agentAddress); + var validatorAddress = guildDelegator.Delegatees.Single(); + var guildDelegatee = repository.GetDelegatee(validatorAddress); + var share = repository.GetBond(guildDelegatee, agentAddress).Share; + return (share, guildDelegatee.TotalShares, guildDelegatee.TotalDelegated); + } + + public static (BigInteger Share, BigInteger TotalShare, FungibleAssetValue TotalDelegated) GetDelegationInfo( + this IWorld world, Address agentAddress) + => new GuildRepository(world, new HallowActionContext()).GetDelegationInfo(agentAddress); + + public static (BigInteger Share, BigInteger TotalShare, FungibleAssetValue TotalDelegated) GetDelegationInfo( + this IWorldState worldState, Address agentAddress) + => GetDelegationInfo(new World(worldState), agentAddress); + + public static FungibleAssetValue FAVFromShare(BigInteger share, BigInteger totalShares, FungibleAssetValue totalDelegated) + => totalShares == share + ? totalDelegated + : (totalDelegated * share).DivRem(totalShares).Quotient; + + public static FungibleAssetValue GetDelegated(this IWorld world, Address agentAddress) + { + var delegationInfo = world.GetDelegationInfo(agentAddress); + return FAVFromShare(delegationInfo.Share, delegationInfo.TotalShare, delegationInfo.TotalDelegated); + } + + public static FungibleAssetValue GetStaked(this IWorld world, Address agentAddress) + { + var stakedGuildGold = world.GetBalance(StakeState.DeriveAddress(agentAddress), Currencies.GuildGold); + + try + { + stakedGuildGold += GetDelegated(world, agentAddress); + } + catch (InvalidOperationException) + { + } + + return ConvertCurrency(stakedGuildGold, world.GetGoldCurrency()).TargetFAV; + } + + public static (FungibleAssetValue TargetFAV, FungibleAssetValue Remainder) + ConvertCurrency(FungibleAssetValue sourceFAV, Currency targetCurrency) + { + var sourceCurrency = sourceFAV.Currency; + if (targetCurrency.DecimalPlaces < sourceCurrency.DecimalPlaces) + { + var d = BigInteger.Pow(10, sourceCurrency.DecimalPlaces - targetCurrency.DecimalPlaces); + var value = FungibleAssetValue.FromRawValue(targetCurrency, sourceFAV.RawValue / d); + var fav2 = FungibleAssetValue.FromRawValue(sourceCurrency, value.RawValue * d); + return (value, sourceFAV - fav2); + } + else + { + var d = BigInteger.Pow(10, targetCurrency.DecimalPlaces - sourceCurrency.DecimalPlaces); + var value = FungibleAssetValue.FromRawValue(targetCurrency, sourceFAV.RawValue * d); + return (value, targetCurrency * 0); + } + } + + private class HallowActionContext : IActionContext + { + public Address Signer => throw new NotImplementedException(); + public TxId? TxId => throw new NotImplementedException(); + public Address Miner => throw new NotImplementedException(); + public long BlockIndex => throw new NotImplementedException(); + public int BlockProtocolVersion => throw new NotImplementedException(); + public IWorld PreviousState => throw new NotImplementedException(); + public bool IsPolicyAction => throw new NotImplementedException(); + public IReadOnlyList Txs => throw new NotImplementedException(); + public IReadOnlyList Evidence => throw new NotImplementedException(); + public BlockCommit LastCommit => throw new NotImplementedException(); + public int RandomSeed => throw new NotImplementedException(); + public FungibleAssetValue? MaxGasPrice => throw new NotImplementedException(); + public IRandom GetRandom() => throw new NotImplementedException(); + } } } diff --git a/Lib9c/Module/Guild/GuildParticipantModule.cs b/Lib9c/Module/Guild/GuildParticipantModule.cs index 085b5bec56..f39d90e47f 100644 --- a/Lib9c/Module/Guild/GuildParticipantModule.cs +++ b/Lib9c/Module/Guild/GuildParticipantModule.cs @@ -1,11 +1,8 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; -using System.Numerics; using Bencodex.Types; using Lib9c; -using Libplanet.Types.Assets; -using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Model.Guild; using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TypedAddress; @@ -117,9 +114,8 @@ public static GuildRepository LeaveGuild( var height = repository.ActionContext.BlockIndex; var guildParticipant = repository.GetGuildParticipant(agentAddress); - var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); - var validatorDelegatee = validatorRepository.GetDelegatee(guild.ValidatorAddress); - var bond = validatorRepository.GetBond(validatorDelegatee, guild.Address); + var delegatee = repository.GetDelegatee(guild.ValidatorAddress); + var bond = repository.GetBond(delegatee, agentAddress); var share = bond.Share; if (bond.Share > 0) diff --git a/Lib9c/SerializeKeys.cs b/Lib9c/SerializeKeys.cs index 06234ebd7d..80312f0f7b 100644 --- a/Lib9c/SerializeKeys.cs +++ b/Lib9c/SerializeKeys.cs @@ -106,6 +106,7 @@ public static class SerializeKeys public const string CancellableBlockIndexKey = "cbi"; public const string AchievementsKey = "ach"; public const string AmountKey = "am"; + public const string StakeAvatarAddressKey = "saa"; // State public const string AddressKey = "a"; diff --git a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs index 92db73c64e..83091a65d3 100644 --- a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs +++ b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs @@ -10,7 +10,6 @@ using Libplanet.Types.Assets; using Libplanet.Types.Consensus; using Nekoyume.Delegation; -using Nekoyume.Model.State; namespace Nekoyume.ValidatorDelegation { @@ -55,9 +54,6 @@ public ValidatorDelegatee( IsActive = false; CommissionPercentage = commissionPercentage; CommissionPercentageLastUpdateHeight = creationHeight; - DelegationChanged += OnDelegationChanged; - Enjailed += OnEnjailed; - Unjailed += OnUnjailed; } public ValidatorDelegatee( @@ -83,19 +79,18 @@ public ValidatorDelegatee( IsActive = (Bencodex.Types.Boolean)bencoded[1]; CommissionPercentage = (Integer)bencoded[2]; CommissionPercentageLastUpdateHeight = (Integer)bencoded[3]; - DelegationChanged += OnDelegationChanged; - Enjailed += OnEnjailed; - Unjailed += OnUnjailed; + Metadata.UnbondingPeriod = ValidatorUnbondingPeriod; + Metadata.MaxUnbondLockInEntries = ValidatorMaxUnbondLockInEntries; + Metadata.MaxRebondGraceEntries = ValidatorMaxRebondGraceEntries; } public static Currency ValidatorDelegationCurrency => Currencies.GuildGold; - // TODO: [MigrateGuild] Change unbonding period after migration. - public static long ValidatorUnbondingPeriod => LegacyStakeState.LockupInterval; + public static long ValidatorUnbondingPeriod => 75600L; - public static int ValidatorMaxUnbondLockInEntries => 2; + public static int ValidatorMaxUnbondLockInEntries => 0; - public static int ValidatorMaxRebondGraceEntries => 2; + public static int ValidatorMaxRebondGraceEntries => 0; public static BigInteger BaseProposerRewardPercentage => 1; @@ -244,7 +239,7 @@ public void Deactivate() } } - public void OnDelegationChanged(object? sender, long height) + protected override void OnDelegationChanged(long height) { ValidatorRepository repository = Repository; @@ -269,13 +264,13 @@ public void OnDelegationChanged(object? sender, long height) } } - public void OnEnjailed(object? sender, EventArgs e) + protected override void OnEnjailed() { ValidatorRepository repository = Repository; repository.SetValidatorList(repository.GetValidatorList().RemoveValidator(Validator.PublicKey)); } - public void OnUnjailed(object? sender, EventArgs e) + protected override void OnUnjailed() { ValidatorRepository repository = Repository; repository.SetValidatorList(repository.GetValidatorList().SetValidator(Validator)); diff --git a/integrations/javascript/@planetarium/lib9c/src/actions/stake.ts b/integrations/javascript/@planetarium/lib9c/src/actions/stake.ts index 2571b5339f..d4ceaae432 100644 --- a/integrations/javascript/@planetarium/lib9c/src/actions/stake.ts +++ b/integrations/javascript/@planetarium/lib9c/src/actions/stake.ts @@ -1,3 +1,4 @@ +import type { Address } from "@planetarium/account"; import { BencodexDictionary, type Dictionary } from "@planetarium/bencodex"; import { GameAction, type GameActionArgs } from "./common.js"; @@ -6,6 +7,7 @@ import { GameAction, type GameActionArgs } from "./common.js"; */ export type StakeArgs = { amount: bigint; + avatarAddress: Address | null; } & GameActionArgs; /** @@ -19,17 +21,29 @@ export class Stake extends GameAction { */ public readonly amount: bigint; + /** + * The address of avatar to claim reward. + */ + public readonly avatarAddress: Address | null; + /** * Create a new `Stake` action. * @param params The arguments of the `Stake` action. */ - constructor({ amount, id }: StakeArgs) { + constructor({ amount, avatarAddress, id }: StakeArgs) { super({ id }); this.amount = amount; + this.avatarAddress = avatarAddress || null; } protected plain_value_internal(): Dictionary { - return new BencodexDictionary([["am", this.amount]]); + if (this.avatarAddress == null) { + return new BencodexDictionary([["am", this.amount]]); + } + return new BencodexDictionary([ + ["am", this.amount], + ["saa", this.avatarAddress.toBytes()], + ]); } } diff --git a/integrations/javascript/@planetarium/lib9c/tests/actions/stake.test.ts b/integrations/javascript/@planetarium/lib9c/tests/actions/stake.test.ts index f9b309a72a..69a140d021 100644 --- a/integrations/javascript/@planetarium/lib9c/tests/actions/stake.test.ts +++ b/integrations/javascript/@planetarium/lib9c/tests/actions/stake.test.ts @@ -1,11 +1,17 @@ import { describe } from "vitest"; import { Stake } from "../../src/index.js"; import { runTests } from "./common.js"; +import { avatarAddress } from "./fixtures.js"; describe("Stake", () => { runTests("valid case", [ new Stake({ amount: 1n, + avatarAddress: avatarAddress, + }), + new Stake({ + amount: 1n, + avatarAddress: null, }), ]); });