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 805ea2f53a..4331f434a2 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 @@ -71,6 +71,8 @@ describe("PoX-4 invariant tests", () => { amountLocked: 0, amountUnlocked: initialUstxBalance, unlockHeight: 0, + allowedContractCaller: '', + callerAllowedBy: [] }; }); diff --git a/contrib/core-contract-tests/tests/pox-4/pox_AllowContractCallerCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_AllowContractCallerCommand.ts new file mode 100644 index 0000000000..d8988e1309 --- /dev/null +++ b/contrib/core-contract-tests/tests/pox-4/pox_AllowContractCallerCommand.ts @@ -0,0 +1,99 @@ +import { PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel.ts"; +import { expect } from "vitest"; +import { boolCV, Cl, ClarityType, OptionalCV, UIntCV } from "@stacks/transactions"; + +/** + * The `AllowContractCallerComand` gives a `contract-caller` authorization to call stacking methods. + * Normally, stacking methods may only be invoked by direct transactions (i.e., the tx-sender + * issues a direct contract-call to the stacking methods). + * By issuing an allowance, the tx-sender may call stacking methods through the allowed contract. + * + * There are no constraints for running this command. + */ +export class AllowContractCallerCommand implements PoxCommand { + readonly wallet: Wallet; + readonly allowanceTo: Wallet; + readonly allowUntilBurnHt: OptionalCV; + + /** + * Constructs an `AllowContractCallerComand` that authorizes a `contract-caller` to call + * stacking methods. + * + * @param wallet - Represents the Stacker's wallet. + * @param allowanceTo - Represents the authorized `contract-caller` (i.e. a stacking pool) + * @param alllowUntilBurnHt - The burn block height until the authorization is valid. + */ + + constructor( + wallet: Wallet, + allowanceTo: Wallet, + allowUntilBurnHt: OptionalCV, + ) { + this.wallet = wallet; + this.allowanceTo = allowanceTo; + this.allowUntilBurnHt = allowUntilBurnHt; + } + + check(): boolean { + // There are no constraints for running this command. + return true; + } + + run(model: Stub, real: Real): void { + // Act + const allowContractCaller = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "allow-contract-caller", + [ + // (caller principal) + Cl.principal(this.allowanceTo.stxAddress), + // (until-burn-ht (optional uint)) + this.allowUntilBurnHt, + ], + this.wallet.stxAddress, + ); + + // Assert + expect(allowContractCaller.result).toBeOk(boolCV(true)); + + // Get the wallets involved from the model and update it with the new state. + const wallet = model.wallets.get(this.wallet.stxAddress)!; + const callerToAllow = model.wallets.get(this.allowanceTo.stxAddress)!; + // Update model so that we know this wallet has authorized a contract-caller. + + wallet.allowedContractCaller = this.allowanceTo.stxAddress; + callerToAllow.callerAllowedBy.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( + `✓ ${ + this.wallet.label.padStart( + 8, + " ", + ) + } ${ + "allow-contract-caller".padStart( + 34, + " ", + ) + } ${this.allowanceTo.label.padStart(12, " ")} ${"until".padStart(53)} ${ + optionalCVToString(this.allowUntilBurnHt).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.wallet.stxAddress} allow-contract-caller ${this.allowanceTo.stxAddress} until burn ht ${ + optionalCVToString(this.allowUntilBurnHt) + }`; + } +} + +const optionalCVToString = (optional: OptionalCV): string => + optional.type === ClarityType.OptionalSome + ? (optional.value as UIntCV).value.toString() + : "none"; 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 138027d5d5..ad5b3dd2fa 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts @@ -32,6 +32,8 @@ export type Wallet = { amountLocked: number; amountUnlocked: number; unlockHeight: number; + allowedContractCaller: StxAddress; + callerAllowedBy: StxAddress[]; }; 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 92d2b87ae5..859316451c 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_Commands.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_Commands.ts @@ -6,8 +6,9 @@ import { StackStxCommand } from "./pox_StackStxCommand"; import { DelegateStxCommand } from "./pox_DelegateStxCommand"; import { DelegateStackStxCommand } from "./pox_DelegateStackStxCommand"; import { Simnet } from "@hirosystems/clarinet-sdk"; -import { Cl, cvToValue } from "@stacks/transactions"; +import { Cl, cvToValue, OptionalCV, UIntCV } from "@stacks/transactions"; import { RevokeDelegateStxCommand } from "./pox_RevokeDelegateStxCommand"; +import { AllowContractCallerCommand } from "./pox_AllowContractCallerCommand"; export function PoxCommands( wallets: Map, network: Simnet, @@ -70,15 +71,9 @@ export function PoxCommands( // RevokeDelegateStxCommand fc.record({ wallet: fc.constantFrom(...wallets.values()), - delegateTo: fc.constantFrom(...wallets.values()), - untilBurnHt: fc.integer({ min: 1 }), - amount: fc.bigInt({ min:0n, max: 100_000_000_000_000n }), }).map(( r: { wallet: Wallet; - delegateTo: Wallet; - untilBurnHt: number; - amount: bigint; }, ) => new RevokeDelegateStxCommand( @@ -95,23 +90,46 @@ export function PoxCommands( }), 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; - }, - ) => + }).chain((r) => + fc.record({ + unlockBurnHt: fc.constant( + currentCycleFirstBlock(network) + 1050 * (r.period + 1), + ), + }).map((unlockBurnHtRecord) => ({ + ...r, + ...unlockBurnHtRecord, + })) + ).map((r) => new DelegateStackStxCommand( r.operator, r.stacker, r.startBurnHt, r.period, - r.amount + r.amount, + r.unlockBurnHt, ) ), + // AllowContractCallerCommand + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + allowanceTo: fc.constantFrom(...wallets.values()), + alllowUntilBurnHt: fc.oneof( + fc.constant(Cl.none()), + fc.integer({ min: 1 }).map((value) => Cl.some(Cl.uint(value))), + ), + }) + .map( + (r: { + wallet: Wallet; + allowanceTo: Wallet; + alllowUntilBurnHt: OptionalCV; + }) => + new AllowContractCallerCommand( + r.wallet, + r.allowanceTo, + r.alllowUntilBurnHt, + ), + ), // GetStxAccountCommand fc.record({ wallet: fc.constantFrom(...wallets.values()), @@ -141,7 +159,7 @@ const currentCycle = (network: Simnet) => ).result, )); -const currentCycleFirstBlock = (network: Simnet) => +export const currentCycleFirstBlock = (network: Simnet) => Number(cvToValue( network.callReadOnlyFn( "ST000000000000000000002AMW42H.pox-4", diff --git a/contrib/core-contract-tests/tests/pox-4/pox_DelegateStackStxCommand.ts b/contrib/core-contract-tests/tests/pox-4/pox_DelegateStackStxCommand.ts index 4fb9ed09a4..f23b4817a5 100644 --- a/contrib/core-contract-tests/tests/pox-4/pox_DelegateStackStxCommand.ts +++ b/contrib/core-contract-tests/tests/pox-4/pox_DelegateStackStxCommand.ts @@ -16,10 +16,11 @@ import { Cl, ClarityType, isClarityType } from "@stacks/transactions"; * - 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 + * Stacker's balance. * - The stacked uSTX amount should be greater than or equal to the - * minimum threshold of uSTX + * minimum threshold of uSTX. * - The Operator has to currently be delegated by the Stacker. + * - The Period has to fit the last delegation burn block height. */ export class DelegateStackStxCommand implements PoxCommand { readonly operator: Wallet; @@ -27,6 +28,7 @@ export class DelegateStackStxCommand implements PoxCommand { readonly startBurnHt: number; readonly period: number; readonly amountUstx: bigint; + readonly unlockBurnHt: number; /** * Constructs a `DelegateStackStxCommand` to lock uSTX as a Pool Operator @@ -38,6 +40,7 @@ export class DelegateStackStxCommand implements PoxCommand { * @param period - Number of reward cycles to lock uSTX. * @param amountUstx - The uSTX amount stacked by the Operator on behalf * of the Stacker + * @param unlockBurnHt - The burn height at which the uSTX is unlocked. */ constructor( operator: Wallet, @@ -45,12 +48,14 @@ export class DelegateStackStxCommand implements PoxCommand { startBurnHt: number, period: number, amountUstx: bigint, + unlockBurnHt: number, ) { this.operator = operator; this.stacker = stacker; this.startBurnHt = startBurnHt; this.period = period; this.amountUstx = amountUstx; + this.unlockBurnHt = unlockBurnHt; } check(model: Readonly): boolean { @@ -58,18 +63,20 @@ export class DelegateStackStxCommand implements PoxCommand { // - 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 + // 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 + // delegated amount. // - The stacked uSTX amount should be less than or equal to the - // Stacker's balance + // 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 + // minimum threshold of uSTX. + // - The Operator has to currently be delegated by the Stacker. + // - The Period has to fit the last delegation burn block height. const operatorWallet = model.wallets.get(this.operator.stxAddress)!; const stackerWallet = model.wallets.get(this.stacker.stxAddress)!; + return ( model.stackingMinimum > 0 && !stackerWallet.isStacking && @@ -77,7 +84,8 @@ export class DelegateStackStxCommand implements PoxCommand { stackerWallet.delegatedMaxAmount >= Number(this.amountUstx) && Number(this.amountUstx) <= stackerWallet.ustxBalance && Number(this.amountUstx) >= model.stackingMinimum && - operatorWallet.hasPoolMembers.includes(stackerWallet.stxAddress) + operatorWallet.hasPoolMembers.includes(stackerWallet.stxAddress) && + this.unlockBurnHt <= stackerWallet.delegatedUntilBurnHt ); }