diff --git a/contrib/core-contract-tests/tests/pox-4/pox-4.stateful-prop.test.ts b/contrib/core-contract-tests/tests/pox-4/pox-4.stateful-prop.test.ts index 615364016..6935d35cf 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox-4.stateful-prop.test.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox-4.stateful-prop.test.ts @@ -100,6 +100,7 @@ it("statefully interacts with PoX-4", async () => { firstLockedRewardCycle: 0, allowedContractCaller: "", callerAllowedBy: [], + committedRewCycleIndexes: [], }; }); diff --git a/contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts b/contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts index 1630f3e61..91f68e68b 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts @@ -64,6 +64,7 @@ export type Wallet = { firstLockedRewardCycle: number; allowedContractCaller: StxAddress; callerAllowedBy: StxAddress[]; + committedRewCycleIndexes: number[]; }; export type PoxCommand = fc.Command; diff --git a/contrib/core-contract-tests/tests/pox-4/pox_Commands.ts b/contrib/core-contract-tests/tests/pox-4/pox_Commands.ts index 238538f08..81f3967b2 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_Commands.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_Commands.ts @@ -15,6 +15,7 @@ import { StackAggregationCommitAuthCommand } from "./pox_StackAggregationCommitA import { StackAggregationCommitSigCommand } from "./pox_StackAggregationCommitSigCommand"; import { StackAggregationCommitIndexedSigCommand } from "./pox_StackAggregationCommitIndexedSigCommand"; import { StackAggregationCommitIndexedAuthCommand } from "./pox_StackAggregationCommitIndexedAuthCommand"; +import { StackAggregationIncreaseCommand } from "./pox_StackAggregationIncreaseCommand"; export function PoxCommands( wallets: Map, @@ -150,6 +151,33 @@ export function PoxCommands( r.currentCycle, ) ), + // StackAggregationIncreaseCommand + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + currentCycle: fc.constant(currentCycle(network)), + }).chain((r) => { + const committedRewCycleIndexesOrFallback = + r.wallet.committedRewCycleIndexes.length > 0 + ? r.wallet.committedRewCycleIndexes + : [-1]; + return fc.record({ + rewardCycleIndex: fc.constantFrom( + ...committedRewCycleIndexesOrFallback, + ), + }).map((cycleIndex) => ({ ...r, ...cycleIndex })); + }).map(( + r: { + wallet: Wallet; + currentCycle: number; + rewardCycleIndex: number; + }, + ) => + new StackAggregationIncreaseCommand( + r.wallet, + r.currentCycle, + r.rewardCycleIndex, + ) + ), // RevokeDelegateStxCommand fc.record({ wallet: fc.constantFrom(...wallets.values()), diff --git a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts index 289b6c03c..22544274c 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts @@ -112,6 +112,7 @@ export class StackAggregationCommitAuthCommand implements PoxCommand { const operatorWallet = model.wallets.get(this.operator.stxAddress)!; operatorWallet.amountToCommit -= committedAmount; + operatorWallet.committedRewCycleIndexes.push(model.nextRewardSetIndex); model.nextRewardSetIndex++; // Log to console for debugging purposes. This is not necessary for the diff --git a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts index f33b509fc..54e4639d7 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts @@ -116,6 +116,7 @@ export class StackAggregationCommitIndexedAuthCommand implements PoxCommand { // Update the model const operatorWallet = model.wallets.get(this.operator.stxAddress)!; operatorWallet.amountToCommit -= committedAmount; + operatorWallet.committedRewCycleIndexes.push(model.nextRewardSetIndex); model.nextRewardSetIndex++; // Log to console for debugging purposes. This is not necessary for the diff --git a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts index 673625f30..3ee837852 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts @@ -116,6 +116,7 @@ export class StackAggregationCommitIndexedSigCommand implements PoxCommand { // Update the model const operatorWallet = model.wallets.get(this.operator.stxAddress)!; operatorWallet.amountToCommit -= committedAmount; + operatorWallet.committedRewCycleIndexes.push(model.nextRewardSetIndex); model.nextRewardSetIndex++; // Log to console for debugging purposes. This is not necessary for the diff --git a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts index ed7b99447..e1bf23364 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts @@ -112,6 +112,7 @@ export class StackAggregationCommitSigCommand implements PoxCommand { const operatorWallet = model.wallets.get(this.operator.stxAddress)!; operatorWallet.amountToCommit -= committedAmount; + operatorWallet.committedRewCycleIndexes.push(model.nextRewardSetIndex); model.nextRewardSetIndex++; // Log to console for debugging purposes. This is not necessary for the diff --git a/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts new file mode 100644 index 000000000..e15ac26c5 --- /dev/null +++ b/contrib/core-contract-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts @@ -0,0 +1,109 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; + +/** + * The `StackAggregationIncreaseCommand` allows an operator to commit + * partially stacked STX to a PoX address which has already received + * some STX (more than the `stacking minimum`). + * This allows a delegator to lock up marginally more STX from new + * delegates, even if they collectively do not exceed the Stacking + * minimum, so long as the target PoX address already represents at + * least as many STX as the `stacking minimum`. + * This command calls stack-aggregation-increase. + * + * Constraints for running this command include: + * - The Operator must have locked STX on behalf of at least one stacker. + * - The PoX address must have partial committed STX. + * - The Reward Cycle Index must be positive. + */ +export class StackAggregationIncreaseCommand implements PoxCommand { + readonly operator: Wallet; + readonly currentCycle: number; + readonly rewardCycleIndex: number; + + /** + * Constructs a `StackAggregationIncreaseCommand` to commit partially + * stacked STX to a PoX address which has already received some STX. + * + * @param operator - Represents the `Operator`'s wallet. + * @param currentCycle - The current reward cycle. + * @param rewardCycleIndex - The cycle index to increase the commit for. + */ + constructor( + operator: Wallet, + currentCycle: number, + rewardCycleIndex: number, + ) { + this.operator = operator; + this.currentCycle = currentCycle; + this.rewardCycleIndex = rewardCycleIndex; + } + + check(_model: Readonly): boolean { + // Constraints for running this command include: + // - The Operator must have locked STX on behalf of at least one stacker. + // - The PoX address must have partial committed STX. + // - The Reward Cycle Index must be positive. + + return ( + this.operator.lockedAddresses.length > 0 && + this.rewardCycleIndex >= 0 && + this.operator.amountToCommit > 0 + ); + } + + run(model: Stub, real: Real): void { + model.trackCommandRun(this.constructor.name); + + const committedAmount = this.operator.amountToCommit; + + // Act + const stackAggregationIncrease = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-increase", + [ + // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(this.currentCycle + 1), + // (reward-cycle-index uint)) + Cl.uint(this.rewardCycleIndex), + ], + this.operator.stxAddress, + ); + + // Assert + expect(stackAggregationIncrease.result).toBeOk(Cl.bool(true)); + + const operatorWallet = model.wallets.get(this.operator.stxAddress)!; + operatorWallet.amountToCommit -= committedAmount; + + // Log to console for debugging purposes. This is not necessary for the + // test to pass but it is useful for debugging and eyeballing the test. + logCommand( + `✓ ${this.operator.label}`, + "stack-agg-increase", + "amount committed", + committedAmount.toString(), + "cycle index", + this.rewardCycleIndex.toString(), + ); + } + + toString() { + // fast-check will call toString() in case of errors, e.g. property failed. + // It will then make a minimal counterexample, a process called 'shrinking' + // https://github.com/dubzzz/fast-check/issues/2864#issuecomment-1098002642 + return `${this.operator.label} stack-aggregation-increase for reward cycle ${ + this.currentCycle + 1 + } index ${this.rewardCycleIndex}`; + } +} \ No newline at end of file