Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add delegate-stack-extend command #4610

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ it("statefully interacts with PoX-4", async () => {
amountLocked: 0,
amountUnlocked: initialUstxBalance,
unlockHeight: 0,
firstLockedRewardCycle: 0,
allowedContractCaller: "",
callerAllowedBy: [],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type Wallet = {
amountLocked: number;
amountUnlocked: number;
unlockHeight: number;
firstLockedRewardCycle: number;
allowedContractCaller: StxAddress;
callerAllowedBy: StxAddress[];
};
Expand Down
39 changes: 38 additions & 1 deletion contrib/core-contract-tests/tests/pox-4/pox_Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Cl, cvToValue, OptionalCV, UIntCV } from "@stacks/transactions";
import { RevokeDelegateStxCommand } from "./pox_RevokeDelegateStxCommand";
import { AllowContractCallerCommand } from "./pox_AllowContractCallerCommand";
import { DelegateStackIncreaseCommand } from "./pox_DelegateStackIncreaseCommand";
import { DelegateStackExtendCommand } from "./pox_DelegateStackExtendCommand";

export function PoxCommands(
wallets: Map<StxAddress, Wallet>,
Expand Down Expand Up @@ -168,6 +169,38 @@ export function PoxCommands(
final.increaseBy,
);
}),
// DelegateStackExtendCommand
fc.record({
operator: fc.constantFrom(...wallets.values()),
extendCount: fc.integer({ min: 1, max: 11 }),
}).chain((r) => {
const delegatorsList = r.operator.poolMembers;
const availableStackers = delegatorsList.filter((delegator) => {
const delegatorWallet = wallets.get(delegator)!;
return delegatorWallet.unlockHeight > nextCycleFirstBlock(network);
});

const availableStackersOrFallback = availableStackers.length === 0
? [r.operator.stxAddress]
: availableStackers;

return fc.record({
stacker: fc.constantFrom(...availableStackersOrFallback),
currentCycle: fc.constant(currentCycle(network)),
})
.map((additionalProps) => ({
...r,
stacker: wallets.get(additionalProps.stacker)!,
currentCycle: additionalProps.currentCycle,
}));
}).map((final) =>
new DelegateStackExtendCommand(
final.operator,
final.stacker,
final.extendCount,
final.currentCycle,
)
),
// AllowContractCallerCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
Expand Down Expand Up @@ -208,7 +241,11 @@ export function PoxCommands(
return fc.commands(cmds, { size: "large" });
}

const currentCycle = (network: Simnet) =>
export const REWARD_CYCLE_LENGTH = 1050;

export const FIRST_BURNCHAIN_BLOCK_HEIGHT = 0;

export const currentCycle = (network: Simnet) =>
Number(cvToValue(
network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {
logCommand,
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";
import {
FIRST_BURNCHAIN_BLOCK_HEIGHT,
REWARD_CYCLE_LENGTH,
} from "./pox_Commands.ts";

/**
* The `DelegateStackExtendCommand` allows a pool operator to
* extend an active stacking lock, issuing a "partial commitment"
* for the extended-to cycles.
*
* This method extends stacker's current lockup for an additional
* extend-count and partially commits those new cycles to `pox-addr`.
*
* Constraints for running this command include:
* - Stacker must have locked uSTX.
* - The Operator has to currently be delegated by the Stacker.
* - The new lock period must be less than or equal to 12.
*/
export class DelegateStackExtendCommand implements PoxCommand {
readonly operator: Wallet;
readonly stacker: Wallet;
readonly extendCount: number;
readonly currentCycle: number;

/**
* Constructs a `DelegateStackExtendCommand` to extend the unlock
* height 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 extendCount - Represents the cycles to be expended.
* @param currentCycle - Represents the current PoX reward cycle.
*/
constructor(
operator: Wallet,
stacker: Wallet,
extendCount: number,
currentCycle: number,
) {
this.operator = operator;
this.stacker = stacker;
this.extendCount = extendCount;
this.currentCycle = currentCycle;
}

check(model: Readonly<Stub>): boolean {
// Constraints for running this command include:
// - Stacker must have locked uSTX.
// - The Stacker's uSTX must have been locked by the Operator.
// - The Operator has to currently be delegated by the Stacker.
// - The new lock period must be less than or equal to 12.

const operatorWallet = model.wallets.get(this.operator.stxAddress)!;
const stackerWallet = model.wallets.get(this.stacker.stxAddress)!;

const firstRewardCycle =
this.currentCycle > this.stacker.firstLockedRewardCycle
? this.currentCycle
: this.stacker.firstLockedRewardCycle;
const firstExtendCycle = Math.floor(
(this.stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) /
REWARD_CYCLE_LENGTH,
);
const lastExtendCycle = firstExtendCycle + this.extendCount - 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this value? You are substracting one and adding one afterwards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@friedger I am doing this to simulate how the lock period is calculated inside PoX-4 contract

const totalPeriod = lastExtendCycle - firstRewardCycle + 1;

return (
stackerWallet.amountLocked > 0 &&
stackerWallet.hasDelegated === true &&
stackerWallet.isStacking === true &&
operatorWallet.poolMembers.includes(stackerWallet.stxAddress) &&
operatorWallet.lockedAddresses.includes(stackerWallet.stxAddress) &&
totalPeriod <= 12
);
}

run(model: Stub, real: Real): void {
model.trackCommandRun(this.constructor.name);

// Act
const delegateStackExtend = real.network.callPublicFn(
"ST000000000000000000002AMW42H.pox-4",
"delegate-stack-extend",
[
// (stacker principal)
Cl.principal(this.stacker.stxAddress),
// (pox-addr { version: (buff 1), hashbytes: (buff 32) })
poxAddressToTuple(this.stacker.delegatedPoxAddress),
// (extend-count uint)
Cl.uint(this.extendCount),
],
this.operator.stxAddress,
);

const { result: firstExtendCycle } = real.network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"burn-height-to-reward-cycle",
[Cl.uint(this.stacker.unlockHeight)],
this.operator.stxAddress,
);
assert(isClarityType(firstExtendCycle, ClarityType.UInt));

const lastExtendCycle = Number(firstExtendCycle.value) + this.extendCount -
1;

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

// Assert
expect(delegateStackExtend.result).toBeOk(
Cl.tuple({
stacker: Cl.principal(this.stacker.stxAddress),
"unlock-burn-height": Cl.uint(newUnlockHeight),
}),
);

// 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's unlock height was extended.
stackerWallet.unlockHeight = Number(newUnlockHeight);

// 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} Ӿ ${this.stacker.label}`,
"delegate-stack-extend",
"extend count",
this.extendCount.toString(),
"new unlock height",
this.stacker.unlockHeight.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} delegate-stack-extend extend count ${this.extendCount} previous unlock height ${this.stacker.unlockHeight}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { poxAddressToTuple } from "@stacks/stacking";
import { assert, expect } from "vitest";
import { Cl, ClarityType, isClarityType } from "@stacks/transactions";
import { currentCycle } from "./pox_Commands.ts";

/**
* The `DelegateStackStxCommand` locks STX for stacking within PoX-4 on behalf of a delegator.
Expand Down Expand Up @@ -151,6 +152,7 @@ export class DelegateStackStxCommand implements PoxCommand {
stackerWallet.amountLocked = Number(this.amountUstx);
stackerWallet.unlockHeight = Number(unlockBurnHeight.value);
stackerWallet.amountUnlocked -= Number(this.amountUstx);
stackerWallet.firstLockedRewardCycle = currentCycle(real.network) + 1;
// Add stacker to the operators lock list. This will help knowing that
// the stacker's funds are locked when calling delegate-stack-extend,
// delegate-stack-increase
Expand Down