diff --git a/.Lib9c.Tests/Action/Guild/Migration/FixToRefundFromNonValidatorTest.cs b/.Lib9c.Tests/Action/Guild/Migration/FixToRefundFromNonValidatorTest.cs new file mode 100644 index 0000000000..3f2a646d74 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/Migration/FixToRefundFromNonValidatorTest.cs @@ -0,0 +1,135 @@ +namespace Lib9c.Tests.Action.Guild.Migration +{ + using System; + using System.Collections.Generic; + using Lib9c.Tests.Fixtures.TableCSV.Stake; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Action.Guild.Migration; + using Nekoyume.Extensions; + using Nekoyume.Model.Stake; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData.Stake; + using Xunit; + + // TODO: Remove this test class after the migration is completed. + public class FixToRefundFromNonValidatorTest : GuildTestBase + { + private IWorld _world; + private Currency _ncg; + private Currency _gg; + private Address _agentAddress; + private Address _stakeAddress; + private Address _adminAddress; + + public FixToRefundFromNonValidatorTest() + { + _agentAddress = new PrivateKey().Address; + _adminAddress = new PrivateKey().Address; + _stakeAddress = StakeState.DeriveAddress(_agentAddress); + _ncg = World.GetGoldCurrency(); + _gg = Currencies.GuildGold; + var sheetsOverride = new Dictionary + { + { + "StakeRegularFixedRewardSheet_V1", + StakeRegularFixedRewardSheetFixtures.V1 + }, + { + "StakeRegularFixedRewardSheet_V2", + StakeRegularFixedRewardSheetFixtures.V2 + }, + { + "StakeRegularRewardSheet_V1", + StakeRegularRewardSheetFixtures.V1 + }, + { + "StakeRegularRewardSheet_V2", + StakeRegularRewardSheetFixtures.V2 + }, + { + nameof(StakePolicySheet), + StakePolicySheetFixtures.V2 + }, + }; + (_, _, _, _world) = InitializeUtil.InitializeStates( + agentAddr: _agentAddress, + sheetsOverride: sheetsOverride); + var stakePolicySheet = _world.GetSheet(); + var contract = new Contract(stakePolicySheet); + var adminState = new AdminState(_adminAddress, 100L); + var stakeState = new StakeState(new Contract(stakePolicySheet), 1L); + + _world = World + .SetLegacyState(Addresses.Admin, adminState.Serialize()) + .SetLegacyState(_stakeAddress, stakeState.Serialize()) + .MintAsset(new ActionContext { }, Addresses.NonValidatorDelegatee, _gg * 10000) + .MintAsset(new ActionContext { }, _stakeAddress, _ncg * 10) + .MintAsset(new ActionContext { }, _stakeAddress, _gg * 5); + } + + [Fact] + public void Execute() + { + var world = new FixToRefundFromNonValidator(new Address[] { _agentAddress }).Execute(new ActionContext + { + PreviousState = _world, + Signer = _adminAddress, + }); + + Assert.Equal(_gg * 10, world.GetBalance(_stakeAddress, _gg)); + Assert.Equal(_gg * (10000 - 5), world.GetBalance(Addresses.NonValidatorDelegatee, _gg)); + } + + [Fact] + public void AssertWhenExecutedByNonAdmin() + { + Assert.Throws(() => + { + new FixToRefundFromNonValidator(new Address[] { _agentAddress }).Execute(new ActionContext + { + PreviousState = _world, + Signer = new PrivateKey().Address, + BlockIndex = 2L, + }); + }); + } + + [Fact] + public void AssertWhenHasSufficientGG() + { + var world = _world + .MintAsset(new ActionContext { }, _stakeAddress, _gg * 5); + Assert.Throws(() => + { + new FixToRefundFromNonValidator(new Address[] { _agentAddress }).Execute(new ActionContext + { + PreviousState = world, + Signer = _adminAddress, + BlockIndex = 2L, + }); + }); + } + + [Fact] + public void AssertWhenLegacyStakeState() + { + var stakeState = new LegacyStakeState(_stakeAddress, 0L); + var world = _world.SetLegacyState(_stakeAddress, stakeState.Serialize()); + Assert.Throws(() => + { + new FixToRefundFromNonValidator(new Address[] { _agentAddress }).Execute(new ActionContext + { + PreviousState = World, + Signer = _adminAddress, + BlockIndex = 2L, + }); + }); + } + } +} diff --git a/Lib9c/Action/Guild/Migration/FixToRefundFromNonValidator.cs b/Lib9c/Action/Guild/Migration/FixToRefundFromNonValidator.cs new file mode 100644 index 0000000000..1f807cb93e --- /dev/null +++ b/Lib9c/Action/Guild/Migration/FixToRefundFromNonValidator.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using Bencodex.Types; +using Lib9c; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Types.Assets; +using Nekoyume.Model.Stake; +using Nekoyume.Model.State; +using Nekoyume.Module; +using Libplanet.Crypto; +using System.Linq; + +namespace Nekoyume.Action.Guild.Migration +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + /// + /// An action to fix refund from non-validator. + /// + [ActionType(TypeIdentifier)] + public class FixToRefundFromNonValidator : ActionBase + { + public const string TypeIdentifier = "fix_to_refund_from_non_validator"; + + private const string TargetsKey = "t"; + + public List
Targets { get; private set; } + + public FixToRefundFromNonValidator() + { + } + + public FixToRefundFromNonValidator(IEnumerable
targets) + { + Targets = targets.ToList(); + } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Dictionary.Empty + .Add(TargetsKey, new List(Targets.Select(t => t.Bencoded)))); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Dictionary values || + !values.TryGetValue((Text)TargetsKey, out var rawTarget) || + rawTarget is not List targets) + { + throw new InvalidCastException(); + } + + Targets = targets.Select(t => new Address(t)).ToList(); + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + 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); + } + + foreach (var target in Targets) + { + world = RefundFromNonValidator(context, world, target); + } + + return world; + } + + private IWorld RefundFromNonValidator(IActionContext context, IWorld world, Address target) + { + var stakeStateAddress = StakeState.DeriveAddress(target); + + if (!world.TryGetStakeState(target, out var stakeState) + || stakeState.StateVersion != 3) + { + throw new InvalidOperationException( + "Target is not valid for refunding from non-validator."); + } + + var ncgStaked = world.GetBalance(stakeStateAddress, world.GetGoldCurrency()); + var ggStaked = world.GetBalance(stakeStateAddress, Currencies.GuildGold); + + var requiredGG = GetGuildCoinFromNCG(ncgStaked) - ggStaked; + + if (requiredGG.Sign != 1) + { + throw new InvalidOperationException( + "Target has sufficient amount of guild gold."); + } + + return world.TransferAsset(context, Addresses.NonValidatorDelegatee, stakeStateAddress, requiredGG); + } + + private static FungibleAssetValue GetGuildCoinFromNCG(FungibleAssetValue balance) + { + return FungibleAssetValue.Parse(Currencies.GuildGold, + balance.GetQuantityString(true)); + } + } +}