Skip to content

Commit

Permalink
test(pox-4): Add DelegateStackStxCommand
Browse files Browse the repository at this point in the history
Add: generated amount for DelegateStackStx

Add: generator limits for startBurnHt using Simnet

Fix: address `hasPoolMembers`, `stacker balance check` comments

Fix: command descriptions

Add: amount generator for DelegateStx; Fix: command logs
  • Loading branch information
BowTiedRadone authored and moodmosaic committed Mar 22, 2024
1 parent 9f5ac7c commit fe1b3af
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 29 deletions.
@@ -1,6 +1,5 @@
import { describe, it } from "vitest";

import { initSimnet } from "@hirosystems/clarinet-sdk";
import { initSimnet, Simnet } from "@hirosystems/clarinet-sdk";
import { Real, Stub, StxAddress, Wallet } from "./pox_CommandModel.ts";

import {
Expand All @@ -9,7 +8,9 @@ import {
} from "@stacks/encryption";
import { StacksDevnet } from "@stacks/network";
import {
Cl,
createStacksPrivateKey,
cvToValue,
getAddressFromPrivateKey,
TransactionVersion,
} from "@stacks/transactions";
Expand All @@ -18,6 +19,36 @@ import { StackingClient } from "@stacks/stacking";
import fc from "fast-check";
import { PoxCommands } from "./pox_Commands.ts";

export const currentCycle = (network: Simnet) =>
Number(cvToValue(
network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"current-pox-reward-cycle",
[],
"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
).result,
));

export const currentCycleFirstBlock = (network: Simnet) =>
Number(cvToValue(
network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"reward-cycle-to-burn-height",
[Cl.uint(currentCycle(network))],
"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
).result,
));

export const nextCycleFirstBlock = (network: Simnet) =>
Number(cvToValue(
network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"reward-cycle-to-burn-height",
[Cl.uint(currentCycle(network) + 1)],
"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
).result,
));

describe("PoX-4 invariant tests", () => {
it("statefully does solo stacking with a signature", async () => {
// SUT stands for "System Under Test".
Expand Down Expand Up @@ -65,7 +96,9 @@ describe("PoX-4 invariant tests", () => {
ustxBalance: initialUstxBalance,
isStacking: false,
hasDelegated: false,
hasPoolMembers: [],
delegatedTo: "",
delegatedMaxAmount: 0,
amountLocked: 0,
amountUnlocked: initialUstxBalance,
unlockHeight: 0,
Expand All @@ -81,7 +114,7 @@ describe("PoX-4 invariant tests", () => {

fc.assert(
fc.property(
PoxCommands(model.wallets),
PoxCommands(model.wallets, sut.network),
(cmds) => {
const initialState = () => ({ model: model, real: sut });
fc.modelRun(initialState, cmds);
Expand Down
2 changes: 2 additions & 0 deletions contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts
Expand Up @@ -25,7 +25,9 @@ export type Wallet = {
ustxBalance: number;
isStacking: boolean;
hasDelegated: boolean;
hasPoolMembers: StxAddress[];
delegatedTo: StxAddress;
delegatedMaxAmount: number;
amountLocked: number;
amountUnlocked: number;
unlockHeight: number;
Expand Down
38 changes: 34 additions & 4 deletions contrib/core-contract-tests/tests/pox-4/pox_Commands.ts
Expand Up @@ -4,9 +4,12 @@ import { GetStackingMinimumCommand } from "./pox_GetStackingMinimumCommand";
import { GetStxAccountCommand } from "./pox_GetStxAccountCommand";
import { StackStxCommand } from "./pox_StackStxCommand";
import { DelegateStxCommand } from "./pox_DelegateStxCommand";
import { DelegateStackStxCommand } from "./pox_DelegateStackStxCommand";
import { Simnet } from "@hirosystems/clarinet-sdk";
import { currentCycleFirstBlock, nextCycleFirstBlock } from "./pox-4.stateful-prop.test";

export function PoxCommands(
wallets: Map<StxAddress, Wallet>,
wallets: Map<StxAddress, Wallet>, network: Simnet,
): fc.Arbitrary<Iterable<fc.Command<Stub, Real>>> {
const cmds = [
// GetStackingMinimumCommand
Expand Down Expand Up @@ -47,20 +50,47 @@ export function PoxCommands(
wallet: fc.constantFrom(...wallets.values()),
delegateTo: fc.constantFrom(...wallets.values()),
untilBurnHt: fc.integer({ min: 1 }),
margin: fc.integer({ min: 1, max: 9 }),
amount: fc.bigInt({ min:0n, max: 100_000_000_000_000n }),
}).map((
r: {
wallet: Wallet;
delegateTo: Wallet;
untilBurnHt: number;
margin: number;
amount: bigint;
},
) =>
new DelegateStxCommand(
r.wallet,
r.delegateTo,
r.untilBurnHt,
r.margin,
r.amount,
)
),
// DelegateStackStxCommand
fc.record({
operator: fc.constantFrom(...wallets.values()),
stacker: fc.constantFrom(...wallets.values()),
startBurnHt: fc.integer({
min: currentCycleFirstBlock(network),
max: nextCycleFirstBlock(network),
}),
period: fc.integer({ min: 1, max: 12 }),
amount: fc.bigInt({ min:0n, max: 100_000_000_000_000n }),
}).map((
r: {
operator: Wallet;
stacker: Wallet;
startBurnHt: number;
period: number;
amount: bigint;
},
) =>
new DelegateStackStxCommand(
r.operator,
r.stacker,
r.startBurnHt,
r.period,
r.amount
)
),
// GetStxAccountCommand
Expand Down
156 changes: 156 additions & 0 deletions contrib/core-contract-tests/tests/pox-4/pox_DelegateStackStxCommand.ts
@@ -0,0 +1,156 @@
import { PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel.ts";
import { poxAddressToTuple } from "@stacks/stacking";
import { assert, expect } from "vitest";
import { Cl, ClarityType, isClarityType } from "@stacks/transactions";

/**
* The `DelegateStackStxCommand` locks STX for stacking within PoX-4 on behalf of a delegator.
* This operation allows the `operator` to stack the `stacker`'s STX.
*
* Constraints for running this command include:
* - A minimum threshold of uSTX must be met, determined by the
* `get-stacking-minimum` function at the time of this call.
* - The Stacker cannot currently be engaged in another stacking
* operation.
* - The Stacker has to currently be delegating to the Operator.
* - The stacked STX amount should be less than or equal to the
* delegated amount.
* - The stacked uSTX amount should be less than or equal to the
* Stacker's balance
* - The stacked uSTX amount should be greater than or equal to the
* minimum threshold of uSTX
* - The Operator has to currently be delegated by the Stacker.
*/
export class DelegateStackStxCommand implements PoxCommand {
readonly operator: Wallet;
readonly stacker: Wallet;
readonly startBurnHt: number;
readonly period: number;
readonly amountUstx: bigint;

/**
* Constructs a `DelegateStackStxCommand` to lock uSTX as a Pool Operator
* on behalf of a Stacker.
*
* @param operator - Represents the Pool Operator's wallet.
* @param stacker - Represents the STacker's wallet.
* @param startBurnHt - A burn height inside the current reward cycle.
* @param period - Number of reward cycles to lock uSTX.
* @param amountUstx - The uSTX amount stacked by the Operator on behalf
* of the Stacker
*/
constructor(
operator: Wallet,
stacker: Wallet,
startBurnHt: number,
period: number,
amountUstx: bigint,
) {
this.operator = operator;
this.stacker = stacker;
this.startBurnHt = startBurnHt;
this.period = period;
this.amountUstx = amountUstx;
}

check(model: Readonly<Stub>): boolean {
// Constraints for running this command include:
// - A minimum threshold of uSTX must be met, determined by the
// `get-stacking-minimum` function at the time of this call.
// - The Stacker cannot currently be engaged in another stacking
// operation
// - The Stacker has to currently be delegating to the Operator
// - The stacked uSTX amount should be less than or equal to the
// delegated amount
// - The stacked uSTX amount should be less than or equal to the
// Stacker's balance
// - The stacked uSTX amount should be greater than or equal to the
// minimum threshold of uSTX
// - The Operator has to currently be delegated by the Stacker

const operatorWallet = model.wallets.get(this.operator.stxAddress)!;
const stackerWallet = model.wallets.get(this.stacker.stxAddress)!;
return (
model.stackingMinimum > 0 &&
!stackerWallet.isStacking &&
stackerWallet.hasDelegated &&
stackerWallet.delegatedMaxAmount >= Number(this.amountUstx) &&
Number(this.amountUstx) <= stackerWallet.ustxBalance &&
Number(this.amountUstx) >= model.stackingMinimum &&
operatorWallet.hasPoolMembers.includes(stackerWallet.stxAddress)
);
}

run(model: Stub, real: Real): void {
// Act
const delegateStackStx = real.network.callPublicFn(
"ST000000000000000000002AMW42H.pox-4",
"delegate-stack-stx",
[
// (stacker principal)
Cl.principal(this.stacker.stxAddress),
// (amount-ustx uint)
Cl.uint(this.amountUstx),
// (pox-addr { version: (buff 1), hashbytes: (buff 32) })
poxAddressToTuple(this.operator.btcAddress),
// (start-burn-ht uint)
Cl.uint(this.startBurnHt),
// (lock-period uint)
Cl.uint(this.period)
],
this.operator.stxAddress,
);
const { result: rewardCycle } = real.network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"burn-height-to-reward-cycle",
[Cl.uint(real.network.blockHeight)],
this.operator.stxAddress,
);
assert(isClarityType(rewardCycle, ClarityType.UInt));

const { result: unlockBurnHeight } = real.network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"reward-cycle-to-burn-height",
[Cl.uint(Number(rewardCycle.value) + this.period + 1)],
this.operator.stxAddress,
);
assert(isClarityType(unlockBurnHeight, ClarityType.UInt));

// Assert
expect(delegateStackStx.result).toBeOk(
Cl.tuple({
stacker: Cl.principal(this.stacker.stxAddress),
"lock-amount": Cl.uint(this.amountUstx),
"unlock-burn-height": Cl.uint(Number(unlockBurnHeight.value)),
}),
);

// Get the Stacker's wallet from the model and update it with the new state.
const stackerWallet = model.wallets.get(this.stacker.stxAddress)!;
// Update model so that we know this wallet is stacking. This is important
// in order to prevent the test from stacking multiple times with the same
// address.
stackerWallet.isStacking = true;
// Update locked, unlocked, and unlock-height fields in the model.
stackerWallet.amountLocked = Number(this.amountUstx);
stackerWallet.unlockHeight = Number(unlockBurnHeight.value);
stackerWallet.amountUnlocked -= Number(this.amountUstx);

// 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.
console.info(
`✓ ${this.operator.label.padStart(8, " ")} Ӿ ${this.stacker.label.padStart(8, " ")} ${
"delegate-stack-stx".padStart(23, " ")
} ${"lock-amount".padStart(12, " ")} ${
this.amountUstx.toString().padStart(15, " ")
} ${"until".padStart(37)} ${this.stacker.unlockHeight.toString().padStart(17)}`,
);
}

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} delegate-stack-stx period ${this.period}`;
}
}
34 changes: 15 additions & 19 deletions contrib/core-contract-tests/tests/pox-4/pox_DelegateStxCommand.ts
Expand Up @@ -4,9 +4,9 @@ import { expect } from "vitest";
import { boolCV, Cl } from "@stacks/transactions";

/**
* The `DelegateStxCommand` delegates STX for stacking within PoX-4. This self-service
* operation allows the `tx-sender` (the `wallet` in this case) to delegate stacking
* participation to a `delegatee`.
* The `DelegateStxCommand` delegates STX for stacking within PoX-4. This operation
* allows the `tx-sender` (the `wallet` in this case) to delegate stacking participation
* to a `delegatee`.
*
* Constraints for running this command include:
* - The Stacker cannot currently be a delegator in another delegation.
Expand All @@ -16,27 +16,27 @@ export class DelegateStxCommand implements PoxCommand {
readonly wallet: Wallet;
readonly delegateTo: Wallet;
readonly untilBurnHt: number;
readonly margin: number;
readonly amount: bigint;

/**
* Constructs a `DelegateStxCommand` to delegate uSTX for stacking.
*
* @param wallet - Represents the Stacker's wallet.
* @param delegateTo - Represents the Delegatee's STX address.
* @param untilBurnHt - The burn block height until the delegation is valid.
* @param margin - Multiplier for minimum required uSTX to stack so that each
* Stacker locks a different amount of uSTX across test runs.
* @param amount - The maximum amount the `Stacker` delegates the `Delegatee` to
* stack on his behalf
*/
constructor(
wallet: Wallet,
delegateTo: Wallet,
untilBurnHt: number,
margin: number,
amount: bigint,
) {
this.wallet = wallet;
this.delegateTo = delegateTo;
this.untilBurnHt = untilBurnHt;
this.margin = margin;
this.amount = amount;
}

check(model: Readonly<Stub>): boolean {
Expand All @@ -50,16 +50,9 @@ export class DelegateStxCommand implements PoxCommand {

run(model: Stub, real: Real): void {
// The amount of uSTX delegated by the Stacker to the Delegatee.
// For our tests, we will use the minimum amount of uSTX to be stacked
// in the given reward cycle multiplied by the margin, which is a randomly
// generated number passed to the constructor of this class. Even if there
// are no constraints about the delegated amount, it will be checked in the
// future, when calling delegate-stack-stx.
const delegatedAmount = model.stackingMinimum * this.margin;

// The amount of uSTX to be delegated. For this test, we will use the
// delegated amount calculated before.
const amountUstx = delegatedAmount;
// Even if there are no constraints about the delegated amount,
// it will be checked in the future, when calling delegate-stack-stx.
const amountUstx = Number(this.amount);

// Act
const delegateStx = real.network.callPublicFn(
Expand All @@ -83,12 +76,15 @@ export class DelegateStxCommand implements PoxCommand {

// Get the wallet from the model and update it with the new state.
const wallet = model.wallets.get(this.wallet.stxAddress)!;
const delegatedWallet = model.wallets.get(this.delegateTo.stxAddress)!;
// Update model so that we know this wallet has delegated. This is important
// in order to prevent the test from delegating multiple times with the same
// address.
wallet.hasDelegated = true;
wallet.delegatedTo = this.delegateTo.stxAddress;
wallet.delegatedMaxAmount = amountUstx;

delegatedWallet.hasPoolMembers.push(wallet.stxAddress);
// 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.
console.info(
Expand All @@ -100,7 +96,7 @@ export class DelegateStxCommand implements PoxCommand {
} ${"amount".padStart(12, " ")} ${
amountUstx
.toString()
.padStart(13, " ")
.padStart(15, " ")
} delegated to ${
this.delegateTo.label.padStart(
42,
Expand Down

0 comments on commit fe1b3af

Please sign in to comment.