From efb3127fccb365e139a1488baf1fa403d23cb2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Wed, 9 Nov 2022 20:44:07 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=91=B5=F0=9F=8F=BB=20Once=20upon=20a?= =?UTF-8?q?=20time,=20there=20was=20an=20old=20grandma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dpos_v2/commands/change_commission.ts | 101 ++++++++ framework/src/modules/dpos_v2/constants.ts | 2 + framework/src/modules/dpos_v2/module.ts | 5 + framework/src/modules/dpos_v2/schemas.ts | 13 + framework/src/modules/dpos_v2/types.ts | 4 + .../commands/change_commission.spec.ts | 242 ++++++++++++++++++ 6 files changed, 367 insertions(+) create mode 100644 framework/src/modules/dpos_v2/commands/change_commission.ts create mode 100644 framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts diff --git a/framework/src/modules/dpos_v2/commands/change_commission.ts b/framework/src/modules/dpos_v2/commands/change_commission.ts new file mode 100644 index 00000000000..11d6dc9a033 --- /dev/null +++ b/framework/src/modules/dpos_v2/commands/change_commission.ts @@ -0,0 +1,101 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { validator } from '@liskhq/lisk-validator'; +import { + CommandVerifyContext, + VerificationResult, + VerifyStatus, + CommandExecuteContext, +} from '../../../state_machine'; +import { BaseCommand } from '../../base_command'; +import { COMMISSION_INCREASE_PERIOD, MAX_COMMISSION_INCREASE_RATE } from '../constants'; +import { CommissionChangeEvent } from '../events/commission_change'; +import { changeCommissionCommandParamsSchema } from '../schemas'; +import { DelegateStore } from '../stores/delegate'; +import { ChangeCommissionParams } from '../types'; + +export class ChangeCommissionCommand extends BaseCommand { + public schema = changeCommissionCommandParamsSchema; + + public async verify( + context: CommandVerifyContext, + ): Promise { + try { + validator.validate(changeCommissionCommandParamsSchema, context.params); + } catch (err) { + return { + status: VerifyStatus.FAIL, + error: err as Error, + }; + } + + const delegateStore = this.stores.get(DelegateStore); + const delegateExists = await delegateStore.has(context, context.transaction.senderAddress); + + if (!delegateExists) { + return { + status: VerifyStatus.FAIL, + error: new Error('Transaction sender has not registered as a delegate.'), + }; + } + + const delegate = await delegateStore.get(context, context.transaction.senderAddress); + const oldCommission = delegate.commission; + const hasIncreasedCommissionRecently = + context.header.height - delegate.lastCommissionIncreaseHeight < COMMISSION_INCREASE_PERIOD; + + if (context.params.newCommission > oldCommission && hasIncreasedCommissionRecently) { + return { + status: VerifyStatus.FAIL, + error: new Error( + `Can only increase the commission again ${COMMISSION_INCREASE_PERIOD} blocks after the last commission increase.`, + ), + }; + } + + if (context.params.newCommission - oldCommission > MAX_COMMISSION_INCREASE_RATE) { + return { + status: VerifyStatus.FAIL, + error: new Error( + `Invalid argument: Commission increase larger than ${MAX_COMMISSION_INCREASE_RATE}.`, + ), + }; + } + + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const delegateStore = this.stores.get(DelegateStore); + + const delegate = await delegateStore.get(context, context.transaction.senderAddress); + const oldCommission = delegate.commission; + + delegate.commission = context.params.newCommission; + if (delegate.commission > oldCommission) { + delegate.lastCommissionIncreaseHeight = context.header.height; + } + + await delegateStore.set(context, context.transaction.senderAddress, delegate); + + this.events.get(CommissionChangeEvent).log(context, { + delegateAddress: context.transaction.senderAddress, + oldCommission, + newCommission: context.params.newCommission, + }); + } +} diff --git a/framework/src/modules/dpos_v2/constants.ts b/framework/src/modules/dpos_v2/constants.ts index 3986d27d88a..c4ad75e8a59 100644 --- a/framework/src/modules/dpos_v2/constants.ts +++ b/framework/src/modules/dpos_v2/constants.ts @@ -39,6 +39,8 @@ export const LOCAL_ID_LENGTH = 4; export const TOKEN_ID_LENGTH = CHAIN_ID_LENGTH + LOCAL_ID_LENGTH; export const MAX_NUMBER_BYTES_Q96 = 24; export const COMMISSION = 10000; +export const COMMISSION_INCREASE_PERIOD = 260000; +export const MAX_COMMISSION_INCREASE_RATE = 500; // Key length export const ED25519_PUBLIC_KEY_LENGTH = 32; diff --git a/framework/src/modules/dpos_v2/module.ts b/framework/src/modules/dpos_v2/module.ts index 5b82217cf72..70a45b5a8f0 100644 --- a/framework/src/modules/dpos_v2/module.ts +++ b/framework/src/modules/dpos_v2/module.ts @@ -24,6 +24,7 @@ import { ReportDelegateMisbehaviorCommand } from './commands/pom'; import { UnlockCommand } from './commands/unlock'; import { UpdateGeneratorKeyCommand } from './commands/update_generator_key'; import { VoteDelegateCommand } from './commands/vote_delegate'; +import { ChangeCommissionCommand } from './commands/change_commission'; import { DELEGATE_LIST_ROUND_OFFSET, EMPTY_KEY, @@ -82,6 +83,7 @@ import { DelegateBannedEvent } from './events/delegate_banned'; import { DelegatePunishedEvent } from './events/delegate_punished'; import { DelegateRegisteredEvent } from './events/delegate_registered'; import { DelegateVotedEvent } from './events/delegate_voted'; +import { CommissionChangeEvent } from './events/commission_change'; export class DPoSModule extends BaseModule { public method = new DPoSMethod(this.stores, this.events); @@ -102,6 +104,7 @@ export class DPoSModule extends BaseModule { this.events, ); private readonly _voteCommand = new VoteDelegateCommand(this.stores, this.events); + private readonly _changeCommissionCommand = new ChangeCommissionCommand(this.stores, this.events); // eslint-disable-next-line @typescript-eslint/member-ordering public commands = [ @@ -110,6 +113,7 @@ export class DPoSModule extends BaseModule { this._unlockCommand, this._updateGeneratorKeyCommand, this._voteCommand, + this._changeCommissionCommand, ]; private _randomMethod!: RandomMethod; @@ -131,6 +135,7 @@ export class DPoSModule extends BaseModule { this.events.register(DelegatePunishedEvent, new DelegatePunishedEvent(this.name)); this.events.register(DelegateRegisteredEvent, new DelegateRegisteredEvent(this.name)); this.events.register(DelegateVotedEvent, new DelegateVotedEvent(this.name)); + this.events.register(CommissionChangeEvent, new CommissionChangeEvent(this.name)); } public get name() { diff --git a/framework/src/modules/dpos_v2/schemas.ts b/framework/src/modules/dpos_v2/schemas.ts index 27d2295e85e..9dd98725985 100644 --- a/framework/src/modules/dpos_v2/schemas.ts +++ b/framework/src/modules/dpos_v2/schemas.ts @@ -114,6 +114,19 @@ export const pomCommandParamsSchema = { }, }; +export const changeCommissionCommandParamsSchema = { + $id: '/dpos/command/changeCommissionCommandParams', + type: 'object', + required: ['newCommission'], + properties: { + newCommission: { + dataType: 'uint32', + fieldNumber: 1, + maximum: MAX_COMMISSION, + }, + }, +}; + export const configSchema = { $id: '/dpos/config', type: 'object', diff --git a/framework/src/modules/dpos_v2/types.ts b/framework/src/modules/dpos_v2/types.ts index 2593a5e150b..da795e1ffa2 100644 --- a/framework/src/modules/dpos_v2/types.ts +++ b/framework/src/modules/dpos_v2/types.ts @@ -214,6 +214,10 @@ export interface PomCommandDependencies { validatorsMethod: ValidatorsMethod; } +export interface ChangeCommissionParams { + newCommission: number; +} + export interface ValidatorKeys { generatorKey: Buffer; blsKey: Buffer; diff --git a/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts b/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts new file mode 100644 index 00000000000..9b923df069b --- /dev/null +++ b/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts @@ -0,0 +1,242 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Transaction } from '@liskhq/lisk-chain'; +import { codec } from '@liskhq/lisk-codec'; +import { utils } from '@liskhq/lisk-cryptography'; +import * as testing from '../../../../../src/testing'; +import { ChangeCommissionCommand } from '../../../../../src/modules/dpos_v2/commands/change_commission'; +import { changeCommissionCommandParamsSchema as schema } from '../../../../../src/modules/dpos_v2/schemas'; +import { ChangeCommissionParams } from '../../../../../src/modules/dpos_v2/types'; +import { EventQueue, VerifyStatus } from '../../../../../src/state_machine'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing/in_memory_prefixed_state'; +import { DelegateStore } from '../../../../../src/modules/dpos_v2/stores/delegate'; +import { DPoSModule } from '../../../../../src'; +import { createStoreGetter } from '../../../../../src/testing/utils'; +import { + COMMISSION_INCREASE_PERIOD, + MAX_COMMISSION_INCREASE_RATE, +} from '../../../../../src/modules/dpos_v2/constants'; +import { createFakeBlockHeader } from '../../../../../src/testing'; +import { CommissionChangeEvent } from '../../../../../src/modules/dpos_v2/events/commission_change'; + +describe('Change Commission command', () => { + const dpos = new DPoSModule(); + const changeCommissionCommand = new ChangeCommissionCommand(dpos.stores, dpos.events); + let stateStore: PrefixedStateReadWriter; + let delegateStore: DelegateStore; + let commissionChangedEvent: CommissionChangeEvent; + + const publicKey = utils.getRandomBytes(32); + + const delegateDetails = { + name: 'PamelaAnderson', + totalVotesReceived: BigInt(0), + selfVotes: BigInt(0), + lastGeneratedHeight: 0, + isBanned: false, + pomHeights: [], + consecutiveMissedBlocks: 0, + commission: 100, + lastCommissionIncreaseHeight: 0, + sharingCoefficients: [], + }; + + const commandParams: ChangeCommissionParams = { + newCommission: delegateDetails.commission + MAX_COMMISSION_INCREASE_RATE - 1, + }; + const encodedCommandParams = codec.encode(schema, commandParams); + const transactionDetails = { + module: 'dpos', + command: changeCommissionCommand.name, + senderPublicKey: publicKey, + nonce: BigInt(0), + fee: BigInt(100000000), + params: encodedCommandParams, + signatures: [publicKey], + }; + let transaction = new Transaction(transactionDetails); + + // TODO: move this function to utils and import from all other tests using it + const checkEventResult = ( + eventQueue: EventQueue, + EventClass: any, + moduleName: string, + expectedResult: any, + length = 1, + index = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new EventClass(moduleName).name); + + const eventData = codec.decode>( + new EventClass(moduleName).schema, + eventQueue.getEvents()[index].toObject().data, + ); + + expect(eventData).toEqual(expectedResult); + }; + + beforeEach(async () => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + delegateStore = dpos.stores.get(DelegateStore); + + await delegateStore.set( + createStoreGetter(stateStore), + transaction.senderAddress, + delegateDetails, + ); + + commissionChangedEvent = dpos.events.get(CommissionChangeEvent); + jest.spyOn(commissionChangedEvent, 'log'); + }); + + describe('verify', () => { + it('should return status OK for valid params', async () => { + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandVerifyContext(schema); + + const result = await changeCommissionCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + + it('should return error when changing commission of an unregistered delegate', async () => { + await delegateStore.del(createStoreGetter(stateStore), transaction.senderAddress); + + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandVerifyContext(schema); + + const result = await changeCommissionCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + 'Transaction sender has not registered as a delegate.', + ); + }); + + it('should return error when changing commission before commission increase period has expired', async () => { + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: 8 }), + }) + .createCommandVerifyContext(schema); + + const result = await changeCommissionCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `Can only increase the commission again ${COMMISSION_INCREASE_PERIOD} blocks after the last commission increase.`, + ); + }); + + it('should return error when requested commission change is higher than allowed', async () => { + commandParams.newCommission = delegateDetails.commission + MAX_COMMISSION_INCREASE_RATE + 1; + transactionDetails.params = codec.encode(schema, commandParams); + transaction = new Transaction(transactionDetails); + + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandVerifyContext(schema); + + const result = await changeCommissionCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `Invalid argument: Commission increase larger than ${MAX_COMMISSION_INCREASE_RATE}.`, + ); + }); + }); + + describe('execute', () => { + it('should update last commission increase height in the delegate store after INCREASING commission', async () => { + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandExecuteContext(schema); + + await changeCommissionCommand.execute(context); + const delegate = await delegateStore.get(context, transaction.senderAddress); + + expect(delegate.commission).toBe(commandParams.newCommission); + expect(delegate.lastCommissionIncreaseHeight).toBe(COMMISSION_INCREASE_PERIOD + 1); + }); + + it('should NOT update last commission increase height in the delegate store after DECREASING commission', async () => { + commandParams.newCommission = 50; + transactionDetails.params = codec.encode(schema, commandParams); + transaction = new Transaction(transactionDetails); + + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandExecuteContext(schema); + + await changeCommissionCommand.execute(context); + const delegate = await delegateStore.get(context, transaction.senderAddress); + + expect(delegate.commission).toBe(commandParams.newCommission); + expect(delegate.lastCommissionIncreaseHeight).toBe(0); + }); + + it('should emit event after changing commission', async () => { + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandExecuteContext(schema); + + await changeCommissionCommand.execute(context); + + // check if the event has been dispatched correctly + expect(commissionChangedEvent.log).toHaveBeenCalledWith(expect.anything(), { + delegateAddress: transaction.senderAddress, + oldCommission: delegateDetails.commission, + newCommission: commandParams.newCommission, + }); + + // check if the event is in the event queue + checkEventResult(context.eventQueue, CommissionChangeEvent, 'dpos', { + delegateAddress: transaction.senderAddress, + oldCommission: delegateDetails.commission, + newCommission: commandParams.newCommission, + }); + }); + }); +}); From 37690dde3aa719cf413871b8fd86e12509dccb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Wed, 9 Nov 2022 21:25:43 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8F=A0=20She=20lived=20in=20a=20house?= =?UTF-8?q?=20on=20top=20of=20a=20hill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dpos_v2/commands/change_commission.ts | 18 +++++++++++++----- framework/src/modules/dpos_v2/constants.ts | 2 ++ framework/src/modules/dpos_v2/endpoint.ts | 2 ++ framework/src/modules/dpos_v2/module.ts | 4 ++++ framework/src/modules/dpos_v2/schemas.ts | 8 ++++++++ framework/src/modules/dpos_v2/types.ts | 2 ++ .../dpos_v2/commands/change_commission.spec.ts | 5 +++++ .../test/unit/modules/dpos_v2/module.spec.ts | 8 +++++++- 8 files changed, 43 insertions(+), 6 deletions(-) diff --git a/framework/src/modules/dpos_v2/commands/change_commission.ts b/framework/src/modules/dpos_v2/commands/change_commission.ts index 11d6dc9a033..4584945d048 100644 --- a/framework/src/modules/dpos_v2/commands/change_commission.ts +++ b/framework/src/modules/dpos_v2/commands/change_commission.ts @@ -20,7 +20,6 @@ import { CommandExecuteContext, } from '../../../state_machine'; import { BaseCommand } from '../../base_command'; -import { COMMISSION_INCREASE_PERIOD, MAX_COMMISSION_INCREASE_RATE } from '../constants'; import { CommissionChangeEvent } from '../events/commission_change'; import { changeCommissionCommandParamsSchema } from '../schemas'; import { DelegateStore } from '../stores/delegate'; @@ -29,6 +28,14 @@ import { ChangeCommissionParams } from '../types'; export class ChangeCommissionCommand extends BaseCommand { public schema = changeCommissionCommandParamsSchema; + private _commissionIncreasePeriod!: number; + private _maxCommissionIncreaseRate!: number; + + public init(args: { commissionIncreasePeriod: number; maxCommissionIncreaseRate: number }) { + this._commissionIncreasePeriod = args.commissionIncreasePeriod; + this._maxCommissionIncreaseRate = args.maxCommissionIncreaseRate; + } + public async verify( context: CommandVerifyContext, ): Promise { @@ -54,22 +61,23 @@ export class ChangeCommissionCommand extends BaseCommand { const delegate = await delegateStore.get(context, context.transaction.senderAddress); const oldCommission = delegate.commission; const hasIncreasedCommissionRecently = - context.header.height - delegate.lastCommissionIncreaseHeight < COMMISSION_INCREASE_PERIOD; + context.header.height - delegate.lastCommissionIncreaseHeight < + this._commissionIncreasePeriod; if (context.params.newCommission > oldCommission && hasIncreasedCommissionRecently) { return { status: VerifyStatus.FAIL, error: new Error( - `Can only increase the commission again ${COMMISSION_INCREASE_PERIOD} blocks after the last commission increase.`, + `Can only increase the commission again ${this._commissionIncreasePeriod} blocks after the last commission increase.`, ), }; } - if (context.params.newCommission - oldCommission > MAX_COMMISSION_INCREASE_RATE) { + if (context.params.newCommission - oldCommission > this._maxCommissionIncreaseRate) { return { status: VerifyStatus.FAIL, error: new Error( - `Invalid argument: Commission increase larger than ${MAX_COMMISSION_INCREASE_RATE}.`, + `Invalid argument: Commission increase larger than ${this._maxCommissionIncreaseRate}.`, ), }; } diff --git a/framework/src/modules/dpos_v2/constants.ts b/framework/src/modules/dpos_v2/constants.ts index c4ad75e8a59..02401579cb0 100644 --- a/framework/src/modules/dpos_v2/constants.ts +++ b/framework/src/modules/dpos_v2/constants.ts @@ -64,6 +64,8 @@ export const defaultConfig = { tokenIDFee: TOKEN_ID_FEE.toString('hex'), delegateRegistrationFee: DELEGATE_REGISTRATION_FEE.toString(), maxBFTWeightCap: 500, + commissionIncreasePeriod: COMMISSION_INCREASE_PERIOD, + maxCommissionIncreaseRate: MAX_COMMISSION_INCREASE_RATE, }; export const enum PoSEventResult { diff --git a/framework/src/modules/dpos_v2/endpoint.ts b/framework/src/modules/dpos_v2/endpoint.ts index 6fcee37d26b..6b06c0043b6 100644 --- a/framework/src/modules/dpos_v2/endpoint.ts +++ b/framework/src/modules/dpos_v2/endpoint.ts @@ -141,6 +141,8 @@ export class DPoSEndpoint extends BaseEndpoint { tokenIDFee: this._moduleConfig.tokenIDFee.toString('hex'), delegateRegistrationFee: this._moduleConfig.delegateRegistrationFee.toString(), maxBFTWeightCap: this._moduleConfig.maxBFTWeightCap, + commissionIncreasePeriod: this._moduleConfig.commissionIncreasePeriod, + maxCommissionIncreaseRate: this._moduleConfig.maxCommissionIncreaseRate, }; } diff --git a/framework/src/modules/dpos_v2/module.ts b/framework/src/modules/dpos_v2/module.ts index 70a45b5a8f0..cb7a65ea61f 100644 --- a/framework/src/modules/dpos_v2/module.ts +++ b/framework/src/modules/dpos_v2/module.ts @@ -261,6 +261,10 @@ export class DPoSModule extends BaseModule { roundLength: this._moduleConfig.roundLength, }); this._voteCommand.init({ governanceTokenID: this._moduleConfig.governanceTokenID }); + this._changeCommissionCommand.init({ + commissionIncreasePeriod: this._moduleConfig.commissionIncreasePeriod, + maxCommissionIncreaseRate: this._moduleConfig.maxCommissionIncreaseRate, + }); this.stores.get(EligibleDelegatesStore).init(this._moduleConfig); } diff --git a/framework/src/modules/dpos_v2/schemas.ts b/framework/src/modules/dpos_v2/schemas.ts index 9dd98725985..666f049f19b 100644 --- a/framework/src/modules/dpos_v2/schemas.ts +++ b/framework/src/modules/dpos_v2/schemas.ts @@ -193,6 +193,14 @@ export const configSchema = { minimum: 1, maximum: 9999, }, + commissionIncreasePeriod: { + type: 'integer', + format: 'uint32', + }, + maxCommissionIncreaseRate: { + type: 'integer', + format: 'uint32', + }, }, required: [ 'factorSelfVotes', diff --git a/framework/src/modules/dpos_v2/types.ts b/framework/src/modules/dpos_v2/types.ts index da795e1ffa2..ae4deb974bb 100644 --- a/framework/src/modules/dpos_v2/types.ts +++ b/framework/src/modules/dpos_v2/types.ts @@ -37,6 +37,8 @@ export interface ModuleConfig { tokenIDFee: Buffer; delegateRegistrationFee: bigint; maxBFTWeightCap: number; + commissionIncreasePeriod: number; + maxCommissionIncreaseRate: number; } export type ModuleConfigJSON = JSONObject; diff --git a/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts b/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts index 9b923df069b..094eee41fa8 100644 --- a/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts +++ b/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts @@ -35,6 +35,11 @@ import { CommissionChangeEvent } from '../../../../../src/modules/dpos_v2/events describe('Change Commission command', () => { const dpos = new DPoSModule(); const changeCommissionCommand = new ChangeCommissionCommand(dpos.stores, dpos.events); + changeCommissionCommand.init({ + commissionIncreasePeriod: COMMISSION_INCREASE_PERIOD, + maxCommissionIncreaseRate: MAX_COMMISSION_INCREASE_RATE, + }); + let stateStore: PrefixedStateReadWriter; let delegateStore: DelegateStore; let commissionChangedEvent: CommissionChangeEvent; diff --git a/framework/test/unit/modules/dpos_v2/module.spec.ts b/framework/test/unit/modules/dpos_v2/module.spec.ts index 1ae202fa71d..aae9b309ddf 100644 --- a/framework/test/unit/modules/dpos_v2/module.spec.ts +++ b/framework/test/unit/modules/dpos_v2/module.spec.ts @@ -47,7 +47,11 @@ import { PreviousTimestampStore } from '../../../../src/modules/dpos_v2/stores/p import { GenesisDataStore } from '../../../../src/modules/dpos_v2/stores/genesis'; import { SnapshotStore } from '../../../../src/modules/dpos_v2/stores/snapshot'; import { createStoreGetter } from '../../../../src/testing/utils'; -import { TOKEN_ID_LENGTH } from '../../../../src/modules/dpos_v2/constants'; +import { + COMMISSION_INCREASE_PERIOD, + MAX_COMMISSION_INCREASE_RATE, + TOKEN_ID_LENGTH, +} from '../../../../src/modules/dpos_v2/constants'; import { EligibleDelegatesStore } from '../../../../src/modules/dpos_v2/stores/eligible_delegates'; import { getDelegateWeight, ValidatorWeight } from '../../../../src/modules/dpos_v2/utils'; @@ -69,6 +73,8 @@ describe('DPoS module', () => { tokenIDFee: '0000000000000000', delegateRegistrationFee: (BigInt(10) * BigInt(10) ** BigInt(8)).toString(), maxBFTWeightCap: 500, + commissionIncreasePeriod: COMMISSION_INCREASE_PERIOD, + maxCommissionIncreaseRate: MAX_COMMISSION_INCREASE_RATE, }; const sortValidatorsByWeightDesc = (validators: ValidatorWeight[]) => From d698dbb9b0f94fb80d179501866a0ff892019072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Fri, 11 Nov 2022 11:33:30 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=8C=9E=20It=20was=20a=20beautiful=20s?= =?UTF-8?q?ummer=20day=20when=20someone=20knocked=20on=20her=20door?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/change_commission.spec.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts b/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts index 094eee41fa8..ba5942c8183 100644 --- a/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts +++ b/framework/test/unit/modules/dpos_v2/commands/change_commission.spec.ts @@ -27,6 +27,7 @@ import { DPoSModule } from '../../../../../src'; import { createStoreGetter } from '../../../../../src/testing/utils'; import { COMMISSION_INCREASE_PERIOD, + MAX_COMMISSION, MAX_COMMISSION_INCREASE_RATE, } from '../../../../../src/modules/dpos_v2/constants'; import { createFakeBlockHeader } from '../../../../../src/testing'; @@ -179,6 +180,25 @@ describe('Change Commission command', () => { `Invalid argument: Commission increase larger than ${MAX_COMMISSION_INCREASE_RATE}.`, ); }); + + it('should not allow the commission to be set higher than 100%', async () => { + commandParams.newCommission = MAX_COMMISSION + 1; + transactionDetails.params = codec.encode(schema, commandParams); + transaction = new Transaction(transactionDetails); + + const context = testing + .createTransactionContext({ + stateStore, + transaction, + header: createFakeBlockHeader({ height: COMMISSION_INCREASE_PERIOD + 1 }), + }) + .createCommandVerifyContext(schema); + + const result = await changeCommissionCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude(`must be <= ${MAX_COMMISSION}`); + }); }); describe('execute', () => { From 4776b31dc5106c61e0102d41def3f155e1b8dc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Fri, 11 Nov 2022 11:56:28 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=9A=AA=20Expecting=20her=20granddaugh?= =?UTF-8?q?ter,=20the=20grandma=20happily=20rushed=20to=20open=20the=20doo?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- framework/src/modules/dpos_v2/commands/change_commission.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/modules/dpos_v2/commands/change_commission.ts b/framework/src/modules/dpos_v2/commands/change_commission.ts index 4584945d048..c7fbf1f0169 100644 --- a/framework/src/modules/dpos_v2/commands/change_commission.ts +++ b/framework/src/modules/dpos_v2/commands/change_commission.ts @@ -64,7 +64,7 @@ export class ChangeCommissionCommand extends BaseCommand { context.header.height - delegate.lastCommissionIncreaseHeight < this._commissionIncreasePeriod; - if (context.params.newCommission > oldCommission && hasIncreasedCommissionRecently) { + if (context.params.newCommission >= oldCommission && hasIncreasedCommissionRecently) { return { status: VerifyStatus.FAIL, error: new Error(