diff --git a/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj b/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj
index 059f555c09..e5b1dba72c 100644
--- a/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj
+++ b/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj
@@ -22,6 +22,7 @@
+
diff --git a/.Lib9c.Miner.Tests/ProposerTest.cs b/.Lib9c.Miner.Tests/ProposerTest.cs
new file mode 100644
index 0000000000..aae4ba2857
--- /dev/null
+++ b/.Lib9c.Miner.Tests/ProposerTest.cs
@@ -0,0 +1,145 @@
+using System.Collections.Immutable;
+using Lib9c.Renderers;
+using Libplanet.Action;
+using Libplanet.Blockchain.Policies;
+using Libplanet.Blockchain;
+using Libplanet.Crypto;
+using Libplanet.Store;
+using Libplanet.Store.Trie;
+using Libplanet.Types.Assets;
+using Libplanet.Types.Blocks;
+using Libplanet.Types.Consensus;
+using Libplanet.Types.Tx;
+using Nekoyume.Action;
+using Nekoyume.Action.Loader;
+using Nekoyume.Blockchain.Policy;
+using Nekoyume.Model.State;
+using System.Numerics;
+
+namespace Lib9c.Proposer.Tests
+{
+ public class ProposerTest
+ {
+ private readonly PrivateKey _admin;
+ private readonly PrivateKey _proposer;
+ private readonly BlockChain _blockChain;
+
+ public ProposerTest()
+ {
+ _admin = new PrivateKey();
+ _proposer = new PrivateKey();
+ var ncg = Currency.Uncapped("ncg", 2, null);
+ var policy = new DebugPolicy();
+ var actionTypeLoader = new NCActionLoader();
+ IStagePolicy stagePolicy = new VolatileStagePolicy();
+ var mint = new PrepareRewardAssets
+ {
+ RewardPoolAddress = _proposer.Address,
+ Assets = new List
+ {
+ 1 * Currencies.Mead,
+ },
+ };
+
+ var validatorSet = new ValidatorSet(
+ new List { new(_proposer.PublicKey, 10_000_000_000_000_000_000) });
+
+ var initializeStates = new InitializeStates(
+ validatorSet,
+ new RankingState0(),
+ new ShopState(),
+ new Dictionary(),
+ new GameConfigState(),
+ new RedeemCodeState(new Dictionary()),
+ new ActivatedAccountsState(),
+ new GoldCurrencyState(ncg),
+ new GoldDistribution[] { },
+ new PendingActivationState[] { });
+
+ List actions = new List
+ {
+ initializeStates,
+ };
+
+ var genesis = BlockChain.ProposeGenesisBlock(
+ privateKey: _proposer,
+ transactions: ImmutableList.Empty
+ .Add(Transaction.Create(
+ 0, _proposer, null, actions.ToPlainValues())),
+ timestamp: DateTimeOffset.MinValue);
+
+ var store = new MemoryStore();
+ var stateStore = new TrieStateStore(new MemoryKeyValueStore());
+
+ _blockChain = BlockChain.Create(
+ policy,
+ stagePolicy,
+ store,
+ stateStore,
+ genesis,
+ new ActionEvaluator(
+ policy.PolicyActionsRegistry,
+ stateStore,
+ new NCActionLoader()
+ ),
+ new[] { new BlockRenderer(), }
+ );
+ }
+
+ [Fact]
+ public void ProposeBlock()
+ {
+ Block block = _blockChain.ProposeBlock(_proposer);
+ _blockChain.Append(
+ block,
+ GenerateBlockCommit(
+ block,
+ _proposer,
+ 10_000_000_000_000_000_000));
+ }
+
+ [Fact]
+ public void AssertInvalidProposer()
+ {
+ Block block = _blockChain.ProposeBlock(_proposer);
+ Assert.Throws(() => _blockChain.Append(
+ block,
+ GenerateBlockCommit(
+ block,
+ new PrivateKey(),
+ 10_000_000_000_000_000_000)));
+ }
+
+ [Fact]
+ public void AssertInvalidPower()
+ {
+ Block block = _blockChain.ProposeBlock(_proposer);
+ Assert.Throws(() => _blockChain.Append(
+ block,
+ GenerateBlockCommit(
+ block,
+ _proposer,
+ 10_000_000_000_000_000)));
+ }
+
+ private BlockCommit? GenerateBlockCommit(
+ Block block, PrivateKey privateKey, BigInteger power)
+ {
+ return block.Index != 0
+ ? new BlockCommit(
+ block.Index,
+ 0,
+ block.Hash,
+ ImmutableArray.Create(
+ new VoteMetadata(
+ block.Index,
+ 0,
+ block.Hash,
+ DateTimeOffset.UtcNow,
+ privateKey.PublicKey,
+ power,
+ VoteFlag.PreCommit).Sign(privateKey)))
+ : null;
+ }
+ }
+}
diff --git a/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumValidatorTest.cs b/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumValidatorTest.cs
new file mode 100644
index 0000000000..cd15015166
--- /dev/null
+++ b/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumValidatorTest.cs
@@ -0,0 +1,116 @@
+namespace Lib9c.Tests.Action.Guild.Migration
+{
+ 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 Libplanet.Types.Consensus;
+ using Nekoyume.Action.Guild;
+ using Nekoyume.Action.Guild.Migration;
+ using Nekoyume.Action.Guild.Migration.LegacyModels;
+ using Nekoyume.Action.ValidatorDelegation;
+ using Nekoyume.Model.Guild;
+ using Nekoyume.Model.Stake;
+ using Nekoyume.TypedAddress;
+ using Nekoyume.ValidatorDelegation;
+ using Xunit;
+
+ // TODO: Remove this test class after the migration is completed.
+ public class MigratePlanetariumValidatorTest : GuildTestBase
+ {
+ [Fact]
+ public void Execute()
+ {
+ var guildAddress = AddressUtil.CreateGuildAddress();
+ var validatorKey = new PrivateKey().PublicKey;
+ var validatorAddress = validatorKey.Address;
+ var power = 10_000_000_000_000_000_000;
+ var guildGold = Currencies.GuildGold;
+ var delegated = FungibleAssetValue.FromRawValue(guildGold, power);
+
+ var world = EnsureLegacyPlanetariumValidator(
+ World, guildAddress, validatorKey, power);
+
+ var guildRepository = new GuildRepository(world, new ActionContext { });
+ var validatorRepository = new ValidatorRepository(world, new ActionContext { });
+ var guildDelegatee = guildRepository.GetGuildDelegatee(validatorAddress);
+ var validatorDelegatee = validatorRepository.GetValidatorDelegatee(validatorAddress);
+
+ Assert.False(validatorDelegatee.IsActive);
+ Assert.Equal(ValidatorDelegatee.InactiveDelegationPoolAddress, guildDelegatee.DelegationPoolAddress);
+ Assert.Equal(ValidatorDelegatee.InactiveDelegationPoolAddress, validatorDelegatee.DelegationPoolAddress);
+ Assert.Equal(delegated, world.GetBalance(ValidatorDelegatee.InactiveDelegationPoolAddress, Currencies.GuildGold));
+ Assert.Equal(guildGold * 0, world.GetBalance(ValidatorDelegatee.ActiveDelegationPoolAddress, Currencies.GuildGold));
+
+ var action = new MigratePlanetariumValidator();
+ var actionContext = new ActionContext
+ {
+ PreviousState = world,
+ Signer = new PrivateKey().Address,
+ };
+ world = action.Execute(actionContext);
+
+ guildRepository = new GuildRepository(world, new ActionContext { });
+ validatorRepository = new ValidatorRepository(world, new ActionContext { });
+ guildDelegatee = guildRepository.GetGuildDelegatee(validatorAddress);
+ validatorDelegatee = validatorRepository.GetValidatorDelegatee(validatorAddress);
+
+ Assert.True(validatorRepository.GetValidatorDelegatee(validatorAddress).IsActive);
+ Assert.Equal(ValidatorDelegatee.ActiveDelegationPoolAddress, guildDelegatee.DelegationPoolAddress);
+ Assert.Equal(ValidatorDelegatee.ActiveDelegationPoolAddress, validatorDelegatee.DelegationPoolAddress);
+ Assert.Equal(delegated, world.GetBalance(ValidatorDelegatee.ActiveDelegationPoolAddress, Currencies.GuildGold));
+ Assert.Equal(guildGold * 0, world.GetBalance(ValidatorDelegatee.InactiveDelegationPoolAddress, Currencies.GuildGold));
+
+ Assert.Throws(() =>
+ {
+ var actionContext = new ActionContext
+ {
+ PreviousState = world,
+ Signer = new PrivateKey().Address,
+ };
+
+ world = action.Execute(actionContext);
+ });
+ }
+
+ private static IWorld EnsureLegacyPlanetariumValidator(
+ IWorld world, GuildAddress guildAddress, PublicKey validatorKey, BigInteger power)
+ {
+ world = world.SetDelegationMigrationHeight(0L);
+
+ var toDelegate = FungibleAssetValue.FromRawValue(Currencies.GuildGold, power);
+ world = world
+ .MintAsset(
+ new ActionContext { },
+ StakeState.DeriveAddress(validatorKey.Address),
+ toDelegate)
+ .MintAsset(
+ new ActionContext { },
+ validatorKey.Address,
+ Currencies.Mead * 1);
+
+ world = new PromoteValidator(validatorKey, toDelegate).Execute(new ActionContext
+ {
+ PreviousState = world,
+ Signer = validatorKey.Address,
+ });
+
+ world = new MakeGuild(validatorKey.Address).Execute(new ActionContext
+ {
+ PreviousState = world,
+ Signer = GuildConfig.PlanetariumGuildOwner,
+ });
+
+ world = world.SetValidatorSet(new ValidatorSet(
+ new List
+ {
+ new Validator(validatorKey, power),
+ }));
+
+ return world;
+ }
+ }
+}
diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs
index 3173039718..27699ba63a 100644
--- a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs
+++ b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs
@@ -210,4 +210,50 @@ public void Execute_ToJailedValidator_ThenNothingHappens()
Assert.Equal(expectedJailed, actualJailed);
}
+
+ [Fact]
+ public void Execute_ByAbstain_ToJailedValidator_ThenNothingHappens()
+ {
+ // Given
+ var world = World;
+ var validatorKey = new PrivateKey();
+ var height = 1L;
+ var actionContext = new ActionContext();
+ world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++);
+ world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++);
+ world = EnsureJailedValidator(world, validatorKey, ref height);
+
+ var expectedRepository = new ValidatorRepository(world, actionContext);
+ var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address);
+ var expectedTotalDelegated = expectedDelegatee.TotalDelegated;
+
+ // When
+ for (var i = 0L; i <= AbstainHistory.MaxAbstainAllowance; i++)
+ {
+ var vote = CreateNullVote(validatorKey, height - 1);
+ var lastCommit = new BlockCommit(
+ height: height - 1,
+ round: 0,
+ blockHash: vote.BlockHash,
+ ImmutableArray.Create(vote));
+ var slashValidator = new SlashValidator();
+ actionContext = new ActionContext
+ {
+ PreviousState = world,
+ Signer = validatorKey.Address,
+ BlockIndex = height++,
+ LastCommit = lastCommit,
+ };
+ world = slashValidator.Execute(actionContext);
+ }
+
+ // Then
+ var actualRepisitory = new ValidatorRepository(world, actionContext);
+ var actualDelegatee = actualRepisitory.GetValidatorDelegatee(validatorKey.Address);
+ var actualTotalDelegated = actualDelegatee.TotalDelegated;
+
+ Assert.True(actualDelegatee.Jailed);
+ Assert.False(actualDelegatee.Tombstoned);
+ Assert.Equal(expectedTotalDelegated, actualTotalDelegated);
+ }
}
diff --git a/Directory.Build.props b/Directory.Build.props
index af6e2c7447..f18e8496d7 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 5.4.0
+ 5.4.1
diff --git a/Lib9c.Policy/Policy/DebugPolicy.cs b/Lib9c.Policy/Policy/DebugPolicy.cs
index e10765da2f..8a1f5d1fbd 100644
--- a/Lib9c.Policy/Policy/DebugPolicy.cs
+++ b/Lib9c.Policy/Policy/DebugPolicy.cs
@@ -5,6 +5,7 @@
using Libplanet.Types.Blocks;
using Libplanet.Types.Tx;
using Nekoyume.Action;
+using Nekoyume.Action.ValidatorDelegation;
namespace Nekoyume.Blockchain.Policy
{
@@ -14,7 +15,25 @@ public DebugPolicy()
{
}
- public IPolicyActionsRegistry PolicyActionsRegistry { get; } = new PolicyActionsRegistry();
+ public IPolicyActionsRegistry PolicyActionsRegistry { get; } =
+ new PolicyActionsRegistry(
+ beginBlockActions: new IAction[] {
+ new SlashValidator(),
+ new AllocateGuildReward(),
+ new AllocateReward(),
+ }.ToImmutableArray(),
+ endBlockActions: new IAction[] {
+ new UpdateValidators(),
+ new RecordProposer(),
+ new RewardGold(),
+ new ReleaseValidatorUnbondings(),
+ }.ToImmutableArray(),
+ beginTxActions: new IAction[] {
+ new Mortgage(),
+ }.ToImmutableArray(),
+ endTxActions: new IAction[] {
+ new Reward(), new Refund(),
+ }.ToImmutableArray());
public TxPolicyViolationException ValidateNextBlockTx(
BlockChain blockChain, Transaction transaction)
diff --git a/Lib9c.Proposer/Proposer.cs b/Lib9c.Proposer/Proposer.cs
index 65abac0838..0810c19713 100644
--- a/Lib9c.Proposer/Proposer.cs
+++ b/Lib9c.Proposer/Proposer.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Threading;
using Libplanet.Action;
+using Libplanet.Action.State;
using Libplanet.Blockchain;
using Libplanet.Crypto;
using Libplanet.Types.Blocks;
@@ -33,6 +34,16 @@ public class Proposer
block = _chain.ProposeBlock(
_privateKey,
lastCommit: lastCommit);
+
+ if (!(_chain.GetNextWorldState() is IWorldState worldState))
+ {
+ throw new InvalidOperationException(
+ "Failed to get next world state. Appending is not completed.");
+ }
+
+ var proposerPower = worldState.GetValidatorSet().GetValidatorsPower(
+ new List { _privateKey.PublicKey });
+
BlockCommit? commit = block.Index > 0
? new BlockCommit(
block.Index,
@@ -45,7 +56,7 @@ public class Proposer
block.Hash,
DateTimeOffset.UtcNow,
_privateKey.PublicKey,
- null,
+ proposerPower,
VoteFlag.PreCommit).Sign(_privateKey)))
: null;
_chain.Append(block, commit);
diff --git a/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs b/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs
index 4d2724c6f5..493282e363 100644
--- a/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs
+++ b/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs
@@ -1,4 +1,3 @@
-using System;
using Bencodex.Types;
using Libplanet.Action.State;
using Libplanet.Crypto;
@@ -23,11 +22,6 @@ public static readonly Address DelegationMigrationHeight
public static IWorld SetDelegationMigrationHeight(this IWorld world, long height)
{
- if (world.GetDelegationMigrationHeight() is long)
- {
- throw new InvalidOperationException("Cannot overwrite delegation migration index.");
- }
-
return world
.MutateAccount(
Addresses.Migration,
diff --git a/Lib9c/Action/Guild/Migration/MigrateDelegation.cs b/Lib9c/Action/Guild/Migration/MigrateDelegation.cs
index 65dbec9dde..14e77621ac 100644
--- a/Lib9c/Action/Guild/Migration/MigrateDelegation.cs
+++ b/Lib9c/Action/Guild/Migration/MigrateDelegation.cs
@@ -119,23 +119,28 @@ public override IWorld Execute(IActionContext context)
return repository.World;
}
- catch (FailedLoadStateException)
+ catch (Exception e)
{
- var pledgeAddress = ((Address)Target).GetPledgeAddress();
-
- // Patron contract structure:
- // [0] = PatronAddress
- // [1] = IsApproved
- // [2] = Mead amount to refill.
- if (!world.TryGetLegacyState(pledgeAddress, out List list) || list.Count < 3 ||
- list[0] is not Binary || list[0].ToAddress() != MeadConfig.PatronAddress ||
- list[1] is not Bencodex.Types.Boolean approved || !approved)
+ if (e is FailedLoadStateException || e is NullReferenceException)
{
- throw new GuildMigrationFailedException("Unexpected pledge structure.");
+ var pledgeAddress = ((Address)Target).GetPledgeAddress();
+
+ // Patron contract structure:
+ // [0] = PatronAddress
+ // [1] = IsApproved
+ // [2] = Mead amount to refill.
+ if (!world.TryGetLegacyState(pledgeAddress, out List list) || list.Count < 3 ||
+ list[0] is not Binary || list[0].ToAddress() != MeadConfig.PatronAddress ||
+ list[1] is not Bencodex.Types.Boolean approved || !approved)
+ {
+ throw new GuildMigrationFailedException("Unexpected pledge structure.");
+ }
+
+ repository.JoinGuild(planetariumGuildAddress, Target);
+ return repository.World;
}
- repository.JoinGuild(planetariumGuildAddress, Target);
- return repository.World;
+ throw;
}
}
}
diff --git a/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs b/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs
index 48d1ad2b9d..dfd5aaa531 100644
--- a/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs
+++ b/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs
@@ -3,6 +3,7 @@
using Libplanet.Action;
using Libplanet.Action.State;
using Nekoyume.Action.Guild.Migration.LegacyModels;
+using Nekoyume.Model.State;
namespace Nekoyume.Action.Guild.Migration
{
@@ -53,6 +54,16 @@ public override IWorld Execute(IActionContext context)
var world = context.PreviousState;
+ if (!TryGetAdminState(context, out AdminState adminState))
+ {
+ throw new InvalidOperationException("Couldn't find admin state");
+ }
+
+ if (context.Signer != adminState.AdminAddress)
+ {
+ throw new PermissionDeniedException(adminState, context.Signer);
+ }
+
return world.SetDelegationMigrationHeight(Height);
}
}
diff --git a/Lib9c/Action/Guild/Migration/MigratePlanetariumValidator.cs b/Lib9c/Action/Guild/Migration/MigratePlanetariumValidator.cs
new file mode 100644
index 0000000000..0a91936bf2
--- /dev/null
+++ b/Lib9c/Action/Guild/Migration/MigratePlanetariumValidator.cs
@@ -0,0 +1,83 @@
+using System;
+using Bencodex.Types;
+using Libplanet.Action;
+using Libplanet.Action.State;
+using Nekoyume.Model.Guild;
+using Nekoyume.Module.Guild;
+using Nekoyume.ValidatorDelegation;
+
+namespace Nekoyume.Action.Guild.Migration
+{
+ // TODO: [GuildMigration] Remove this class when the migration is done.
+ ///
+ /// An action to migrate the planetarium guild.
+ ///
+ [ActionType(TypeIdentifier)]
+ public class MigratePlanetariumValidator : ActionBase
+ {
+ public const string TypeIdentifier = "migrate_planetarium_validator";
+
+ public MigratePlanetariumValidator()
+ {
+ }
+
+ public override IValue PlainValue => Dictionary.Empty
+ .Add("type_id", TypeIdentifier)
+ .Add("values", Null.Value);
+
+ public override void LoadPlainValue(IValue plainValue)
+ {
+ if (plainValue is not Dictionary root ||
+ !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 guildRepository = new GuildRepository(world, context);
+ var guildAddress = guildRepository.GetJoinedGuild(GuildConfig.PlanetariumGuildOwner);
+ if (guildAddress is not { } planetariumGuildAddress)
+ {
+ throw new InvalidOperationException("The planetarium guild not exists");
+ }
+
+ var planetariumGuild = guildRepository.GetGuild(
+ planetariumGuildAddress);
+
+ var validatorRepository = new ValidatorRepository(guildRepository);
+ var validatorDelegatee = validatorRepository.GetValidatorDelegatee(
+ planetariumGuild.ValidatorAddress);
+
+ var validatorSet = world.GetValidatorSet();
+
+ if (!validatorSet.ContainsPublicKey(validatorDelegatee.PublicKey))
+ {
+ throw new InvalidOperationException(
+ "The planetarium validator is not in the validator set.");
+ }
+
+ if (validatorDelegatee.IsActive)
+ {
+ throw new InvalidOperationException(
+ "The planetarium validator is already active.");
+ }
+
+ validatorDelegatee.Activate();
+ validatorRepository.SetValidatorDelegatee(validatorDelegatee);
+
+ guildRepository.UpdateWorld(validatorRepository.World);
+ var guildDelegatee = guildRepository.GetGuildDelegatee(
+ planetariumGuild.ValidatorAddress);
+ guildDelegatee.Activate();
+ guildRepository.SetGuildDelgatee(guildDelegatee);
+
+ return guildRepository.World;
+ }
+ }
+}
diff --git a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs
index 5d9fc74550..e97424efc0 100644
--- a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs
+++ b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs
@@ -52,6 +52,11 @@ public override IWorld Execute(IActionContext context)
foreach (var abstain in abstainsToSlash)
{
var validatorDelegatee = repository.GetValidatorDelegatee(abstain.Address);
+ if (validatorDelegatee.Jailed)
+ {
+ continue;
+ }
+
validatorDelegatee.Slash(LivenessSlashFactor, context.BlockIndex, context.BlockIndex);
validatorDelegatee.Jail(context.BlockIndex + AbstainJailTime);
diff --git a/Lib9c/Delegation/Delegatee.cs b/Lib9c/Delegation/Delegatee.cs
index e18ae6d3e8..52db964657 100644
--- a/Lib9c/Delegation/Delegatee.cs
+++ b/Lib9c/Delegation/Delegatee.cs
@@ -295,7 +295,7 @@ record = record.AddLumpSumRewards(rewards);
Repository.SetLumpSumRewardsRecord(record);
}
- public void Slash(BigInteger slashFactor, long infractionHeight, long height)
+ public virtual void Slash(BigInteger slashFactor, long infractionHeight, long height)
{
FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem);
if (rem.Sign > 0)
diff --git a/Lib9c/Model/Guild/GuildDelegatee.cs b/Lib9c/Model/Guild/GuildDelegatee.cs
index 079fe52377..1ffedb2514 100644
--- a/Lib9c/Model/Guild/GuildDelegatee.cs
+++ b/Lib9c/Model/Guild/GuildDelegatee.cs
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.Numerics;
using Libplanet.Crypto;
using Libplanet.Types.Assets;
using Nekoyume.Delegation;
@@ -40,6 +41,23 @@ public GuildDelegatee(
{
}
+ public override void Slash(BigInteger slashFactor, long infractionHeight, long height)
+ {
+ FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem);
+ if (rem.Sign > 0)
+ {
+ slashed += FungibleAssetValue.FromRawValue(rem.Currency, 1);
+ }
+
+ if (slashed > Metadata.TotalDelegatedFAV)
+ {
+ slashed = Metadata.TotalDelegatedFAV;
+ }
+
+ Metadata.RemoveDelegatedFAV(slashed);
+ Repository.SetDelegateeMetadata(Metadata);
+ }
+
public void Activate()
{
Metadata.DelegationPoolAddress = ValidatorDelegatee.ActiveDelegationPoolAddress;
diff --git a/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs b/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs
index 13f792cbef..7581ed37f6 100644
--- a/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs
+++ b/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs
@@ -35,7 +35,7 @@ public static ValidatorDelegatee CreateValidatorDelegatee(
if (repository.TryGetValidatorDelegatee(publicKey.Address, out _))
{
- throw new InvalidOperationException("The signer already has a validator delegatee.");
+ throw new InvalidOperationException("The public key already has a validator delegatee.");
}
var validatorDelegatee = new ValidatorDelegatee(
diff --git a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs
index c3fa0d537b..7ebe20bea4 100644
--- a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs
+++ b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs
@@ -199,6 +199,11 @@ public void SetCommissionPercentage(BigInteger percentage, long height)
public void Activate()
{
+ if (IsActive)
+ {
+ throw new InvalidOperationException("The validator is already active.");
+ }
+
ValidatorRepository repository = (ValidatorRepository)Repository;
IsActive = true;
Metadata.DelegationPoolAddress = ActiveDelegationPoolAddress;
@@ -214,6 +219,11 @@ public void Activate()
public void Deactivate()
{
+ if (!IsActive)
+ {
+ throw new InvalidOperationException("The validator is already inactive.");
+ }
+
ValidatorRepository repository = (ValidatorRepository)Repository;
IsActive = false;
Metadata.DelegationPoolAddress = InactiveDelegationPoolAddress;