diff --git a/doc/icon_chainscore_api.md b/doc/icon_chainscore_api.md index ec1a3991c..0db729790 100644 --- a/doc/icon_chainscore_api.md +++ b/doc/icon_chainscore_api.md @@ -70,6 +70,7 @@ + [requestUnjail](#requestunjail) + [setPRepCountConfig](#setprepcountconfig) + [handleDoubleSignReport](#handledoublesignreport) + + [setBondRequirementRate](#setbondrequirementrate) - [BTP](#btp) * ReadOnly APIs + [getBTPNetworkTypeID](#getbtpnetworktypeid) @@ -770,7 +771,7 @@ def estimateUnstakeLockPeriod() -> dict: ### getPRepTerm -Returns information for the current term. +Returns the information about the current term. ``` def getPRepTerm() -> dict: @@ -778,26 +779,31 @@ def getPRepTerm() -> dict: *Returns:* -| Key | Value Type | Description | -|:-----------------|:--------------------------|:------------------------------------------------------------| -| blockHeight | int | latest block height when this request was processed | -| sequence | int | sequence number | -| startBlockHeight | int | start block height of the term | -| endBlockHeight | int | end block height of the term | -| totalSupply | int | total supply amount at `startBlockHeight` | -| preps | List\[[PRep](#prep)\] | Main/Sub P-Rep list at `startBlockHeight` | -| totalDelegated | int | total delegation amount of `preps` | -| totalPower | int | total power amount of `preps` | -| period | int | term period | -| rewardFund | [RewardFund](#rewardfund) | reward fund information for the term | -| bondRequirement | int | bondRequirement for the term | -| revision | int | revision for the term | -| isDecentralized | bool | `true` if network is decentralized | -| mainPRepCount | int | Main P-Reps count for the term | -| iissVersion | int | IISS version for the term | -| irep | int | (Optional. revision < 25) Irep for the term | -| rrep | int | (Optional. revision < 25) Rrep for the term | -| minimumBond | int | (Optional. revision >= 25) minimum bond amount for the term | +| Key | Value Type | Description | +|:--------------------|:--------------------------|:------------------------------------------------------------------------------------------------------------| +| blockHeight | int | latest block height when this request was processed | +| sequence | int | sequence number | +| startBlockHeight | int | start block height of the term | +| endBlockHeight | int | end block height of the term | +| totalSupply | int | total supply amount at `startBlockHeight` | +| preps | List\[[PRep](#prep)\] | Main/Sub P-Rep list at `startBlockHeight` | +| totalDelegated | int | total delegation amount of `preps` | +| totalPower | int | total power amount of `preps` | +| period | int | term period | +| rewardFund | [RewardFund](#rewardfund) | reward fund information for the term | +| bondRequirement | int | bond requirement rate ranging from 0 (0%) to 100 (100%) for the current term
removed after revision 28 | +| bondRequirementRate | int | bond requirement rate ranging from 0 (0%) to 10,000 (100%) for the current term
added after revision 28 | +| revision | int | revision for the term | +| isDecentralized | bool | `true` if network is decentralized | +| mainPRepCount | int | Main P-Reps count for the term | +| iissVersion | int | IISS version for the term | +| irep | int | (Optional. revision < 25) Irep for the term | +| rrep | int | (Optional. revision < 25) Rrep for the term | +| minimumBond | int | (Optional. revision >= 25) minimum bond amount for the term | + +* `bondRequirement` field is replaced with `bondRequirementRate` field after revision 28 +* 0 <= `bondRequirement` <= 100 (0: 0%, 100: 100%) +* 0 <= `bondRequirementRate` <= 10,000 (0: 0%, 10,000: 100%) *Revision:* 5 ~ @@ -850,24 +856,29 @@ def getNetworkInfo() -> dict: *Returns:* -| Key | Value Type | Description | -|:------------------|:--------------------------|:-------------------------------------------| -| mainPRepCount | int | Main P-Reps count | -| extraPRepCount | int | Extra Main P-Reps count | -| subPRepCount | int | Sub Main P-Reps count | -| iissVersion | int | IISS version | -| termPeriod | int | period of term | -| bondRequirement | int | bond requirement | -| lockMinMultiplier | int | multiplier for minimum unstake lock period | -| lockMaxMultiplier | int | multiplier for maximum unstake lock period | -| unstakeSlotMax | int | maximum unstakes count of a account | -| delegationSlotMax | int | maximum delegation count of a account | -| rewardFund | [RewardFund](#rewardfund) | reward fund information | -| totalStake | int | total stakes of ICONist | -| totalBonded | int | total bonded amount of P-Rep | -| totalDelegated | int | total delegated amount of P-Rep | -| totalPower | int | total power amount of P-Rep | -| preps | int | count of all P-Reps | +| Key | Value Type | Description | +|:--------------------|:--------------------------|:---------------------------------------------------------------------------------------| +| mainPRepCount | int | Main P-Reps count | +| extraPRepCount | int | Extra Main P-Reps count | +| subPRepCount | int | Sub Main P-Reps count | +| iissVersion | int | IISS version | +| termPeriod | int | period of term | +| bondRequirement | int | bond requirement rate ranging from 0 (0%) to 100 (100%)
removed after revision 28 | +| bondRequirementRate | int | bond requirement rate ranging from 0 (0%) to 10,000 (100%)
added after revision 28 | +| lockMinMultiplier | int | multiplier for minimum unstake lock period | +| lockMaxMultiplier | int | multiplier for maximum unstake lock period | +| unstakeSlotMax | int | maximum unstakes count of a account | +| delegationSlotMax | int | maximum delegation count of a account | +| rewardFund | [RewardFund](#rewardfund) | reward fund information | +| totalStake | int | total stakes of ICONist | +| totalBonded | int | total bonded amount of P-Rep | +| totalDelegated | int | total delegated amount of P-Rep | +| totalPower | int | total power amount of P-Rep | +| preps | int | count of all P-Reps | + +* `bondRequirement` field is replaced with `bondRequirementRate` field after revision 28 +* 0 <= `bondRequirement` <= 100 (0: 0%, 100: 100%) +* 0 <= `bondRequirementRate` <= 10,000 (0: 0%, 10,000: 100%) *Revision:* 13 ~ @@ -1546,6 +1557,36 @@ def DoubleSignReported(owner Address, blockHeight int, type: str) *Revision:* 25 ~ +### setBondRequirementRate + +* Update bondRequirementRate +* Governance Only + +``` +def setBondRequirementRate(rate: int) -> None: +``` + +*Parameters:* + +| Name | Type | Description | +|:-----|:-----|:--------------------------| +| rate | int | new bond requirement rate | + +* 0 <= `rate` <= 10,000 (0: 0%, 10,000: 100%) + +*Event Log:* + +``` +@eventlog(indexed=0) +def BondRequirementRateSet(rate: int) -> None: +``` + +| Name | Type | Description | +|:------|:--------|:--------------------------| +| rate | int | new bond requirement rate | + +*Revision:* 28 ~ + # BTP ## ReadOnly APIs @@ -1806,7 +1847,7 @@ The list of fields below is subject to change based on revisions | nodeAddress | Address | node Key for only consensus | | p2pEndpoint | str | network information used for connecting among P-Rep nodes | | penalty | int | [PENALTY_TYPE_ID](#penalty_type_id) | -| power | int | amount of power that a P-Rep receives from ICONist. (= min(`bonded`+`delegated`, `bonded` * 20)) | +| power | int | amount of power that a P-Rep receives from ICONist. See [Power](#power) section for further information | | status | int | [PREP_STATUS](#prep_status) | | totalBlocks | int | number of blocks that a P-Rep received when running as a Main P-Rep | | validatedBlocks | int | number of blocks that a P-Rep validated when running as a Main P-Rep | @@ -1826,7 +1867,7 @@ The list of fields below is subject to change based on revisions | name | str | P-Rep name | | address | Address | P-Rep address | | delegated | int | delegation amount that a P-Rep receives from ICONist | -| power | int | amount of power that a P-Rep receives from ICONist. (= min(`bonded`+`delegated`, `bonded` * 20)) | +| power | int | amount of power that a P-Rep receives from ICONist. See [Power](#power) section for more details | ## PRepStats @@ -1894,6 +1935,30 @@ The list of fields below is subject to change based on revisions | name | str | name | | value | int | value | +## Power + +* `power` is also known as `votingPower`. +* `power` is calculated according to the following formular: + +``` +power = min(bonded + delegated, bonded * 10000 / bondRequirementRate) +``` + +| Name | Type | Description | +|:--------------------|:-----|:-----------------------------------------------------------------------------------------------------------| +| bonded | int | bond amount that a P-Rep receives from ICONist | +| delegated | int | delegation amount that a P-Rep receives from ICONist | +| bondRequirementRate | int | Network configuration value that can be adjusted via [setBondRequirementRate](#setbondrequirementrate) API | + +* bondRequirementRate contains the value ranging from 0 to 10000. + - 0: 0% + - 1: 0.01% + - 10: 0.1% + - 100: 1% + - 1000: 10% + - 5000: 50% + - 10000: 100% + # Event logs ## PenaltyImposed(Address,int,int) diff --git a/icon/chainscore.go b/icon/chainscore.go index 8ec758264..971c2eb9f 100644 --- a/icon/chainscore.go +++ b/icon/chainscore.go @@ -870,6 +870,14 @@ var chainMethods = []*chainMethod{ scoreapi.Dict, }, }, icmodule.RevisionIISS4R0, 0}, + {scoreapi.Method{ + scoreapi.Function, "setBondRequirementRate", + scoreapi.FlagExternal, 1, + []scoreapi.Parameter{ + {"rate", scoreapi.Integer, nil, nil}, + }, + nil, + }, icmodule.RevisionSetBondRequirementRate, 0}, } func applyStepLimits(fee *FeeConfig, as state.AccountState) error { diff --git a/icon/chainscore_iiss.go b/icon/chainscore_iiss.go index 29f131320..ea32b93d6 100644 --- a/icon/chainscore_iiss.go +++ b/icon/chainscore_iiss.go @@ -1048,3 +1048,14 @@ func (s *chainScore) Ex_getPRepCountConfig() (map[string]interface{}, error) { } return es.GetPRepCountConfig() } + +func (s *chainScore) Ex_setBondRequirementRate(rate int64) error { + if err := s.checkGovernance(true); err != nil { + return err + } + es, err := s.getExtensionState() + if err != nil { + return err + } + return es.SetBondRequirementRate(s.newCallContext(s.cc), icmodule.Rate(rate)) +} diff --git a/icon/icmodule/revision.go b/icon/icmodule/revision.go index 3de0d9626..605e4e319 100644 --- a/icon/icmodule/revision.go +++ b/icon/icmodule/revision.go @@ -47,6 +47,7 @@ const ( Revision25 Revision26 Revision27 + Revision28 RevisionReserved ) @@ -126,6 +127,8 @@ const ( RevisionFixIssueRegulator = Revision26 RevisionRecoverUnderIssuance = Revision27 + + RevisionSetBondRequirementRate = Revision28 ) var revisionFlags []module.Revision diff --git a/icon/icsim/revision.go b/icon/icsim/revision.go index 057ac473b..83837a06a 100644 --- a/icon/icsim/revision.go +++ b/icon/icsim/revision.go @@ -29,6 +29,7 @@ var revHandlerTable = []revHandlerItem{ //{icmodule.RevisionBlockAccounts2, onRevBlockAccounts2}, {icmodule.RevisionIISS4R0, onRevIISS4R0}, {icmodule.RevisionIISS4R1, onRevIISS4R1}, + {icmodule.RevisionSetBondRequirementRate, onRevSetBondRequirementRate}, } // DO NOT update revHandlerMap manually @@ -99,7 +100,7 @@ func onRevIISS(sim *simulatorImpl, wc WorldContext, rev, _ int) error { if err := es.State.SetSubPRepCount(cfg.SubPRepCount); err != nil { return err } - if err := es.State.SetBondRequirement(cfg.BondRequirement); err != nil { + if err := es.State.SetBondRequirement(rev, cfg.BondRequirement); err != nil { return err } if err := es.State.SetLockVariables(big.NewInt(cfg.LockMinMultiplier), big.NewInt(cfg.LockMaxMultiplier)); err != nil { @@ -194,7 +195,7 @@ func onRevIISS2(_ *simulatorImpl, wc WorldContext, _, _ int) error { return nil } -func onRevICON2R1(_ *simulatorImpl, wc WorldContext, _, _ int) error { +func onRevICON2R1(_ *simulatorImpl, wc WorldContext, rev, _ int) error { es := getExtensionState(wc) as := wc.GetAccountState(state.SystemID) @@ -209,8 +210,8 @@ func onRevICON2R1(_ *simulatorImpl, wc WorldContext, _, _ int) error { if err := scoredb.NewVarDB(as, state.VarNextBlockVersion).Set(module.BlockVersion2); err != nil { return err } - if es.State.GetBondRequirement() == icmodule.ToRate(icmodule.IISS2BondRequirement) { - if err := es.State.SetBondRequirement(icmodule.ToRate(icmodule.DefaultBondRequirement)); err != nil { + if es.State.GetBondRequirement(rev) == icmodule.ToRate(icmodule.IISS2BondRequirement) { + if err := es.State.SetBondRequirement(rev, icmodule.ToRate(icmodule.DefaultBondRequirement)); err != nil { return err } } @@ -267,3 +268,8 @@ func onRevIISS4R1(_ *simulatorImpl, wc WorldContext, _, _ int) error { es := getExtensionState(wc) return es.State.SetIISSVersion(icstate.IISSVersion4) } + +func onRevSetBondRequirementRate(sim *simulatorImpl, wc WorldContext, rev, _ int) error { + es := getExtensionState(wc) + return es.State.MigrateBondRequirement(rev) +} diff --git a/icon/icsim/setbondrequirementrate_test.go b/icon/icsim/setbondrequirementrate_test.go new file mode 100644 index 000000000..5875689f2 --- /dev/null +++ b/icon/icsim/setbondrequirementrate_test.go @@ -0,0 +1,147 @@ +/* + * Copyright 2024 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package icsim + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/icon-project/goloop/icon/icmodule" + "github.com/icon-project/goloop/icon/iiss" + "github.com/icon-project/goloop/module" + "github.com/icon-project/goloop/service/state" + "github.com/icon-project/goloop/service/txresult" +) + +func assertBondRequirement(t *testing.T, sim Simulator, br, nextBr icmodule.Rate) { + term := sim.GetPRepTermInJSON() + networkInfo := sim.GetNetworkInfoInJSON() + + if term["revision"].(int) < icmodule.RevisionSetBondRequirementRate { + assert.Equal(t, br.Percent(), term["bondRequirement"]) + assert.NotContains(t, term, "bondRequirementRate") + } else { + assert.NotContains(t, term, "bondRequirement") + assert.Equal(t, br.NumInt64(), term["bondRequirementRate"]) + } + + if sim.Revision().Value() < icmodule.RevisionSetBondRequirementRate { + assert.Equal(t, nextBr.Percent(), networkInfo["bondRequirement"]) + assert.NotContains(t, networkInfo, "bondRequirementRate") + } else { + assert.NotContains(t, networkInfo, "bondRequirement") + assert.Equal(t, nextBr.NumInt64(), networkInfo["bondRequirementRate"]) + } +} + +func assertEventSetBondRequirementRate(t *testing.T, ev *txresult.TestEventLog, rate icmodule.Rate) { + err := ev.Assert(state.SystemAddress, iiss.EventBondRequirementRateSet, nil, []any{rate.NumInt64()}) + assert.NoError(t, err) +} + +func TestSimulatorImpl_SetBondRequirementRate(t *testing.T) { + const ( + termPeriod = int64(10) + ) + var err error + var csi module.ConsensusInfo + var receipts []Receipt + var nextBr icmodule.Rate + br := icmodule.ToRate(5) + rev := icmodule.ValueToRevision(icmodule.RevisionSetBondRequirementRate - 1) + + cfg := NewSimConfigWithParams(map[SimConfigOption]interface{}{ + SCOTermPeriod: termPeriod, + SCOBondRequirement: br, + }) + env, err := NewEnv(cfg, rev) + sim := env.Simulator() + assert.NoError(t, err) + assert.NotNil(t, sim) + assert.Equal(t, sim.Revision().Value(), rev.Value()) + + // T(0) + assertBondRequirement(t, sim, br, br) + assert.NoError(t, sim.Go(csi, 1)) + assertBondRequirement(t, sim, br, br) + assert.NoError(t, sim.GoToTermEnd(csi)) + + // T(1) + // SetBondRequirementRate is forbidden before icmodule.RevisionSetBondRequirementRate + receipts, err = sim.GoBySetBondRequirementRate(csi, env.Governance(), icmodule.ToRate(3)) + assert.NoError(t, err) + assert.Equal(t, Failure, receipts[1].Status()) + assertBondRequirement(t, sim, br, br) + + // Revision update to RevisionSetBondRequirementRate + rev = icmodule.ValueToRevision(icmodule.RevisionSetBondRequirementRate) + receipts, err = sim.GoBySetRevision(csi, env.Governance(), rev) + assert.NoError(t, err) + assert.Equal(t, 2, len(receipts)) + assert.Equal(t, Success, receipts[1].Status()) + assert.Equal(t, sim.Revision().Value(), rev.Value()) + + // GetBondRequirementRate() works after RevisionSetBondRequirementRate + assertBondRequirement(t, sim, br, br) + + // Ensure that calling setBondRequirementRate() more than once during the same term works well + for i := 0; i < 2; i++ { + nextBr = icmodule.ToRate(int64(i)) + receipts, err = sim.GoBySetBondRequirementRate(csi, env.Governance(), nextBr) + rcpt := receipts[1] + assert.NoError(t, err) + assert.Equal(t, 2, len(receipts)) + assert.Equal(t, Success, rcpt.Status()) + assert.Equal(t, 1, len(rcpt.Events())) + assertEventSetBondRequirementRate(t, rcpt.Events()[0], nextBr) + assertBondRequirement(t, sim, br, nextBr) + } + + // When calling setBondRequirementRate() with the new rate that is the same as the existing one, + // the transaction succeeds without any event logs + receipts, err = sim.GoBySetBondRequirementRate(csi, env.Governance(), nextBr) + assert.NoError(t, err) + assert.Equal(t, 2, len(receipts)) + rcpt := receipts[1] + assert.Equal(t, Success, rcpt.Status()) + assert.Zero(t, len(rcpt.Events())) + assertBondRequirement(t, sim, br, nextBr) + + assert.NoError(t, sim.GoToTermEnd(csi)) + + // T(2) + assertBondRequirement(t, sim, nextBr, nextBr) + assert.NoError(t, sim.Go(csi, 1)) + assertBondRequirement(t, sim, nextBr, nextBr) + assert.NoError(t, sim.GoToTermEnd(csi)) + + // T(3) + assertBondRequirement(t, sim, nextBr, nextBr) + assert.NoError(t, sim.Go(csi, 1)) + assertBondRequirement(t, sim, nextBr, nextBr) + + // Ensure that only valid BondRequirementRates are allowed + for _, rate := range []icmodule.Rate{-1, icmodule.ToRate(101)} { + receipts, err = sim.GoBySetBondRequirementRate(csi, env.Governance(), rate) + assert.NoError(t, err) + assert.Equal(t, 2, len(receipts)) + assert.Equal(t, Failure, receipts[1].Status()) + assert.Zero(t, len(receipts[1].Events())) + assertBondRequirement(t, sim, nextBr, nextBr) + } +} diff --git a/icon/icsim/simulator.go b/icon/icsim/simulator.go index a5f373bb3..68d61bd68 100644 --- a/icon/icsim/simulator.go +++ b/icon/icsim/simulator.go @@ -26,6 +26,7 @@ import ( ) type TxType int + const ( TypeTransfer TxType = iota TypeSetStake @@ -46,6 +47,7 @@ const ( TypeHandleDoubleSignReport TypeSetPRepCountConfig TypeSetRewardFundAllocation2 + TypeSetBondRequirementRate ) type Transaction interface { @@ -182,4 +184,8 @@ type Simulator interface { SetRewardFundAllocation2(from module.Address, values map[icstate.RFundKey]icmodule.Rate) Transaction GoBySetRewardFundAllocation2( csi module.ConsensusInfo, from module.Address, values map[icstate.RFundKey]icmodule.Rate) ([]Receipt, error) + + // After RevisionSetBondRequirementRate + SetBondRequirementRate(from module.Address, rate icmodule.Rate) Transaction + GoBySetBondRequirementRate(csi module.ConsensusInfo, from module.Address, rate icmodule.Rate) ([]Receipt, error) } diff --git a/icon/icsim/simulatorimpl.go b/icon/icsim/simulatorimpl.go index 7e1d176bf..b2ec85cb8 100644 --- a/icon/icsim/simulatorimpl.go +++ b/icon/icsim/simulatorimpl.go @@ -448,6 +448,8 @@ func (sim *simulatorImpl) executeTx(cc *callContext, tx Transaction) error { err = sim.setPRepCountConfig(es, cc, tx) case TypeSetRewardFundAllocation2: err = sim.setRewardFundAllocation2(es, cc, tx) + case TypeSetBondRequirementRate: + err = sim.setBondRequirementRate(es, cc, tx) default: return errors.Errorf("Unexpected transaction: %v", tx.Type()) } @@ -877,6 +879,21 @@ func (sim *simulatorImpl) setRewardFundAllocation2( return es.State.SetRewardFund(rf) } +func (sim *simulatorImpl) SetBondRequirementRate(from module.Address, rate icmodule.Rate) Transaction { + return NewTransaction(TypeSetBondRequirementRate, from, rate) +} + +func (sim *simulatorImpl) GoBySetBondRequirementRate( + csi module.ConsensusInfo, from module.Address, rate icmodule.Rate) ([]Receipt, error) { + return sim.goByOneTransaction(csi, TypeSetBondRequirementRate, from, rate) +} + +func (sim *simulatorImpl) setBondRequirementRate(es *iiss.ExtensionStateImpl, cc *callContext, tx Transaction) error { + args := tx.Args() + rate := args[0].(icmodule.Rate) + return es.SetBondRequirementRate(cc, rate) +} + func NewSimulator( revision module.Revision, initValidators []module.Validator, initBalances map[string]*big.Int, config *SimConfig, ) (Simulator, error) { diff --git a/icon/iiss/eventlog.go b/icon/iiss/eventlog.go index 95e6cb121..2238fe8a8 100644 --- a/icon/iiss/eventlog.go +++ b/icon/iiss/eventlog.go @@ -58,6 +58,7 @@ const ( EventRewardFundSet = "RewardFundSet(int)" EventRewardFundAllocationSet = "RewardFundAllocationSet(str,int)" EventNetworkScoreSet = "NetworkScoreSet(str,Address)" + EventBondRequirementRateSet = "BondRequirementRateSet(int)" ) func EmitSlashingRateSetEvent(cc icmodule.CallContext, penaltyType icmodule.PenaltyType, rate icmodule.Rate) { @@ -393,3 +394,10 @@ func EmitNetworkScoreSetEvent(cc icmodule.CallContext, role string, address modu [][]byte{[]byte(role), addrBytes}, ) } + +func EmitBondRequirementRateSetEvent(cc icmodule.CallContext, rate icmodule.Rate) { + cc.OnEvent(state.SystemAddress, + [][]byte{[]byte(EventBondRequirementRateSet)}, + [][]byte{intconv.Int64ToBytes(rate.NumInt64())}, + ) +} diff --git a/icon/iiss/extension.go b/icon/iiss/extension.go index 0eb554b39..8f2b4af06 100644 --- a/icon/iiss/extension.go +++ b/icon/iiss/extension.go @@ -2073,3 +2073,22 @@ func (es *ExtensionStateImpl) SetMinimumBond(cc icmodule.CallContext, nBond *big EmitMinimumBondSetEvent(cc, nBond) return nil } + +func (es *ExtensionStateImpl) SetBondRequirementRate(cc icmodule.CallContext, rate icmodule.Rate) error { + revision := cc.Revision().Value() + if revision < icmodule.RevisionSetBondRequirementRate { + return scoreresult.AccessDeniedError.Errorf("SetBondRequirementRateNotAllowed(rev=%d)", revision) + } + if !rate.IsValid() { + return scoreresult.InvalidParameterError.Errorf("InvalidBondRequirementRate(%d)", rate) + } + + var err error + oldRate := es.State.GetBondRequirement(revision) + if rate != oldRate { + if err = es.State.SetBondRequirement(revision, rate); err == nil { + EmitBondRequirementRateSetEvent(cc, rate) + } + } + return err +} diff --git a/icon/iiss/icstate/networkvalue.go b/icon/iiss/icstate/networkvalue.go index dc9a360e4..d36bb6e14 100644 --- a/icon/iiss/icstate/networkvalue.go +++ b/icon/iiss/icstate/networkvalue.go @@ -242,16 +242,34 @@ func (s *State) SetTotalStake(value *big.Int) error { return setValue(s.store, VarTotalStake, value) } -func (s *State) GetBondRequirement() icmodule.Rate { +func (s *State) GetBondRequirement(revision int) icmodule.Rate { v := getValue(s.store, VarBondRequirement).Int64() - return icmodule.ToRate(v) + if revision < icmodule.RevisionSetBondRequirementRate { + return icmodule.ToRate(v) + } else { + return icmodule.Rate(v) + } } -func (s *State) SetBondRequirement(br icmodule.Rate) error { +func (s *State) SetBondRequirement(revision int, br icmodule.Rate) error { if !br.IsValid() { return errors.IllegalArgumentError.New("Bond Requirement should range from 0% to 100%") } - return setValue(s.store, VarBondRequirement, br.Percent()) + + if revision < icmodule.RevisionSetBondRequirementRate { + return setValue(s.store, VarBondRequirement, br.Percent()) + } else { + return setValue(s.store, VarBondRequirement, br.NumInt64()) + } +} + +func (s *State) MigrateBondRequirement(revision int) error { + if revision != icmodule.RevisionSetBondRequirementRate { + return scoreresult.AccessDeniedError.Errorf("MigrateBondRequirementNotAllowed(rev=%d)", revision) + } + // Change BondRequirementRate unit from 1% to 0.01% + rate := s.GetBondRequirement(revision - 1) + return s.SetBondRequirement(revision, rate) } func (s *State) SetUnbondingPeriodMultiplier(value int64) error { @@ -482,7 +500,7 @@ func (s *State) SetMinimumBond(bond *big.Int) error { } func (s *State) GetNetworkInfoInJSON(revision int) (map[string]interface{}, error) { - br := s.GetBondRequirement() + br := s.GetBondRequirement(revision) jso := make(map[string]interface{}) jso["mainPRepCount"] = s.GetMainPRepCount() jso["extraMainPRepCount"] = s.GetExtraMainPRepCount() @@ -490,7 +508,11 @@ func (s *State) GetNetworkInfoInJSON(revision int) (map[string]interface{}, erro jso["totalStake"] = s.GetTotalStake() jso["iissVersion"] = int64(s.GetIISSVersion()) jso["termPeriod"] = s.GetTermPeriod() - jso["bondRequirement"] = br.Percent() + if revision < icmodule.RevisionSetBondRequirementRate { + jso["bondRequirement"] = br.Percent() + } else { + jso["bondRequirementRate"] = br.NumInt64() + } jso["lockMinMultiplier"] = s.GetLockMinMultiplier() jso["lockMaxMultiplier"] = s.GetLockMaxMultiplier() jso["rewardFund"] = s.GetRewardFund(revision).ToJSON() diff --git a/icon/iiss/icstate/networkvalue_test.go b/icon/iiss/icstate/networkvalue_test.go index f25b75ff9..02d3448c2 100644 --- a/icon/iiss/icstate/networkvalue_test.go +++ b/icon/iiss/icstate/networkvalue_test.go @@ -173,29 +173,30 @@ func setTotalStakeTest(t *testing.T, s *State) { } func setBondRequirementTest(t *testing.T, s *State) { + revision := icmodule.RevisionSetBondRequirementRate - 1 br := icmodule.ToRate(0) - actual := s.GetBondRequirement() + actual := s.GetBondRequirement(revision) assert.Equal(t, br, actual) br = icmodule.ToRate(5) - assert.NoError(t, s.SetBondRequirement(br)) - actual = s.GetBondRequirement() + assert.NoError(t, s.SetBondRequirement(revision, br)) + actual = s.GetBondRequirement(revision) assert.Equal(t, br, actual) br = icmodule.ToRate(0) - err := s.SetBondRequirement(br) + err := s.SetBondRequirement(revision, br) assert.NoError(t, err) - actual = s.GetBondRequirement() + actual = s.GetBondRequirement(revision) assert.Equal(t, br, actual) - err = s.SetBondRequirement(icmodule.ToRate(101)) + err = s.SetBondRequirement(revision, icmodule.ToRate(101)) assert.Error(t, err) - actual = s.GetBondRequirement() + actual = s.GetBondRequirement(revision) assert.Equal(t, br, actual) - err = s.SetBondRequirement(icmodule.ToRate(-1)) + err = s.SetBondRequirement(revision, icmodule.ToRate(-1)) assert.Error(t, err) - actual = s.GetBondRequirement() + actual = s.GetBondRequirement(revision) assert.Equal(t, br, actual) } @@ -753,3 +754,39 @@ func TestState_GetNetworkInfoInJSON(t *testing.T) { } } } + +func TestState_SetBondRequirement(t *testing.T) { + nextRate := icmodule.ToRate(5) + + rev := icmodule.RevisionSetBondRequirementRate - 1 + s := newDummyState(false) + + assert.NoError(t, s.SetBondRequirement(rev, nextRate)) + assert.Equal(t, nextRate, s.GetBondRequirement(rev)) + + rev = icmodule.RevisionSetBondRequirementRate + + assert.NoError(t, s.MigrateBondRequirement(rev)) + assert.Equal(t, nextRate, s.GetBondRequirement(rev)) + + nextRate = icmodule.ToRate(3) + assert.NoError(t, s.SetBondRequirement(rev, nextRate)) + assert.Equal(t, nextRate, s.GetBondRequirement(rev)) + + assert.NoError(t, s.SetBondRequirement(rev, nextRate)) + assert.Equal(t, nextRate, s.GetBondRequirement(rev)) +} + +func TestState_MigrateBondRequirement(t *testing.T) { + s := newDummyState(false) + + rev := icmodule.RevisionSetBondRequirementRate - 1 + rate := icmodule.ToRate(10) + assert.NoError(t, s.SetBondRequirement(rev, rate)) + assert.Equal(t, rate, s.GetBondRequirement(rev)) + assert.Error(t, s.MigrateBondRequirement(rev)) + + rev = icmodule.RevisionSetBondRequirementRate + assert.NoError(t, s.MigrateBondRequirement(rev)) + assert.Equal(t, rate, s.GetBondRequirement(rev)) +} diff --git a/icon/iiss/icstate/state.go b/icon/iiss/icstate/state.go index 48f8d79e9..69b21d4db 100644 --- a/icon/iiss/icstate/state.go +++ b/icon/iiss/icstate/state.go @@ -429,7 +429,7 @@ func (s *State) OnValidatorOut(sc icmodule.StateContext, owner module.Address) e func (s *State) GetPRepStatuses() ([]*PRepStatusState, error) { size := s.allPRepCache.Size() - pss := make([]*PRepStatusState, 0, size / 2) + pss := make([]*PRepStatusState, 0, size/2) for i := 0; i < size; i++ { owner := s.allPRepCache.Get(i) @@ -544,7 +544,7 @@ func (s *State) SetLastBlockVotersSnapshot(value *BlockVotersSnapshot) error { func (s *State) IsDecentralizationConditionMet(revision int, totalSupply *big.Int, preps PRepSet) bool { predefinedMainPRepCount := int(s.GetMainPRepCount()) - br := s.GetBondRequirement() + br := s.GetBondRequirement(revision) if revision >= icmodule.RevisionDecentralize && preps.Size() >= predefinedMainPRepCount { prep := preps.GetByIndex(predefinedMainPRepCount - 1) diff --git a/icon/iiss/icstate/term.go b/icon/iiss/icstate/term.go index 4272ed7fe..bf435c162 100644 --- a/icon/iiss/icstate/term.go +++ b/icon/iiss/icstate/term.go @@ -124,8 +124,8 @@ type termDataCommon struct { period int64 revision int isDecentralized bool - totalSupply *big.Int // not nil - totalDelegated *big.Int // total delegated amount of all active P-Reps. not nil + totalSupply *big.Int // not nil + totalDelegated *big.Int // total delegated amount of all active P-Reps. not nil rewardFund *RewardFund // not nil bondRequirement icmodule.Rate mainPRepCount int @@ -245,7 +245,7 @@ func (term *termDataCommon) clone() termDataCommon { } func (term *termDataCommon) ToJSON(sc icmodule.StateContext, state *State) map[string]interface{} { - return map[string]interface{}{ + jso := map[string]interface{}{ "sequence": term.sequence, "startBlockHeight": term.startHeight, "endBlockHeight": term.GetEndHeight(), @@ -254,13 +254,18 @@ func (term *termDataCommon) ToJSON(sc icmodule.StateContext, state *State) map[s "totalPower": term.getTotalPower(), "period": term.period, "rewardFund": term.rewardFund.ToJSON(), - "bondRequirement": term.bondRequirement.Percent(), "revision": term.revision, "isDecentralized": term.isDecentralized, "mainPRepCount": term.mainPRepCount, "iissVersion": term.GetIISSVersion(), "preps": term.prepsToJSON(sc, state), } + if term.revision < icmodule.RevisionSetBondRequirementRate { + jso["bondRequirement"] = term.bondRequirement.Percent() + } else { + jso["bondRequirementRate"] = term.bondRequirement.NumInt64() + } + return jso } func (term *termDataCommon) prepsToJSON(sc icmodule.StateContext, state *State) []interface{} { @@ -411,7 +416,7 @@ func (term *termData) equal(other *termData) bool { func (term *termData) clone() termData { td := termData{ - termDataCommon: term.termDataCommon.clone(), + termDataCommon: term.termDataCommon.clone(), } switch term.Version() { case termVersion1: @@ -630,6 +635,8 @@ func (term *TermState) SetRrep(rrep *big.Int) { // It assumes that state and totalSupply are not nil. func NewNextTerm(sc icmodule.StateContext, state *State, totalSupply *big.Int, preps PRepSet) *TermState { rev := sc.RevisionValue() + br := sc.GetBondRequirement() + // Previous term tss := state.GetTermSnapshot() if tss == nil { @@ -660,7 +667,7 @@ func NewNextTerm(sc icmodule.StateContext, state *State, totalSupply *big.Int, p totalSupply: totalSupply, totalDelegated: state.GetTotalDelegation(), rewardFund: state.GetRewardFund(rev), - bondRequirement: state.GetBondRequirement(), + bondRequirement: br, revision: rev, isDecentralized: isDecentralized, }, @@ -676,7 +683,7 @@ func NewNextTerm(sc icmodule.StateContext, state *State, totalSupply *big.Int, p // Update PRepSnapshots if isDecentralized { termState.mainPRepCount = preps.GetPRepSize(GradeMain) - termState.prepSnapshots = preps.ToPRepSnapshots(sc.GetBondRequirement()) + termState.prepSnapshots = preps.ToPRepSnapshots(br) } return termState @@ -693,7 +700,7 @@ func GenesisTerm(state *State, startHeight int64, revision int) *TermState { totalSupply: icmodule.BigIntZero, totalDelegated: icmodule.BigIntZero, rewardFund: state.GetRewardFundV1().Clone(), - bondRequirement: state.GetBondRequirement(), + bondRequirement: state.GetBondRequirement(revision), revision: revision, isDecentralized: false, }, diff --git a/icon/iiss/icstate/term_test.go b/icon/iiss/icstate/term_test.go index fb413125e..6dbe9661b 100644 --- a/icon/iiss/icstate/term_test.go +++ b/icon/iiss/icstate/term_test.go @@ -606,7 +606,7 @@ func TestGenesisTerm(t *testing.T) { state := newDummyState(false) assert.NoError(t, state.SetTermPeriod(tp)) - assert.NoError(t, state.SetBondRequirement(br)) + assert.NoError(t, state.SetBondRequirement(revision, br)) assert.NoError(t, state.SetIRep(irep)) assert.NoError(t, state.SetRRep(rrep)) assert.NoError(t, state.SetRewardFund(rf)) @@ -653,7 +653,7 @@ func TestNewNextTerm(t *testing.T) { // Initialize State state := newDummyState(false) assert.NoError(t, state.SetTermPeriod(tp)) - assert.NoError(t, state.SetBondRequirement(br)) + assert.NoError(t, state.SetBondRequirement(icmodule.RevisionIISS, br)) assert.NoError(t, state.SetIRep(irep)) assert.NoError(t, state.SetRRep(rrep)) assert.NoError(t, state.SetRewardFund(rf)) diff --git a/icon/iiss/statecontext.go b/icon/iiss/statecontext.go index 71c5c9e8e..d57a1e655 100644 --- a/icon/iiss/statecontext.go +++ b/icon/iiss/statecontext.go @@ -62,7 +62,7 @@ func (sc *stateContext) TermIISSVersion() int { func (sc *stateContext) GetBondRequirement() icmodule.Rate { if !sc.br.IsValid() { - sc.br = sc.State.GetBondRequirement() + sc.br = sc.State.GetBondRequirement(sc.RevisionValue()) } return sc.br } diff --git a/icon/revision.go b/icon/revision.go index 9875e574b..14297a097 100644 --- a/icon/revision.go +++ b/icon/revision.go @@ -47,6 +47,7 @@ var revHandlerTable = []revHandlerItem{ {icmodule.RevisionIISS4R1, onRevIISS4R1}, {icmodule.RevisionFixIssueRegulator, onRevFixIssueRegulator}, {icmodule.RevisionRecoverUnderIssuance, onRevRecoverUnderIssuance}, + {icmodule.RevisionSetBondRequirementRate, onRevSetBondRequirementRate}, } // DO NOT update revHandlerMap manually @@ -80,9 +81,7 @@ func (s *chainScore) handleRevisionChange(r1, r2 int) error { return nil } -func onRevIISS(s *chainScore, _, toRev int) error { - // goloop engine - +func onRevIISS(s *chainScore, rev, toRev int) error { as := s.cc.GetAccountState(state.SystemID) // enable Fee sharing 2.0 @@ -123,7 +122,7 @@ func onRevIISS(s *chainScore, _, toRev int) error { if err := es.State.SetSubPRepCount(iconConfig.SubPRepCount.Int64()); err != nil { return err } - if err := es.State.SetBondRequirement(icmodule.ToRate(iconConfig.BondRequirement.Int64())); err != nil { + if err := es.State.SetBondRequirement(rev, icmodule.ToRate(iconConfig.BondRequirement.Int64())); err != nil { return err } if err := es.State.SetLockVariables(iconConfig.LockMinMultiplier.Value(), iconConfig.LockMaxMultiplier.Value()); err != nil { @@ -176,8 +175,8 @@ func onRevIISS(s *chainScore, _, toRev int) error { return err } } - if es.State.GetBondRequirement() == icmodule.ToRate(icmodule.DefaultBondRequirement) { - if err := es.State.SetBondRequirement(icmodule.ToRate(icmodule.IISS2BondRequirement)); err != nil { + if es.State.GetBondRequirement(rev) == icmodule.ToRate(icmodule.DefaultBondRequirement) { + if err := es.State.SetBondRequirement(rev, icmodule.ToRate(icmodule.IISS2BondRequirement)); err != nil { return err } } @@ -243,7 +242,7 @@ func onRevICON2R0(s *chainScore, _, _ int) error { return nil } -func onRevICON2R1(s *chainScore, _, _ int) error { +func onRevICON2R1(s *chainScore, rev, _ int) error { if s.cc.ChainID() == CIDForMainNet { // The time when predefined accounts will be blocked is changed from rev10 to rev14 s.blockAccounts() @@ -265,8 +264,8 @@ func onRevICON2R1(s *chainScore, _, _ int) error { return err } - if es.State.GetBondRequirement() == icmodule.ToRate(icmodule.IISS2BondRequirement) { - if err := es.State.SetBondRequirement(icmodule.ToRate(icmodule.DefaultBondRequirement)); err != nil { + if es.State.GetBondRequirement(rev) == icmodule.ToRate(icmodule.IISS2BondRequirement) { + if err := es.State.SetBondRequirement(rev, icmodule.ToRate(icmodule.DefaultBondRequirement)); err != nil { return err } } @@ -486,3 +485,8 @@ func onRevRecoverUnderIssuance(s *chainScore, _, _ int) error { return nil } + +func onRevSetBondRequirementRate(s *chainScore, rev, _ int) error { + es := s.cc.GetExtensionState().(*iiss.ExtensionStateImpl) + return es.State.MigrateBondRequirement(rev) +}