diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/err_Commands.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/err_Commands.ts new file mode 100644 index 0000000000..7e7a4f0e95 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/err_Commands.ts @@ -0,0 +1,2147 @@ +import fc from "fast-check"; +import { Simnet } from "@hirosystems/clarinet-sdk"; +import { + hasLockedStackers, + hasPoolMembers, + isAllowedContractCaller, + isAmountAboveThreshold, + isAmountLockedPositive, + isAmountWithinBalance, + isAmountWithinDelegationLimit, + isATCAboveThreshold, + isATCPositive, + isCallerAllowedByStacker, + isIncreaseByGTZero, + isIncreaseByWithinUnlockedBalance, + isPeriodWithinMax, + isStackerDelegatingToOperator, + isDelegating, + isStacking, + isStackingSolo, + isStackingMinimumCalculated, + isUBHWithinDelegationLimit, + isUnlockedWithinCurrentRC, + isStackerInOperatorPool, + isStackerLockedByOperator, + PoxCommand, + Stacker, + StxAddress, + Wallet, + isPositive, +} from "./pox_CommandModel"; +import { + currentCycle, + currentCycleFirstBlock, + FIRST_BURNCHAIN_BLOCK_HEIGHT, + nextCycleFirstBlock, + REWARD_CYCLE_LENGTH, +} from "./pox_Commands"; +import { DelegateStackExtendCommand_Err } from "./pox_DelegateStackExtendCommand_Err"; +import { DelegateStackIncreaseCommand_Err } from "./pox_DelegateStackIncreaseCommand_Err"; +import { DelegateStackStxCommand_Err } from "./pox_DelegateStackStxCommand_Err"; +import { DelegateStxCommand_Err } from "./pox_DelegateStxCommand_Err"; +import { RevokeDelegateStxCommand_Err } from "./pox_RevokeDelegateStxCommand_Err"; +import { StackAggregationCommitAuthCommand_Err } from "./pox_StackAggregationCommitAuthCommand_Err"; +import { StackAggregationCommitIndexedAuthCommand_Err } from "./pox_StackAggregationCommitIndexedAuthCommand_Err"; +import { StackAggregationCommitIndexedSigCommand_Err } from "./pox_StackAggregationCommitIndexedSigCommand_Err"; +import { StackAggregationCommitSigCommand_Err } from "./pox_StackAggregationCommitSigCommand_Err"; +import { StackAggregationIncreaseCommand_Err } from "./pox_StackAggregationIncreaseCommand_Err"; +import { StackExtendAuthCommand_Err } from "./pox_StackExtendAuthCommand_Err"; +import { StackExtendSigCommand_Err } from "./pox_StackExtendSigCommand_Err"; +import { StackIncreaseAuthCommand_Err } from "./pox_StackIncreaseAuthCommand_Err"; +import { StackIncreaseSigCommand_Err } from "./pox_StackIncreaseSigCommand_Err"; +import { StackStxAuthCommand_Err } from "./pox_StackStxAuthCommand_Err"; +import { StackStxSigCommand_Err } from "./pox_StackStxSigCommand_Err"; +import { DisallowContractCallerCommand_Err } from "./pox_DisallowContractCallerCommand_Err"; + +const POX_4_ERRORS = { + ERR_STACKING_INSUFFICIENT_FUNDS: 1, + ERR_STACKING_INVALID_LOCK_PERIOD: 2, + ERR_STACKING_ALREADY_STACKED: 3, + ERR_STACKING_NO_SUCH_PRINCIPAL: 4, + ERR_STACKING_PERMISSION_DENIED: 9, + ERR_STACKING_THRESHOLD_NOT_MET: 11, + ERR_STACKING_INVALID_AMOUNT: 18, + ERR_STACKING_ALREADY_DELEGATED: 20, + ERR_DELEGATION_TOO_MUCH_LOCKED: 22, + ERR_STACK_EXTEND_NOT_LOCKED: 26, + ERR_STACKING_IS_DELEGATED: 30, + ERR_STACKING_NOT_DELEGATED: 31, + ERR_DELEGATION_ALREADY_REVOKED: 34, +}; + +export function ErrCommands( + wallets: Map, + stackers: Map, + network: Simnet, +): fc.Arbitrary[] { + const cmds = [ + // StackStxAuthCommand_Err_Stacking_Already_Stacked_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + period: fc.integer({ min: 1, max: 12 }), + margin: fc.integer({ min: 1, max: 9 }), + }).map(( + r: { + wallet: Wallet; + authId: number; + period: number; + margin: number; + }, + ) => + new StackStxAuthCommand_Err( + r.wallet, + r.authId, + r.period, + r.margin, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "StackStxAuthCommand_Err_Stacking_Already_Stacked_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_STACKED, + ) + ), + // StackStxAuthCommand_Err_Stacking_Already_Stacked_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + period: fc.integer({ min: 1, max: 12 }), + margin: fc.integer({ min: 1, max: 9 }), + }).map(( + r: { + wallet: Wallet; + authId: number; + period: number; + margin: number; + }, + ) => + new StackStxAuthCommand_Err( + r.wallet, + r.authId, + r.period, + r.margin, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "StackStxAuthCommand_Err_Stacking_Already_Stacked_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_STACKED, + ) + ), + // StackStxAuthCommand_Err_Stacking_Already_Delegated + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + period: fc.integer({ min: 1, max: 12 }), + margin: fc.integer({ min: 1, max: 9 }), + }).map(( + r: { + wallet: Wallet; + authId: number; + period: number; + margin: number; + }, + ) => + new StackStxAuthCommand_Err( + r.wallet, + r.authId, + r.period, + r.margin, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stacker) || + !isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "StackStxAuthCommand_Err_Stacking_Already_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_DELEGATED, + ) + ), + // StackStxSigCommand_Err_Stacking_Already_Stacked_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + period: fc.integer({ min: 1, max: 12 }), + margin: fc.integer({ min: 1, max: 9 }), + }).map(( + r: { + wallet: Wallet; + authId: number; + period: number; + margin: number; + }, + ) => + new StackStxSigCommand_Err( + r.wallet, + r.authId, + r.period, + r.margin, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !(isStackingMinimumCalculated(model)) || + !isStacking(stacker) || + isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "StackStxSigCommand_Err_Stacking_Already_Stacked_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_STACKED, + ) + ), + // StackStxSigCommand_Err_Stacking_Already_Stacked_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + period: fc.integer({ min: 1, max: 12 }), + margin: fc.integer({ min: 1, max: 9 }), + }).map(( + r: { + wallet: Wallet; + authId: number; + period: number; + margin: number; + }, + ) => + new StackStxSigCommand_Err( + r.wallet, + r.authId, + r.period, + r.margin, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "StackStxSigCommand_Err_Stacking_Already_Stacked_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_STACKED, + ) + ), + // StackStxSigCommand_Err_Stacking_Already_Delegated + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + period: fc.integer({ min: 1, max: 12 }), + margin: fc.integer({ min: 1, max: 9 }), + }).map(( + r: { + wallet: Wallet; + authId: number; + period: number; + margin: number; + }, + ) => + new StackStxSigCommand_Err( + r.wallet, + r.authId, + r.period, + r.margin, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stacker) || + !isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "StackStxSigCommand_Err_Stacking_Already_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_DELEGATED, + ) + ), + // RevokeDelegateStxCommand_Err_Delegation_Already_Revoked + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + }).map(( + r: { + wallet: Wallet; + }, + ) => + new RevokeDelegateStxCommand_Err( + r.wallet, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "RevokeDelegateStxCommand_Err_Delegation_Already_Revoked", + ); + return true; + }, + POX_4_ERRORS.ERR_DELEGATION_ALREADY_REVOKED, + ) + ), + // DelegateStxCommand_Err_Stacking_Already_Delegated + 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 DelegateStxCommand_Err( + r.wallet, + r.delegateTo, + r.untilBurnHt, + r.amount, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isDelegating(stacker) + ) return false; + + model.trackCommandRun( + "DelegateStxCommand_Err_Stacking_Already_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_DELEGATED, + ) + ), + // StackAggregationCommitSigCommand_Err_Stacking_Threshold_Not_Met + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitSigCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + !isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitSigCommand_Err_Stacking_Threshold_Not_Met", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_THRESHOLD_NOT_MET, + ), + ), + // StackAggregationCommitSigCommand_Err_Stacking_No_Such_Principal_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitSigCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitSigCommand_Err_Stacking_No_Such_Principal_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitSigCommand_Err_Stacking_No_Such_Principal_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitSigCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitSigCommand_Err_Stacking_No_Such_Principal_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitAuthCommand_Err_Stacking_Threshold_Not_Met + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitAuthCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + !isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitAuthCommand_Err_Stacking_Threshold_Not_Met", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_THRESHOLD_NOT_MET, + ), + ), + // StackAggregationCommitAuthCommand_Err_Stacking_No_Such_Principal_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitAuthCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitAuthCommand_Err_Stacking_No_Such_Principal_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitAuthCommand_Err_Stacking_No_Such_Principal_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitAuthCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitAuthCommand_Err_Stacking_No_Such_Principal_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitIndexedSigCommand_Err_Stacking_Threshold_Not_Met + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitIndexedSigCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + !isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitIndexedSigCommand_Err_Stacking_Threshold_Not_Met", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_THRESHOLD_NOT_MET, + ), + ), + // StackAggregationCommitIndexedSigCommand_Err_Stacking_No_Such_Principal_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitIndexedSigCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitIndexedSigCommand_Err_Stacking_No_Such_Principal_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitIndexedSigCommand_Err_Stacking_No_Such_Principal_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitIndexedSigCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitIndexedSigCommand_Err_Stacking_No_Such_Principal_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitIndexedAuthCommand_Err_Stacking_No_Such_Principal_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitIndexedAuthCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitIndexedAuthCommand_Err_Stacking_No_Such_Principal_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitIndexedAuthCommand_Err_Stacking_No_Such_Principal_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitIndexedAuthCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitIndexedAuthCommand_Err_Stacking_No_Such_Principal_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // StackAggregationCommitIndexedAuthCommand_Err_Stacking_Threshold_Not_Met + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).map( + (r: { wallet: Wallet; authId: number }) => + new StackAggregationCommitIndexedAuthCommand_Err( + r.wallet, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + + if ( + !hasLockedStackers(operator) || + isATCAboveThreshold(operator, model) || + !isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationCommitIndexedAuthCommand_Err_Stacking_Threshold_Not_Met", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_THRESHOLD_NOT_MET, + ), + ), + // StackAggregationIncreaseCommand_Err_Stacking_No_Such_Principal + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + }).chain((r) => { + const operator = stackers.get(r.wallet.stxAddress)!; + const committedRewCycleIndexesOrFallback = + operator.committedRewCycleIndexes.length > 0 + ? operator.committedRewCycleIndexes + : [-1]; + return fc + .record({ + rewardCycleIndex: fc.constantFrom( + ...committedRewCycleIndexesOrFallback, + ), + }) + .map((cycleIndex) => ({ ...r, ...cycleIndex })); + }).map( + (r: { wallet: Wallet; rewardCycleIndex: number; authId: number }) => + new StackAggregationIncreaseCommand_Err( + r.wallet, + r.rewardCycleIndex, + r.authId, + function (this, model) { + const operator = model.stackers.get(this.operator.stxAddress)!; + if ( + !hasLockedStackers(operator) || + !isPositive(this.rewardCycleIndex) || + isATCPositive(operator) + ) return false; + + model.trackCommandRun( + "StackAggregationIncreaseCommand_Err_Stacking_No_Such_Principal", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NO_SUCH_PRINCIPAL, + ), + ), + // DelegateStackStxCommand_Err_Delegation_Too_Much_Locked + fc.record({ + operator: fc.constantFrom(...wallets.values()), + startBurnHt: fc.integer({ + min: currentCycleFirstBlock(network), + max: nextCycleFirstBlock(network), + }), + period: fc.integer({ min: 1, max: 12 }), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + // Determine available stackers based on the operator + const availableStackers = operator.poolMembers.length > 0 + ? operator.poolMembers + : [r.operator.stxAddress]; + + return fc.record({ + stacker: fc.constantFrom(...availableStackers), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })).chain((resultWithStacker) => { + return fc.record({ + unlockBurnHt: fc.constant( + currentCycleFirstBlock(network) + + 1050 * (resultWithStacker.period + 1), + ), + }).map((additionalProps) => ({ + ...resultWithStacker, + ...additionalProps, + })); + }).chain((resultWithUnlockHeight) => { + return fc.record({ + amount: fc.bigInt({ + min: 0n, + max: 100_000_000_000_000n, + }), + }).map((amountProps) => ({ + ...resultWithUnlockHeight, + ...amountProps, + })); + }); + }).map((finalResult) => { + return new DelegateStackStxCommand_Err( + finalResult.operator, + finalResult.stacker, + finalResult.period, + finalResult.amount, + finalResult.unlockBurnHt, + function (this, model) { + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stackerWallet) || + !isDelegating(stackerWallet) || + isAmountWithinDelegationLimit(stackerWallet, this.amountUstx) || + !isAmountWithinBalance(stackerWallet, this.amountUstx) || + !isAmountAboveThreshold(model, this.amountUstx) || + !isStackerInOperatorPool(operatorWallet, this.stacker) || + !isUBHWithinDelegationLimit(stackerWallet, this.unlockBurnHt) + ) return false; + + model.trackCommandRun( + "DelegateStackStxCommand_Err_Delegation_Too_Much_Locked", + ); + return true; + }, + POX_4_ERRORS.ERR_DELEGATION_TOO_MUCH_LOCKED, + ); + }), + // DelegateStackStxCommand_Err_Stacking_Permission_Denied + fc.record({ + operator: fc.constantFrom(...wallets.values()), + startBurnHt: fc.integer({ + min: currentCycleFirstBlock(network), + max: nextCycleFirstBlock(network), + }), + period: fc.integer({ min: 1, max: 12 }), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + // Determine available stackers based on the operator + const availableStackers = operator.poolMembers.length > 0 + ? operator.poolMembers + : [r.operator.stxAddress]; + + return fc.record({ + stacker: fc.constantFrom(...availableStackers), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })).chain((resultWithStacker) => { + return fc.record({ + unlockBurnHt: fc.constant( + currentCycleFirstBlock(network) + + 1050 * (resultWithStacker.period + 1), + ), + }).map((additionalProps) => ({ + ...resultWithStacker, + ...additionalProps, + })); + }).chain((resultWithUnlockHeight) => { + return fc.record({ + amount: fc.bigInt({ + min: 0n, + max: BigInt( + stackers.get(resultWithUnlockHeight.stacker.stxAddress)! + .delegatedMaxAmount, + ), + }), + }).map((amountProps) => ({ + ...resultWithUnlockHeight, + ...amountProps, + })); + }); + }).map((finalResult) => { + return new DelegateStackStxCommand_Err( + finalResult.operator, + finalResult.stacker, + finalResult.period, + finalResult.amount, + finalResult.unlockBurnHt, + function (this, model) { + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stackerWallet) || + !isDelegating(stackerWallet) || + !isAmountWithinDelegationLimit(stackerWallet, this.amountUstx) || + !isAmountWithinBalance(stackerWallet, this.amountUstx) || + !isAmountAboveThreshold(model, this.amountUstx) || + isStackerInOperatorPool(operatorWallet, this.stacker) || + !isUBHWithinDelegationLimit(stackerWallet, this.unlockBurnHt) + ) return false; + + model.trackCommandRun( + "DelegateStackStxCommand_Err_Stacking_Permission_Denied_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_PERMISSION_DENIED, + ); + }), + // DelegateStackStxCommand_Err_Stacking_Permission_Denied_2 + fc.record({ + operator: fc.constantFrom(...wallets.values()), + startBurnHt: fc.integer({ + min: currentCycleFirstBlock(network), + max: nextCycleFirstBlock(network), + }), + period: fc.integer({ min: 1, max: 12 }), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + // Determine available stackers based on the operator + const availableStackers = operator.poolMembers.length > 0 + ? operator.poolMembers + : [r.operator.stxAddress]; + + return fc.record({ + stacker: fc.constantFrom(...availableStackers), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })).chain((resultWithStacker) => { + return fc.record({ + unlockBurnHt: fc.constant( + currentCycleFirstBlock(network) + + 1050 * (resultWithStacker.period + 1), + ), + }).map((additionalProps) => ({ + ...resultWithStacker, + ...additionalProps, + })); + }).chain((resultWithUnlockHeight) => { + return fc.record({ + amount: fc.bigInt({ + min: 0n, + max: 100_000_000_000_000n, + }), + }).map((amountProps) => ({ + ...resultWithUnlockHeight, + ...amountProps, + })); + }); + }).map((finalResult) => { + return new DelegateStackStxCommand_Err( + finalResult.operator, + finalResult.stacker, + finalResult.period, + finalResult.amount, + finalResult.unlockBurnHt, + function (this, model) { + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stackerWallet) || + isDelegating(stackerWallet) || + isAmountWithinDelegationLimit(stackerWallet, this.amountUstx) || + !isAmountWithinBalance(stackerWallet, this.amountUstx) || + !isAmountAboveThreshold(model, this.amountUstx) || + isStackerInOperatorPool(operatorWallet, this.stacker) || + isUBHWithinDelegationLimit(stackerWallet, this.unlockBurnHt) + ) return false; + + model.trackCommandRun( + "DelegateStackStxCommand_Err_Stacking_Permission_Denied_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_PERMISSION_DENIED, + ); + }), + // StackIncreaseSigCommand_Err_Stacking_Is_Delegated + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.nat(), + authId: fc.nat(), + }).map( + (r) => + new StackIncreaseSigCommand_Err( + r.operator, + r.increaseBy, + r.authId, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + !isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) || + !isIncreaseByGTZero(this.increaseBy) + ) return false; + + model.trackCommandRun( + "StackIncreaseSigCommand_Err_Stacking_Is_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_IS_DELEGATED, + ), + ), + // StackIncreaseSigCommand_Err_Stacking_Insufficient_Funds + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.constant(100_000_000_000_000), + authId: fc.nat(), + }).map( + (r) => + new StackIncreaseSigCommand_Err( + r.operator, + r.increaseBy, + r.authId, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) || + !isIncreaseByGTZero(this.increaseBy) + ) return false; + + model.trackCommandRun( + "StackIncreaseSigCommand_Err_Stacking_Insufficient_Funds", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INSUFFICIENT_FUNDS, + ), + ), + // StackIncreaseSigCommand_Err_Stacking_Invalid_Amount + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.constant(0), + authId: fc.nat(), + }).map( + (r) => + new StackIncreaseSigCommand_Err( + r.operator, + r.increaseBy, + r.authId, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + !isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) || + isIncreaseByGTZero(this.increaseBy) + ) return false; + + model.trackCommandRun( + "StackIncreaseSigCommand_Err_Stacking_Invalid_Amount", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INVALID_AMOUNT, + ), + ), + // StackIncreaseAuthCommand_Err_Stacking_Is_Delegated + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.nat(), + authId: fc.nat(), + }).map( + (r) => + new StackIncreaseAuthCommand_Err( + r.operator, + r.increaseBy, + r.authId, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + !isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) || + !isIncreaseByGTZero(this.increaseBy) + ) return false; + + model.trackCommandRun( + "StackIncreaseAuthCommand_Err_Stacking_Is_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_IS_DELEGATED, + ), + ), + // StackIncreaseAuthCommand_Err_Stacking_Insufficient_Funds + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.constant(100_000_000_000_000), + authId: fc.nat(), + }).map( + (r) => + new StackIncreaseAuthCommand_Err( + r.operator, + r.increaseBy, + r.authId, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) || + !isIncreaseByGTZero(this.increaseBy) + ) return false; + + model.trackCommandRun( + "StackIncreaseAuthCommand_Err_Stacking_Insufficient_Funds", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INSUFFICIENT_FUNDS, + ), + ), + // StackIncreaseAuthCommand_Err_Stacking_Invalid_Amount + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.constant(0), + authId: fc.nat(), + }).map( + (r) => + new StackIncreaseAuthCommand_Err( + r.operator, + r.increaseBy, + r.authId, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + !isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) || + isIncreaseByGTZero(this.increaseBy) + ) return false; + + model.trackCommandRun( + "StackIncreaseAuthCommand_Err_Stacking_Invalid_Amount", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INVALID_AMOUNT, + ), + ), + // StackExtendSigCommand_Err_Stacking_Is_Delegated_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendSigCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendSigCommand_Err_Stacking_Is_Delegated_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_IS_DELEGATED, + ), + ), + // StackExtendSigCommand_Err_Stacking_Is_Delegated_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendSigCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + !hasPoolMembers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendSigCommand_Err_Stacking_Is_Delegated_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_IS_DELEGATED, + ), + ), + // StackExtendSigCommand_Err_Stacking_Already_Delegated + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendSigCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + !isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendSigCommand_Err_Stacking_Already_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_DELEGATED, + ), + ), + // StackExtendSigCommand_Err_Stacking_Invalid_Lock_Period + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer(), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendSigCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendSigCommand_Err_Stacking_Invalid_Lock_Period", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INVALID_LOCK_PERIOD, + ), + ), + // StackExtendSigCommand_Err_Stack_Extend_Not_Locked + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendSigCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendSigCommand_Err_Stack_Extend_Not_Locked", + ); + return true; + }, + POX_4_ERRORS.ERR_STACK_EXTEND_NOT_LOCKED, + ), + ), + // StackExtendAuthCommand_Err_Stacking_Is_Delegated_1 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendAuthCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendAuthCommand_Err_Stacking_Is_Delegated_1", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_IS_DELEGATED, + ), + ), + // StackExtendAuthCommand_Err_Stacking_Is_Delegated_2 + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendAuthCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + !hasPoolMembers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendAuthCommand_Err_Stacking_Is_Delegated_2", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_IS_DELEGATED, + ), + ), + // StackExtendAuthCommand_Err_Stacking_Already_Delegated + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendAuthCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + !isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendAuthCommand_Err_Stacking_Already_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_ALREADY_DELEGATED, + ), + ), + // StackExtendAuthCommand_Err_Stacking_Invalid_Lock_Period + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer(), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendAuthCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isDelegating(stacker) || + !isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendAuthCommand_Err_Stacking_Invalid_Lock_Period", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INVALID_LOCK_PERIOD, + ), + ), + // StackExtendAuthCommand_Err_Stack_Extend_Not_Locked + fc.record({ + wallet: fc.constantFrom(...wallets.values()), + authId: fc.nat(), + extendCount: fc.integer({ min: 1, max: 12 }), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (r: { + wallet: Wallet; + extendCount: number; + authId: number; + currentCycle: number; + }) => + new StackExtendAuthCommand_Err( + r.wallet, + r.extendCount, + r.authId, + r.currentCycle, + function (this, model) { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + if ( + !isStackingMinimumCalculated(model) || + isStacking(stacker) || + isStackingSolo(stacker) || + isDelegating(stacker) || + isAmountLockedPositive(stacker) || + hasLockedStackers(stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "StackExtendAuthCommand_Err_Stack_Extend_Not_Locked", + ); + return true; + }, + POX_4_ERRORS.ERR_STACK_EXTEND_NOT_LOCKED, + ), + ), + // DelegateStackExtendCommand_Err_Stacking_Invalid_Lock_Period + fc.record({ + operator: fc.constantFrom(...wallets.values()), + extendCount: fc.constant(100000000000000), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.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_Err( + final.operator, + final.stacker, + final.extendCount, + final.currentCycle, + function (this, model) { + const operator = model.stackers.get( + this.operator.stxAddress, + )!; + const stacker = model.stackers.get( + this.stacker.stxAddress, + )!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + const newUnlockHeight = + REWARD_CYCLE_LENGTH * (firstRewardCycle + totalPeriod - 1) + + FIRST_BURNCHAIN_BLOCK_HEIGHT; + const stackedAmount = stacker.amountLocked; + + if ( + !isAmountLockedPositive(stacker) || + !isDelegating(stacker) || + !isStacking(stacker) || + !isStackerDelegatingToOperator(stacker, this.operator) || + isUBHWithinDelegationLimit(stacker, newUnlockHeight) || + !isAmountWithinDelegationLimit(stacker, stackedAmount) || + !isStackerInOperatorPool(operator, this.stacker) || + !isStackerLockedByOperator(operator, this.stacker) || + isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "DelegateStackExtendCommand_Err_Stacking_Invalid_Lock_Period", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INVALID_LOCK_PERIOD, + ), + ), + // DelegateStackExtendCommand_Err_Stacking_Not_Delegated + fc.record({ + operator: fc.constantFrom(...wallets.values()), + extendCount: fc.integer({ min: 1, max: 11 }), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.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_Err( + final.operator, + final.stacker, + final.extendCount, + final.currentCycle, + function (this, model) { + const operator = model.stackers.get( + this.operator.stxAddress, + )!; + const stacker = model.stackers.get( + this.stacker.stxAddress, + )!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + const newUnlockHeight = + REWARD_CYCLE_LENGTH * (firstRewardCycle + totalPeriod - 1) + + FIRST_BURNCHAIN_BLOCK_HEIGHT; + const stackedAmount = stacker.amountLocked; + + if ( + !isAmountLockedPositive(stacker) || + isDelegating(stacker) || + !isStacking(stacker) || + !isStackingSolo(stacker) || + isStackerDelegatingToOperator(stacker, this.operator) || + isUBHWithinDelegationLimit(stacker, newUnlockHeight) || + isAmountWithinDelegationLimit(stacker, stackedAmount) || + isStackerInOperatorPool(operator, this.stacker) || + isStackerLockedByOperator(operator, this.stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "DelegateStackExtendCommand_Err_Stacking_Not_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NOT_DELEGATED, + ), + ), + // DelegateStackExtendCommand_Err_Stack_Extend_Not_Locked + fc.record({ + operator: fc.constantFrom(...wallets.values()), + extendCount: fc.integer({ min: 1, max: 11 }), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.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_Err( + final.operator, + final.stacker, + final.extendCount, + final.currentCycle, + function (this, model) { + const operator = model.stackers.get( + this.operator.stxAddress, + )!; + const stacker = model.stackers.get( + this.stacker.stxAddress, + )!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + const newUnlockHeight = + REWARD_CYCLE_LENGTH * (firstRewardCycle + totalPeriod - 1) + + FIRST_BURNCHAIN_BLOCK_HEIGHT; + const stackedAmount = stacker.amountLocked; + if ( + isAmountLockedPositive(stacker) || + !isDelegating(stacker) || + isStacking(stacker) || + isStackerDelegatingToOperator(stacker, this.operator) || + !isUBHWithinDelegationLimit(stacker, newUnlockHeight) || + isAmountWithinDelegationLimit(stacker, stackedAmount) || + isStackerInOperatorPool(operator, this.stacker) || + isStackerLockedByOperator(operator, this.stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "DelegateStackExtendCommand_Err_Stack_Extend_Not_Locked", + ); + return true; + }, + POX_4_ERRORS.ERR_STACK_EXTEND_NOT_LOCKED, + ), + ), + // DelegateStackExtendCommand_Err_Stacking_Permission_Denied + fc.record({ + operator: fc.constantFrom(...wallets.values()), + extendCount: fc.integer({ min: 1, max: 11 }), + stacker: fc.constantFrom(...wallets.values()), + currentCycle: fc.constant(currentCycle(network)), + }).map( + (final) => + new DelegateStackExtendCommand_Err( + final.operator, + final.stacker, + final.extendCount, + final.currentCycle, + function (this, model) { + const operator = model.stackers.get( + this.operator.stxAddress, + )!; + const stacker = model.stackers.get( + this.stacker.stxAddress, + )!; + + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); + const firstExtendCycle = Math.floor( + (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / + REWARD_CYCLE_LENGTH, + ); + const lastExtendCycle = firstExtendCycle + this.extendCount - 1; + const totalPeriod = lastExtendCycle - firstRewardCycle + 1; + const newUnlockHeight = + REWARD_CYCLE_LENGTH * (firstRewardCycle + totalPeriod - 1) + + FIRST_BURNCHAIN_BLOCK_HEIGHT; + const stackedAmount = stacker.amountLocked; + + if ( + !isAmountLockedPositive(stacker) || + isDelegating(stacker) || + !isStacking(stacker) || + isStackerDelegatingToOperator(stacker, this.operator) || + isUBHWithinDelegationLimit(stacker, newUnlockHeight) || + isAmountWithinDelegationLimit(stacker, stackedAmount) || + isStackerInOperatorPool(operator, this.stacker) || + !isStackerLockedByOperator(operator, this.stacker) || + !isPeriodWithinMax(totalPeriod) + ) return false; + + model.trackCommandRun( + "DelegateStackExtendCommand_Err_Stacking_Permission_Denied", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_PERMISSION_DENIED, + ), + ), + // DelegateStackIncreaseCommand_Err_Stacking_Insufficient_Funds + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.constant(Number.MAX_SAFE_INTEGER), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.get(delegator)!; + return delegatorWallet.unlockHeight > nextCycleFirstBlock(network); + }); + + const availableStackersOrFallback = availableStackers.length === 0 + ? [r.operator.stxAddress] + : availableStackers; + + return fc.record({ + stacker: fc.constantFrom(...availableStackersOrFallback), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })); + }).map( + (final) => + new DelegateStackIncreaseCommand_Err( + final.operator, + final.stacker, + final.increaseBy, + function (this, model) { + const operatorWallet = model.stackers.get( + this.operator.stxAddress, + )!; + const stackerWallet = model.stackers.get( + this.stacker.stxAddress, + )!; + + if ( + !isAmountLockedPositive(stackerWallet) || + !isDelegating(stackerWallet) || + !isStacking(stackerWallet) || + !isIncreaseByGTZero(this.increaseBy) || + !isStackerInOperatorPool(operatorWallet, this.stacker) || + isIncreaseByWithinUnlockedBalance( + stackerWallet, + this.increaseBy, + ) || + isAmountWithinDelegationLimit( + stackerWallet, + this.increaseBy + stackerWallet.amountLocked, + ) || + !isStackerLockedByOperator(operatorWallet, this.stacker) || + isUnlockedWithinCurrentRC(stackerWallet, model) + ) return false; + + model.trackCommandRun( + "DelegateStackIncreaseCommand_Err_Stacking_Insufficient_Funds", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INSUFFICIENT_FUNDS, + ), + ), + // DelegateStackIncreaseCommand_Err_Stacking_Invalid_Amount + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.constant(0), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.get(delegator)!; + return delegatorWallet.unlockHeight > nextCycleFirstBlock(network); + }); + + const availableStackersOrFallback = availableStackers.length === 0 + ? [r.operator.stxAddress] + : availableStackers; + + return fc.record({ + stacker: fc.constantFrom(...availableStackersOrFallback), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })); + }).map( + (final) => + new DelegateStackIncreaseCommand_Err( + final.operator, + final.stacker, + final.increaseBy, + function (this, model) { + const operatorWallet = model.stackers.get( + this.operator.stxAddress, + )!; + const stackerWallet = model.stackers.get( + this.stacker.stxAddress, + )!; + + if ( + !isAmountLockedPositive(stackerWallet) || + !isDelegating(stackerWallet) || + !isStacking(stackerWallet) || + isIncreaseByGTZero(this.increaseBy) || + !isStackerInOperatorPool(operatorWallet, this.stacker) || + !isIncreaseByWithinUnlockedBalance( + stackerWallet, + this.increaseBy, + ) || + !isAmountWithinDelegationLimit( + stackerWallet, + this.increaseBy + stackerWallet.amountLocked, + ) || + !isStackerLockedByOperator(operatorWallet, this.stacker) || + isUnlockedWithinCurrentRC(stackerWallet, model) + ) return false; + + model.trackCommandRun( + "DelegateStackIncreaseCommand_Err_Stacking_Invalid_Amount", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_INVALID_AMOUNT, + ), + ), + // DelegateStackIncreaseCommand_Err_Stacking_Not_Delegated + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.nat(), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.get(delegator)!; + return delegatorWallet.unlockHeight > nextCycleFirstBlock(network); + }); + + const availableStackersOrFallback = availableStackers.length === 0 + ? [r.operator.stxAddress] + : availableStackers; + + return fc.record({ + stacker: fc.constantFrom(...availableStackersOrFallback), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })); + }).map( + (final) => + new DelegateStackIncreaseCommand_Err( + final.operator, + final.stacker, + final.increaseBy, + function (this, model) { + const operatorWallet = model.stackers.get( + this.operator.stxAddress, + )!; + const stackerWallet = model.stackers.get( + this.stacker.stxAddress, + )!; + + if ( + !isAmountLockedPositive(stackerWallet) || + isDelegating(stackerWallet) || + !isStacking(stackerWallet) || + !isStackingSolo(stackerWallet) || + !isIncreaseByGTZero(this.increaseBy) || + isStackerInOperatorPool(operatorWallet, this.stacker) || + !isIncreaseByWithinUnlockedBalance( + stackerWallet, + this.increaseBy, + ) || + isAmountWithinDelegationLimit( + stackerWallet, + this.increaseBy + stackerWallet.amountLocked, + ) || + isStackerLockedByOperator(operatorWallet, this.stacker) || + isUnlockedWithinCurrentRC(stackerWallet, model) + ) return false; + + model.trackCommandRun( + "DelegateStackIncreaseCommand_Err_Stacking_Not_Delegated", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_NOT_DELEGATED, + ), + ), + // DelegateStackIncreaseCommand_Err_Stacking_Permission_Denied + fc.record({ + operator: fc.constantFrom(...wallets.values()), + increaseBy: fc.nat(), + }).chain((r) => { + const operator = stackers.get(r.operator.stxAddress)!; + const delegatorsList = operator.poolMembers; + + const availableStackers = delegatorsList.filter((delegator) => { + const delegatorWallet = stackers.get(delegator)!; + return delegatorWallet.unlockHeight > nextCycleFirstBlock(network); + }); + + const availableStackersOrFallback = availableStackers.length === 0 + ? [r.operator.stxAddress] + : availableStackers; + + return fc.record({ + stacker: fc.constantFrom(...availableStackersOrFallback), + }).map((stacker) => ({ + ...r, + stacker: wallets.get(stacker.stacker)!, + })); + }).map( + (final) => + new DelegateStackIncreaseCommand_Err( + final.operator, + final.stacker, + final.increaseBy, + function (this, model) { + const operatorWallet = model.stackers.get( + this.operator.stxAddress, + )!; + const stackerWallet = model.stackers.get( + this.stacker.stxAddress, + )!; + + if ( + !isAmountLockedPositive(stackerWallet) || + isDelegating(stackerWallet) || + !isStacking(stackerWallet) || + !isIncreaseByGTZero(this.increaseBy) || + isStackerInOperatorPool(operatorWallet, this.stacker) || + !isIncreaseByWithinUnlockedBalance( + stackerWallet, + this.increaseBy, + ) || + isAmountWithinDelegationLimit( + stackerWallet, + this.increaseBy + stackerWallet.amountLocked, + ) || + !isStackerLockedByOperator(operatorWallet, this.stacker) || + isUnlockedWithinCurrentRC(stackerWallet, model) + ) return false; + + model.trackCommandRun( + "DelegateStackIncreaseCommand_Err_Stacking_Permission_Denied", + ); + return true; + }, + POX_4_ERRORS.ERR_STACKING_PERMISSION_DENIED, + ), + ), + // DisallowContractCallerCommand_Err + fc.record({ + stacker: fc.constantFrom(...wallets.values()), + callerToDisallow: fc.constantFrom(...wallets.values()), + }).map( + (r: { stacker: Wallet; callerToDisallow: Wallet }) => + new DisallowContractCallerCommand_Err( + r.stacker, + r.callerToDisallow, + function (this, model) { + const stacker = model.stackers.get(this.stacker.stxAddress)!; + const callerToDisallow = model.stackers.get( + this.callerToDisallow.stxAddress, + )!; + if ( + isAllowedContractCaller(stacker, this.callerToDisallow) || + isCallerAllowedByStacker(this.stacker, callerToDisallow) + ) return false; + + model.trackCommandRun( + "DisallowContractCallerCommand_Err", + ); + return true; + }, + ), + ), + ]; + + return cmds; +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts index bf8b63ffe7..31a9239a44 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox-4.stateful-prop.test.ts @@ -16,6 +16,7 @@ import { StackingClient } from "@stacks/stacking"; import fc from "fast-check"; import { PoxCommands } from "./pox_Commands.ts"; +import { ErrCommands } from "./err_Commands.ts"; import fs from "fs"; import path from "path"; @@ -108,7 +109,8 @@ it("statefully interacts with PoX-4", async () => { // commands are run at least once. const statistics = fs.readdirSync(path.join(__dirname)).filter((file) => file.startsWith("pox_") && file.endsWith(".ts") && - file !== "pox_CommandModel.ts" && file !== "pox_Commands.ts" + file !== "pox_CommandModel.ts" && file !== "pox_Commands.ts" && + !file.includes("_Err") ).map((file) => file.slice(4, -3)); // Remove "pox_" prefix and ".ts" suffix. // This is the initial state of the model. @@ -143,9 +145,14 @@ it("statefully interacts with PoX-4", async () => { simnet.setEpoch("3.0"); + const successPath = PoxCommands(model.wallets, model.stackers, sut.network); + const failurePath = ErrCommands(model.wallets, model.stackers, sut.network); + fc.assert( fc.property( - PoxCommands(model.wallets, model.stackers, sut.network), + // More on size: https://github.com/dubzzz/fast-check/discussions/2978 + // More on cmds: https://github.com/dubzzz/fast-check/discussions/3026 + fc.commands(successPath.concat(failurePath), { size: "xsmall" }), (cmds) => { const initialState = () => ({ model: model, real: sut }); fc.modelRun(initialState, cmds); @@ -153,7 +160,7 @@ it("statefully interacts with PoX-4", async () => { ), { // Defines the number of test iterations to run; default is 100. - numRuns: 1000, + numRuns: 20000, // Adjusts the level of detail in test reports. Default is 0 (minimal). // At level 2, reports include extensive details, helpful for deep // debugging. This includes not just the failing case and its seed, but diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts index 141676cdae..931326ce1f 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_AllowContractCallerCommand.ts @@ -77,7 +77,6 @@ export class AllowContractCallerCommand implements PoxCommand { const callerToAllow = model.stackers.get(this.allowanceTo.stxAddress)!; // Update model so that we know this wallet has authorized a contract-caller. - // If the caller is already allowed, there's no need to add it again. const callerToAllowIndexInAllowedList = wallet.allowedContractCallers .indexOf(this.allowanceTo.stxAddress); diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts index ce1d2a28b4..5b6cb95c27 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_CommandModel.ts @@ -46,9 +46,49 @@ export class Stub { reportCommandRuns() { console.log("Command run method execution counts:"); - this.statistics.forEach((count, commandName) => { - console.log(`${commandName}: ${count}`); + const orderedStatistics = Array.from(this.statistics.entries()).sort( + ([keyA], [keyB]) => { + return keyA.localeCompare(keyB); + }, + ); + + this.logAsTree(orderedStatistics); + } + + private logAsTree(statistics: [string, number][]) { + const tree: { [key: string]: any } = {}; + + statistics.forEach(([commandName, count]) => { + const split = commandName.split("_"); + let root: string = split[0], + rest: string = "base"; + + if (split.length > 1) { + rest = split.slice(1).join("_"); + } + if (!tree[root]) { + tree[root] = {}; + } + tree[root][rest] = count; }); + + const printTree = (node: any, indent: string = "") => { + const keys = Object.keys(node); + keys.forEach((key, index) => { + const isLast = index === keys.length - 1; + const boxChar = isLast ? "└─ " : "├─ "; + if (key !== "base") { + if (typeof node[key] === "object") { + console.log(`${indent}${boxChar}${key}: ${node[key]["base"]}`); + printTree(node[key], indent + (isLast ? " " : "│ ")); + } else { + console.log(`${indent}${boxChar}${key}: ${node[key]}`); + } + } + }); + }; + + printTree(tree); } refreshStateForNextRewardCycle(real: Real) { @@ -215,3 +255,258 @@ export const logCommand = (...items: (string | undefined)[]) => { process.stdout.write(prettyPrint.join("")); }; + +/** + * Helper function that checks if the minimum uSTX threshold was set in the model. + * @param model - the model at a given moment in time. + * @returns boolean. + */ +export const isStackingMinimumCalculated = (model: Readonly): boolean => + model.stackingMinimum > 0; + +/** + * Helper function that checks if a stacker is currently stacking. + * @param stacker - the stacker's state at a given moment in time. + * @returns boolean. + */ +export const isStacking = (stacker: Stacker): boolean => + stacker.isStacking; + +/** + * Helper function that checks if a stacker has an active delegation. + * @param stacker - the stacker's state at a given moment in time. + * @returns boolean. + */ +export const isDelegating = (stacker: Stacker): boolean => + stacker.hasDelegated; + +/** + * Helper function that checks if the stacker is stacking using solo + * stacking methods. + * @param stacker - the stacker's state at a given moment in time. + * @returns boolean. + */ +export const isStackingSolo = (stacker: Stacker): boolean => + stacker.isStackingSolo; + +/** + * Helper function that checks if the stacker has locked uSTX. + * @param stacker - the stacker's state at a given moment in time. + * @returns boolean. + */ +export const isAmountLockedPositive = (stacker: Stacker): boolean => + stacker.amountLocked > 0; + +/** + * Helper function that checks if an operator has locked uSTX on + * behalf of at least one stacker. + * @param operator - the operator's state at a given moment in time. + * @returns boolean. + */ +export const hasLockedStackers = (operator: Stacker): boolean => + operator.lockedAddresses.length > 0; + +/** + * Helper function that checks if an operator has uSTX that was not + * yet committed. + * @param operator - the operator's state at a given moment in time. + * @returns boolean. + * + * NOTE: ATC is an abbreviation for "amount to commit". + */ +export const isATCPositive = (operator: Stacker): boolean => + operator.amountToCommit > 0; + +/** + * Helper function that checks if an operator's not committed uSTX + * amount is above the minimum stacking threshold. + * @param operator - the operator's state at a given moment in time. + * @param model - the model at a given moment in time. + * @returns boolean. + * + * NOTE: ATC is an abbreviation for "amount to commit". + */ export const isATCAboveThreshold = ( + operator: Stacker, + model: Readonly, +): boolean => operator.amountToCommit >= model.stackingMinimum; + +/** + * Helper function that checks if a uSTX amount fits within a stacker's + * delegation limit. + * @param stacker - the stacker's state at a given moment in time. + * @param amountToCheck - the uSTX amount to check. + * @returns boolean. + */ +export const isAmountWithinDelegationLimit = ( + stacker: Stacker, + amountToCheck: bigint | number, +): boolean => stacker.delegatedMaxAmount >= Number(amountToCheck); + +/** + * Helper function that checks if a given unlock burn height is within + * a stacker's delegation limit. + * @param stacker - the stacker's state at a given moment in time. + * @param unlockBurnHt - the verified unlock burn height. + * @returns boolean. + * + * NOTE: UBH is an abbreviation for "unlock burn height". + */ +export const isUBHWithinDelegationLimit = ( + stacker: Stacker, + unlockBurnHt: number, +): boolean => + stacker.delegatedUntilBurnHt === undefined || + unlockBurnHt <= stacker.delegatedUntilBurnHt; + +/** + * Helper function that checks if a given amount is within a stacker's + * unlocked uSTX balance. + * @param stacker - the stacker's state at a given moment in time. + * @param amountToCheck - the amount to check. + * @returns boolean. + */ +export const isAmountWithinBalance = ( + stacker: Stacker, + amountToCheck: bigint | number, +): boolean => stacker.ustxBalance >= Number(amountToCheck); + +/** + * Helper function that checks if a given amount is above the minimum + * stacking threshold. + * @param model - the model at a given moment in time. + * @param amountToCheck - the amount to check. + * @returns boolean. + */ +export const isAmountAboveThreshold = ( + model: Readonly, + amountToCheck: bigint | number, +): boolean => Number(amountToCheck) >= model.stackingMinimum; + +/** + * Helper function that checks if an operator has at least one pool + * participant. + * @param operator - the operator's state at a given moment in time. + * @returns boolean. + */ +export const hasPoolMembers = (operator: Stacker): boolean => + operator.poolMembers.length > 0; + +/** + * Helper function that checks if a stacker is a pool member of a + * given operator. + * @param operator - the operator's state at a given moment in time. + * @param stacker - the stacker's state at a given moment in time. + * @returns boolean + */ +export const isStackerInOperatorPool = ( + operator: Stacker, + stacker: Wallet, +): boolean => operator.poolMembers.includes(stacker.stxAddress); + +/** + * Helper function that checks if a given stacker's funds are locked + * by a given operator. + * @param stacker - the stacker's state at a given moment in time. + * @param operator - the operator's state at a given moment in time. + * @returns boolean. + */ +export const isStackerLockedByOperator = ( + operator: Stacker, + stacker: Wallet, +): boolean => + operator.lockedAddresses.includes( + stacker.stxAddress, + ); + +/** + * Helper function that checks if a given stacker's unlock height is + * within the current reward cycle. + * @param stacker - the stacker's state at a given moment in time. + * @param model - the model at a given moment in time. + * @returns boolean. + * + * NOTE: RC is an abbreviation for "reward cycle". + */ +export const isUnlockedWithinCurrentRC = ( + stackerWallet: Stacker, + model: Readonly, +): boolean => (stackerWallet.unlockHeight <= + model.burnBlockHeight + REWARD_CYCLE_LENGTH); + +/** + * Helper function that checks if the increase amount is within a given + * stacker's unlocked balance. + * @param stacker - the stacker's state at a given moment in time. + * @param increaseBy - the increase amount to check. + * @returns boolean. + */ +export const isIncreaseByWithinUnlockedBalance = ( + stacker: Stacker, + increaseBy: number, +): boolean => increaseBy <= stacker.amountUnlocked; + +/** + * Helper function that checks if the increase amount is greater than zero. + * @param increaseBy - the increase amount to check. + * @returns boolean. + */ +export const isIncreaseByGTZero = (increaseBy: number): boolean => + increaseBy >= 1; + +/** + * Helper function that checks if the increase amount does not exceed the + * PoX-4 maximum lock period. + * @param period - the period to check. + * @returns boolean. + */ +export const isPeriodWithinMax = (period: number) => period <= 12; + +/** + * Helper function that checks if a given stacker is currently delegating + * to a given operator. + * @param stacker - the stacker's state at a given moment in time. + * @param operator - the operator's state at a given moment in time. + * @returns boolean. + */ +export const isStackerDelegatingToOperator = ( + stacker: Stacker, + operator: Wallet, +): boolean => stacker.delegatedTo === operator.stxAddress; + +/** + * Helper function that checks if a given increase amount is greater than + * zero. + * @param increaseAmount - the increase amount to check + * @returns boolean. + */ +export const isIncreaseAmountGTZero = (increaseAmount: number): boolean => + increaseAmount > 0; + +/** + * Helper function that checks if a given stacker's has issued an allowance + * to a potential contract caller. + * @param stacker - the stacker's state at a given moment in time. + * @param potentialAllowedStacker - the potential contract caller's state. + * @returns boolean. + */ +export const isAllowedContractCaller = ( + stacker: Stacker, + potentialAllowedStacker: Wallet, +): boolean => + stacker.allowedContractCallers.includes( + potentialAllowedStacker.stxAddress, + ); + +/** + * Helper function that checks if a given contract caller has been allowed by + * a given stacker. + * @param stacker - the stacker's state at a given moment in time. + * @param caller - the contract caller's state. + * @returns boolean. + */ +export const isCallerAllowedByStacker = ( + stacker: Wallet, + caller: Stacker, +): boolean => caller.callerAllowedBy.includes(stacker.stxAddress); + +export const isPositive = (value: number): boolean => value >= 0; \ No newline at end of file diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts index bafbe38a43..a42cb6278e 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_Commands.ts @@ -1,5 +1,5 @@ import fc from "fast-check"; -import { Real, Stacker, Stub, StxAddress, Wallet } from "./pox_CommandModel"; +import { PoxCommand, Stacker, StxAddress, Wallet } from "./pox_CommandModel"; import { GetStackingMinimumCommand } from "./pox_GetStackingMinimumCommand"; import { GetStxAccountCommand } from "./pox_GetStxAccountCommand"; import { StackStxSigCommand } from "./pox_StackStxSigCommand"; @@ -27,7 +27,7 @@ export function PoxCommands( wallets: Map, stackers: Map, network: Simnet, -): fc.Arbitrary>> { +): fc.Arbitrary[] { const cmds = [ // GetStackingMinimumCommand fc.record({ @@ -452,9 +452,7 @@ export function PoxCommands( ), ]; - // More on size: https://github.com/dubzzz/fast-check/discussions/2978 - // More on cmds: https://github.com/dubzzz/fast-check/discussions/3026 - return fc.commands(cmds, { size: "xsmall" }); + return cmds; } export const REWARD_CYCLE_LENGTH = 1050; @@ -481,7 +479,7 @@ export const currentCycleFirstBlock = (network: Simnet) => ).result, )); -const nextCycleFirstBlock = (network: Simnet) => +export const nextCycleFirstBlock = (network: Simnet) => Number(cvToValue( network.callReadOnlyFn( "ST000000000000000000002AMW42H.pox-4", diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts index 2875551342..a3fe2a5f1a 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand.ts @@ -1,4 +1,13 @@ import { + isAmountLockedPositive, + isAmountWithinDelegationLimit, + isPeriodWithinMax, + isStackerDelegatingToOperator, + isDelegating, + isStacking, + isUBHWithinDelegationLimit, + isStackerInOperatorPool, + isStackerLockedByOperator, logCommand, PoxCommand, Real, @@ -37,8 +46,9 @@ export class DelegateStackExtendCommand implements PoxCommand { * 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 stacker - Represents the Stacker's wallet. + * @param extendCount - Represents the number of cycles to extend + * the stack for. * @param currentCycle - Represents the current PoX reward cycle. */ constructor( @@ -63,10 +73,10 @@ export class DelegateStackExtendCommand implements PoxCommand { const operatorWallet = model.stackers.get(this.operator.stxAddress)!; const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; - const firstRewardCycle = - this.currentCycle > stackerWallet.firstLockedRewardCycle - ? this.currentCycle - : stackerWallet.firstLockedRewardCycle; + const firstRewardCycle = Math.max( + stackerWallet.firstLockedRewardCycle, + this.currentCycle, + ); const firstExtendCycle = Math.floor( (stackerWallet.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / REWARD_CYCLE_LENGTH, @@ -79,16 +89,15 @@ export class DelegateStackExtendCommand implements PoxCommand { const stackedAmount = stackerWallet.amountLocked; return ( - stackerWallet.amountLocked > 0 && - stackerWallet.hasDelegated === true && - stackerWallet.isStacking === true && - stackerWallet.delegatedTo === this.operator.stxAddress && - (stackerWallet.delegatedUntilBurnHt === undefined || - stackerWallet.delegatedUntilBurnHt >= newUnlockHeight) && - stackerWallet.delegatedMaxAmount >= stackedAmount && - operatorWallet.poolMembers.includes(this.stacker.stxAddress) && - operatorWallet.lockedAddresses.includes(this.stacker.stxAddress) && - totalPeriod <= 12 + isAmountLockedPositive(stackerWallet) && + isDelegating(stackerWallet) && + isStacking(stackerWallet) && + isStackerDelegatingToOperator(stackerWallet, this.operator) && + isUBHWithinDelegationLimit(stackerWallet, newUnlockHeight) && + isAmountWithinDelegationLimit(stackerWallet, stackedAmount) && + isStackerInOperatorPool(operatorWallet, this.stacker) && + isStackerLockedByOperator(operatorWallet, this.stacker) && + isPeriodWithinMax(totalPeriod) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand_Err.ts new file mode 100644 index 0000000000..680532bef6 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackExtendCommand_Err.ts @@ -0,0 +1,96 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; + +type CheckFunc = ( + this: DelegateStackExtendCommand_Err, + model: Readonly, +) => boolean; + +export class DelegateStackExtendCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly stacker: Wallet; + readonly extendCount: number; + readonly currentCycle: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `DelegateStackExtendCommand_Err` 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 number of cycles to extend the stack for. + * @param currentCycle - Represents the current PoX reward cycle. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + stacker: Wallet, + extendCount: number, + currentCycle: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.stacker = stacker; + this.extendCount = extendCount; + this.currentCycle = currentCycle; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; + + // 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.operator.btcAddress), + // (extend-count uint) + Cl.uint(this.extendCount), + ], + this.operator.stxAddress, + ); + + expect(delegateStackExtend.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label} Ӿ ${this.stacker.label}`, + "delegate-stack-extend", + "extend count", + this.extendCount.toString(), + "new unlock height", + stackerWallet.unlockHeight.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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} Ӿ ${this.stacker.label} delegate-stack-extend extend count ${this.extendCount}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand.ts index b9ec4a837c..43b6a0473a 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand.ts @@ -1,4 +1,13 @@ import { + isAmountLockedPositive, + isAmountWithinDelegationLimit, + isIncreaseAmountGTZero, + isIncreaseByWithinUnlockedBalance, + isStackerDelegatingToOperator, + isDelegating, + isStacking, + isUnlockedWithinCurrentRC, + isStackerLockedByOperator, logCommand, PoxCommand, Real, @@ -10,7 +19,7 @@ import { expect } from "vitest"; import { Cl } from "@stacks/transactions"; /** - * The DelegateStackIncreaseCommand allows a pool operator to + * The `DelegateStackIncreaseCommand` allows a pool operator to * increase an active stacking lock, issuing a "partial commitment" * for the increased cycles. * @@ -33,7 +42,7 @@ export class DelegateStackIncreaseCommand implements PoxCommand { readonly increaseBy: number; /** - * Constructs a DelegateStackIncreaseCommand to increase the uSTX amount + * Constructs a `DelegateStackIncreaseCommand` to increase the uSTX amount * previously locked on behalf of a Stacker. * * @param operator - Represents the Pool Operator's wallet. @@ -61,15 +70,18 @@ export class DelegateStackIncreaseCommand implements PoxCommand { const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; return ( - stackerWallet.amountLocked > 0 && - stackerWallet.hasDelegated === true && - stackerWallet.isStacking === true && - this.increaseBy > 0 && - operatorWallet.poolMembers.includes(this.stacker.stxAddress) && - stackerWallet.amountUnlocked >= this.increaseBy && - stackerWallet.delegatedMaxAmount >= - this.increaseBy + stackerWallet.amountLocked && - operatorWallet.lockedAddresses.indexOf(this.stacker.stxAddress) > -1 + isAmountLockedPositive(stackerWallet) && + isDelegating(stackerWallet) && + isStacking(stackerWallet) && + isIncreaseAmountGTZero(this.increaseBy) && + isStackerDelegatingToOperator(stackerWallet, this.operator) && + isIncreaseByWithinUnlockedBalance(stackerWallet, this.increaseBy) && + isAmountWithinDelegationLimit( + stackerWallet, + this.increaseBy + stackerWallet.amountLocked, + ) && + isStackerLockedByOperator(operatorWallet, this.stacker) && + isUnlockedWithinCurrentRC(stackerWallet, model) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand_Err.ts new file mode 100644 index 0000000000..fe33805264 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackIncreaseCommand_Err.ts @@ -0,0 +1,95 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; + +type CheckFunc = ( + this: DelegateStackIncreaseCommand_Err, + model: Readonly, +) => boolean; + +export class DelegateStackIncreaseCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly stacker: Wallet; + readonly increaseBy: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `DelegateStackIncreaseCommand_Err` to increase the uSTX amount + * previously locked on behalf of a Stacker. + * + * @param operator - Represents the Pool Operator's wallet. + * @param stacker - Represents the Stacker's wallet. + * @param increaseBy - Represents the locked amount to be increased by. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + stacker: Wallet, + increaseBy: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.stacker = stacker; + this.increaseBy = increaseBy; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const stackerWallet = model.stackers.get(this.stacker.stxAddress)!; + const prevLocked = stackerWallet.amountLocked; + // Act + const delegateStackIncrease = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "delegate-stack-increase", + [ + // (stacker principal) + Cl.principal(this.stacker.stxAddress), + // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + poxAddressToTuple(this.operator.btcAddress), + // (increase-by uint) + Cl.uint(this.increaseBy), + ], + this.operator.stxAddress, + ); + + // Assert + expect(delegateStackIncrease.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label} Ӿ ${this.stacker.label}`, + "delegate-stack-increase", + "increased by", + this.increaseBy.toString(), + "previously locked", + prevLocked.toString(), + "total locked", + stackerWallet.amountLocked.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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-increase by ${this.increaseBy}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts index e3d9dd25c1..70f56fc191 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand.ts @@ -1,4 +1,12 @@ import { + isAmountAboveThreshold, + isAmountWithinBalance, + isAmountWithinDelegationLimit, + isStackerDelegatingToOperator, + isDelegating, + isStacking, + isStackingMinimumCalculated, + isUBHWithinDelegationLimit, logCommand, PoxCommand, Real, @@ -26,7 +34,7 @@ import { currentCycle } from "./pox_Commands.ts"; * `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 + * - 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. @@ -47,7 +55,7 @@ export class DelegateStackStxCommand implements PoxCommand { * on behalf of a Stacker. * * @param operator - Represents the Pool Operator's wallet. - * @param stacker - Represents the STacker's wallet. + * @param stacker - Represents the Stacker's wallet. * @param period - Number of reward cycles to lock uSTX. * @param amountUstx - The uSTX amount stacked by the Operator on behalf * of the Stacker. @@ -83,19 +91,17 @@ export class DelegateStackStxCommand implements PoxCommand { // - The Operator has to currently be delegated by the Stacker. // - The Period has to fit the last delegation burn block height. - const operatorWallet = model.stackers.get(this.operator.stxAddress)!; const stackerWallet = model.stackers.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.poolMembers.includes(this.stacker.stxAddress) && - (stackerWallet.delegatedUntilBurnHt === undefined || - this.unlockBurnHt <= stackerWallet.delegatedUntilBurnHt) + isStackingMinimumCalculated(model) && + !isStacking(stackerWallet) && + isDelegating(stackerWallet) && + isAmountWithinDelegationLimit(stackerWallet, this.amountUstx) && + isAmountWithinBalance(stackerWallet, this.amountUstx) && + isAmountAboveThreshold(model, this.amountUstx) && + isStackerDelegatingToOperator(stackerWallet, this.operator) && + isUBHWithinDelegationLimit(stackerWallet, this.unlockBurnHt) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand_Err.ts new file mode 100644 index 0000000000..fdec28a355 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStackStxCommand_Err.ts @@ -0,0 +1,105 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl, ClarityValue, cvToValue } from "@stacks/transactions"; + +type CheckFunc = ( + this: DelegateStackStxCommand_Err, + model: Readonly, +) => boolean; + +export class DelegateStackStxCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly stacker: Wallet; + readonly period: number; + readonly amountUstx: bigint; + readonly unlockBurnHt: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * 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 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, + stacker: Wallet, + period: number, + amountUstx: bigint, + unlockBurnHt: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.stacker = stacker; + this.period = period; + this.amountUstx = amountUstx; + this.unlockBurnHt = unlockBurnHt; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const burnBlockHeightCV = real.network.runSnippet("burn-block-height"); + const burnBlockHeight = Number( + cvToValue(burnBlockHeightCV as ClarityValue), + ); + + // 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(burnBlockHeight), + // (lock-period uint) + Cl.uint(this.period), + ], + this.operator.stxAddress, + ); + + // Assert + expect(delegateStackStx.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label} Ӿ ${this.stacker.label}`, + "delegate-stack-stx", + "lock-amount", + this.amountUstx.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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 stacker ${this.stacker.label} period ${this.period}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts index e70d466c9d..cd14c39ce3 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand.ts @@ -1,4 +1,6 @@ import { + isDelegating, + isStackingMinimumCalculated, logCommand, PoxCommand, Real, @@ -24,8 +26,6 @@ import { * * Constraints for running this command include: * - The Stacker cannot currently be a delegator in another delegation. - * - The PoX address provided should have a valid version (between 0 and 6 - * inclusive). */ export class DelegateStxCommand implements PoxCommand { readonly wallet: Wallet; @@ -57,10 +57,11 @@ export class DelegateStxCommand implements PoxCommand { check(model: Readonly): boolean { // Constraints for running this command include: // - The Stacker cannot currently be a delegator in another delegation. + const stackerWallet = model.stackers.get(this.wallet.stxAddress)!; return ( - model.stackingMinimum > 0 && - !model.stackers.get(this.wallet.stxAddress)?.hasDelegated + isStackingMinimumCalculated(model) && + !isDelegating(stackerWallet) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand_Err.ts new file mode 100644 index 0000000000..138d99265f --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DelegateStxCommand_Err.ts @@ -0,0 +1,104 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; + +type CheckFunc = ( + this: DelegateStxCommand_Err, + model: Readonly, +) => boolean; + +export class DelegateStxCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly delegateTo: Wallet; + readonly untilBurnHt: number; + readonly amount: bigint; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `DelegateStxCommand_Err` 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 amount - The maximum amount the `Stacker` delegates the `Delegatee` + * to stack on his behalf. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + delegateTo: Wallet, + untilBurnHt: number, + amount: bigint, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.delegateTo = delegateTo; + this.untilBurnHt = untilBurnHt; + this.amount = amount; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + // The amount of uSTX delegated by the Stacker to the Delegatee. + // 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( + "ST000000000000000000002AMW42H.pox-4", + "delegate-stx", + [ + // (amount-ustx uint) + Cl.uint(amountUstx), + // (delegate-to principal) + Cl.principal(this.delegateTo.stxAddress), + // (until-burn-ht (optional uint)) + Cl.some(Cl.uint(this.untilBurnHt)), + // (pox-addr (optional { version: (buff 1), hashbytes: (buff 32) })) + Cl.some(poxAddressToTuple(this.delegateTo.btcAddress)), + ], + this.wallet.stxAddress, + ); + + // Assert + expect(delegateStx.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "delegate-stx", + "amount", + amountUstx.toString(), + "delegated to", + this.delegateTo.label, + "until", + this.untilBurnHt.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} delegate-stx to ${this.delegateTo.label} until burn ht ${this.untilBurnHt}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts index 16b830b5fb..6108a5973f 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand.ts @@ -1,4 +1,6 @@ import { + isAllowedContractCaller, + isCallerAllowedByStacker, logCommand, PoxCommand, Real, @@ -41,14 +43,10 @@ export class DisallowContractCallerCommand implements PoxCommand { const callerToDisallow = model.stackers.get( this.callerToDisallow.stxAddress, )!; + return ( - stacker.allowedContractCallers.includes( - this.callerToDisallow.stxAddress, - ) && - callerToDisallow.callerAllowedBy.includes( - this.stacker.stxAddress, - ) === - true + isAllowedContractCaller(stacker, this.callerToDisallow) && + isCallerAllowedByStacker(this.stacker, callerToDisallow) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand_Err.ts new file mode 100644 index 0000000000..028457b41e --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_DisallowContractCallerCommand_Err.ts @@ -0,0 +1,73 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; + +type CheckFunc = ( + this: DisallowContractCallerCommand_Err, + model: Readonly, +) => boolean; + +export class DisallowContractCallerCommand_Err implements PoxCommand { + readonly stacker: Wallet; + readonly callerToDisallow: Wallet; + readonly checkFunc: CheckFunc; + + /** + * Constructs a `DisallowContractCallerComand` to revoke authorization + * for calling stacking methods. + * + * @param stacker - Represents the `Stacker`'s wallet. + * @param callerToDisallow - The `contract-caller` to be revoked. + * @param checkFunc - A function to check constraints for running this command. + */ + constructor(stacker: Wallet, callerToDisallow: Wallet, checkFunc: CheckFunc) { + this.stacker = stacker; + this.callerToDisallow = callerToDisallow; + this.checkFunc = checkFunc; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); // Constraints for running this command include: + // - The Caller to be disallowed must have been previously allowed + // by the Operator. + + run(model: Stub, real: Real): void { + // Act + const disallowContractCaller = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "disallow-contract-caller", + [ + // (caller principal) + Cl.principal(this.callerToDisallow.stxAddress), + ], + this.stacker.stxAddress, + ); + + // Assert + expect(disallowContractCaller.result).toBeOk(Cl.bool(false)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.stacker.label}`, + "disallow-contract-caller", + this.callerToDisallow.label, + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.stacker.label} disallow-contract-caller ${this.callerToDisallow.label}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts index c39a1a5e42..98e2349a1f 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand.ts @@ -1,4 +1,7 @@ import { + isDelegating, + isStackingMinimumCalculated, + isUBHWithinDelegationLimit, logCommand, PoxCommand, Real, @@ -21,7 +24,7 @@ export class RevokeDelegateStxCommand implements PoxCommand { readonly wallet: Wallet; /** - * Constructs a RevokeDelegateStxCommand to revoke delegate uSTX for stacking. + * Constructs a `RevokeDelegateStxCommand` to revoke a stacking delegation. * * @param wallet - Represents the Stacker's wallet. */ @@ -34,11 +37,11 @@ export class RevokeDelegateStxCommand implements PoxCommand { // - The Stacker has to currently be delegating. // - The Stacker's delegation must not be expired. const stacker = model.stackers.get(this.wallet.stxAddress)!; + return ( - model.stackingMinimum > 0 && - stacker.hasDelegated === true && - (stacker.delegatedUntilBurnHt === undefined || - stacker.delegatedUntilBurnHt > model.burnBlockHeight) + isStackingMinimumCalculated(model) && + isDelegating(stacker) && + isUBHWithinDelegationLimit(stacker, model.burnBlockHeight) ); } @@ -80,7 +83,7 @@ export class RevokeDelegateStxCommand implements PoxCommand { // Update model so that we know this wallet is not delegating anymore. // This is important in order to prevent the test from revoking the // delegation multiple times with the same address. - // We update delegatedUntilBurnHt to 0, and not undefined. Undefined + // We update delegatedUntilBurnHt to 0, and not undefined. Undefined // stands for indefinite delegation. wallet.hasDelegated = false; wallet.delegatedTo = ""; diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand_Err.ts new file mode 100644 index 0000000000..a7a4cb0a6e --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_RevokeDelegateStxCommand_Err.ts @@ -0,0 +1,66 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; + +type CheckFunc = ( + this: RevokeDelegateStxCommand_Err, + model: Readonly, +) => boolean; + +export class RevokeDelegateStxCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `RevokeDelegateStxCommand_Err` to revoke a stacking delegation. + * + * @param wallet - Represents the Stacker's wallet. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor(wallet: Wallet, checkFunc: CheckFunc, errorCode: number) { + this.wallet = wallet; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + // Act + const revokeDelegateStx = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "revoke-delegate-stx", + [], + this.wallet.stxAddress, + ); + + // Assert + expect(revokeDelegateStx.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "revoke-delegate-stx", + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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} revoke-delegate-stx`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts index 62622f4bd3..7145c673d4 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand.ts @@ -1,4 +1,6 @@ import { + hasLockedStackers, + isATCAboveThreshold, logCommand, PoxCommand, Real, @@ -31,7 +33,8 @@ export class StackAggregationCommitAuthCommand implements PoxCommand { readonly authId: number; /** - * Constructs a `StackAggregationCommitAuthCommand` to lock uSTX for stacking. + * Constructs a `StackAggregationCommitAuthCommand` to commit partially + * locked uSTX. * * @param operator - Represents the `Operator`'s wallet. * @param authId - Unique `auth-id` for the authorization. @@ -51,8 +54,10 @@ export class StackAggregationCommitAuthCommand implements PoxCommand { // stackers has to be greater than the uSTX threshold. const operator = model.stackers.get(this.operator.stxAddress)!; - return operator.lockedAddresses.length > 0 && - operator.amountToCommit >= model.stackingMinimum; + return ( + hasLockedStackers(operator) && + isATCAboveThreshold(operator, model) + ); } run(model: Stub, real: Real): void { @@ -63,10 +68,10 @@ export class StackAggregationCommitAuthCommand implements PoxCommand { // Act - // Include the authorization and the `stack-aggregation-commit` transactions - // in a single block. This way we ensure both the authorization and the - // stack-aggregation-commit transactions are called during the same reward - // cycle, so the authorization currentRewCycle param is relevant for the + // Include the authorization and the `stack-aggregation-commit` transactions + // in a single block. This way we ensure both the authorization and the + // stack-aggregation-commit transactions are called during the same reward + // cycle, so the authorization currentRewCycle param is relevant for the // upcoming stack-aggregation-commit call. const block = real.network.mineBlock([ tx.callPublicFn( diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand_Err.ts new file mode 100644 index 0000000000..ddc986f1a4 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitAuthCommand_Err.ts @@ -0,0 +1,128 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; +import { currentCycle } from "./pox_Commands.ts"; +import { tx } from "@hirosystems/clarinet-sdk"; + +type CheckFunc = ( + this: StackAggregationCommitAuthCommand_Err, + model: Readonly, +) => boolean; + +export class StackAggregationCommitAuthCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackAggregationCommitAuthCommand_Err` to commit partially + * locked uSTX. + * + * @param operator - Represents the `Operator`'s wallet. + * @param authId - Unique `auth-id` for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const committedAmount = operatorWallet.amountToCommit; + + // Include the authorization and the `stack-aggregation-commit` transactions + // in a single block. This way we ensure both the authorization and the + // stack-aggregation-commit transactions are called during the same reward + // cycle, so the authorization currentRewCycle param is relevant for the + // upcoming stack-aggregation-commit call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (period uint) + Cl.uint(1), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (topic (string-ascii 14)) + Cl.stringAscii("agg-commit"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-commit", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + ]); + + // Assert + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label}`, + "stack-agg-commit", + "amount committed", + committedAmount.toString(), + "authorization", + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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-commit auth-id ${this.authId}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts index cfafccc674..ba9679e639 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand.ts @@ -1,4 +1,6 @@ import { + hasLockedStackers, + isATCAboveThreshold, logCommand, PoxCommand, Real, @@ -33,8 +35,8 @@ export class StackAggregationCommitIndexedAuthCommand implements PoxCommand { readonly authId: number; /** - * Constructs a `StackAggregationCommitIndexedAuthCommand` to lock uSTX - * for stacking. + * Constructs a `StackAggregationCommitIndexedAuthCommand` to commit partially + * locked uSTX. * * @param operator - Represents the `Operator`'s wallet. * @param authId - Unique `auth-id` for the authorization. @@ -55,8 +57,8 @@ export class StackAggregationCommitIndexedAuthCommand implements PoxCommand { const operator = model.stackers.get(this.operator.stxAddress)!; return ( - operator.lockedAddresses.length > 0 && - operator.amountToCommit >= model.stackingMinimum + hasLockedStackers(operator) && + isATCAboveThreshold(operator, model) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand_Err.ts new file mode 100644 index 0000000000..1c891df270 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedAuthCommand_Err.ts @@ -0,0 +1,133 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; +import { currentCycle } from "./pox_Commands.ts"; +import { tx } from "@hirosystems/clarinet-sdk"; + +type CheckFunc = ( + this: StackAggregationCommitIndexedAuthCommand_Err, + model: Readonly, +) => boolean; + +export class StackAggregationCommitIndexedAuthCommand_Err + implements PoxCommand { + readonly operator: Wallet; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackAggregationCommitIndexedAuthCommand_Err` to commit partially + * locked uSTX. + * + * @param operator - Represents the `Operator`'s wallet. + * @param authId - Unique `auth-id` for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const committedAmount = operatorWallet.amountToCommit; + + // Act + + // Include the authorization and the `stack-aggregation-commit-indexed` + // transactions in a single block. This way we ensure both the authorization + // and the stack-aggregation-commit-indexed transactions are called during + // the same reward cycle, so the authorization currentRewCycle param is + // relevant for the upcoming stack-aggregation-commit-indexed call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (period uint) + Cl.uint(1), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (topic (string-ascii 14)) + Cl.stringAscii("agg-commit"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-commit-indexed", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ), + ]); + + // Assert + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr( + Cl.int(this.errorCode), + ); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label}`, + "stack-agg-commit-indexed", + "amount committed", + committedAmount.toString(), + "authorization", + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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-commit-indexed auth-id ${this.authId}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts index 59707e21f4..beb91ea87f 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand.ts @@ -1,4 +1,6 @@ import { + hasLockedStackers, + isATCAboveThreshold, logCommand, PoxCommand, Real, @@ -33,8 +35,8 @@ export class StackAggregationCommitIndexedSigCommand implements PoxCommand { readonly authId: number; /** - * Constructs a `StackAggregationCommitIndexedSigCommand` to lock uSTX - * for stacking. + * Constructs a `StackAggregationCommitIndexedSigCommand` to commit partially + * locked uSTX. * * @param operator - Represents the `Operator`'s wallet. * @param authId - Unique `auth-id` for the authorization. @@ -55,8 +57,8 @@ export class StackAggregationCommitIndexedSigCommand implements PoxCommand { const operator = model.stackers.get(this.operator.stxAddress)!; return ( - operator.lockedAddresses.length > 0 && - operator.amountToCommit >= model.stackingMinimum + hasLockedStackers(operator) && + isATCAboveThreshold(operator, model) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand_Err.ts new file mode 100644 index 0000000000..045142f3b4 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitIndexedSigCommand_Err.ts @@ -0,0 +1,124 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; +import { bufferFromHex } from "@stacks/transactions/dist/cl"; +import { currentCycle } from "./pox_Commands.ts"; + +type CheckFunc = ( + this: StackAggregationCommitIndexedSigCommand_Err, + model: Readonly, +) => boolean; + +export class StackAggregationCommitIndexedSigCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackAggregationCommitIndexedSigCommand_Err` to commit partially + * locked uSTX. + * + * @param operator - Represents the `Operator`'s wallet. + * @param authId - Unique `auth-id` for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const committedAmount = operatorWallet.amountToCommit; + + const signerSig = this.operator.stackingClient.signPoxSignature({ + // The signer key being authorized. + signerPrivateKey: this.operator.signerPrvKey, + // The reward cycle for which the authorization is valid. + // For stack-stx and stack-extend, this refers to the reward cycle + // where the transaction is confirmed. For stack-aggregation-commit, + // this refers to the reward cycle argument in that function. + rewardCycle: currentRewCycle + 1, + // For stack-stx, this refers to lock-period. For stack-extend, + // this refers to extend-count. For stack-aggregation-commit, this is + // u1. + period: 1, + // A string representing the function where this authorization is valid. + // Either stack-stx, stack-extend, stack-increase or agg-commit. + topic: Pox4SignatureTopic.AggregateCommit, + // The PoX address that can be used with this signer key. + poxAddress: this.operator.btcAddress, + // The unique auth-id for this authorization. + authId: this.authId, + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. + maxAmount: committedAmount, + }); + + // Act + const stackAggregationCommitIndexed = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-commit-indexed", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (signer-sig (optional (buff 65))) + Cl.some(bufferFromHex(signerSig)), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ); + + // Assert + expect(stackAggregationCommitIndexed.result).toBeErr( + Cl.int(this.errorCode), + ); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label}`, + "stack-agg-commit-indexed", + "amount committed", + committedAmount.toString(), + "authorization", + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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-commit-indexed auth-id ${this.authId}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts index 32fe552477..9e6bfd1bde 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand.ts @@ -1,4 +1,6 @@ import { + hasLockedStackers, + isATCAboveThreshold, logCommand, PoxCommand, Real, @@ -31,7 +33,8 @@ export class StackAggregationCommitSigCommand implements PoxCommand { readonly authId: number; /** - * Constructs a `StackAggregationCommitSigCommand` to lock uSTX for stacking. + * Constructs a `StackAggregationCommitSigCommand` to commit partially + * locked uSTX. * * @param operator - Represents the `Operator`'s wallet. * @param authId - Unique `auth-id` for the authorization. @@ -51,8 +54,10 @@ export class StackAggregationCommitSigCommand implements PoxCommand { // stackers has to be greater than the uSTX threshold. const operator = model.stackers.get(this.operator.stxAddress)!; - return operator.lockedAddresses.length > 0 && - operator.amountToCommit >= model.stackingMinimum; + return ( + hasLockedStackers(operator) && + isATCAboveThreshold(operator, model) + ); } run(model: Stub, real: Real): void { diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand_Err.ts new file mode 100644 index 0000000000..1238a4f32b --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationCommitSigCommand_Err.ts @@ -0,0 +1,122 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl } from "@stacks/transactions"; +import { bufferFromHex } from "@stacks/transactions/dist/cl"; +import { currentCycle } from "./pox_Commands.ts"; + +type CheckFunc = ( + this: StackAggregationCommitSigCommand_Err, + model: Readonly, +) => boolean; + +export class StackAggregationCommitSigCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackAggregationCommitAuthCommand_Err` to commit partially + * locked uSTX. + * + * @param operator - Represents the `Operator`'s wallet. + * @param authId - Unique `auth-id` for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const committedAmount = operatorWallet.amountToCommit; + + const signerSig = this.operator.stackingClient.signPoxSignature({ + // The signer key being authorized. + signerPrivateKey: this.operator.signerPrvKey, + // The reward cycle for which the authorization is valid. + // For stack-stx and stack-extend, this refers to the reward cycle + // where the transaction is confirmed. For stack-aggregation-commit, + // this refers to the reward cycle argument in that function. + rewardCycle: currentRewCycle + 1, + // For stack-stx, this refers to lock-period. For stack-extend, + // this refers to extend-count. For stack-aggregation-commit, this is + // u1. + period: 1, + // A string representing the function where this authorization is valid. + // Either stack-stx, stack-extend, stack-increase or agg-commit. + topic: Pox4SignatureTopic.AggregateCommit, + // The PoX address that can be used with this signer key. + poxAddress: this.operator.btcAddress, + // The unique auth-id for this authorization. + authId: this.authId, + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. + maxAmount: committedAmount, + }); + + // Act + const stackAggregationCommit = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-aggregation-commit", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.operator.btcAddress), + // (reward-cycle uint) + Cl.uint(currentRewCycle + 1), + // (signer-sig (optional (buff 65))) + Cl.some(bufferFromHex(signerSig)), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(committedAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ); + + // Assert + expect(stackAggregationCommit.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label}`, + "stack-agg-commit", + "amount committed", + committedAmount.toString(), + "signature", + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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-commit auth-id ${this.authId}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts index 22ae0a0bea..80e1950abb 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand.ts @@ -1,4 +1,7 @@ import { + hasLockedStackers, + isATCPositive, + isPositive, logCommand, PoxCommand, Real, @@ -56,9 +59,9 @@ export class StackAggregationIncreaseCommand implements PoxCommand { // - The Reward Cycle Index must be positive. const operator = model.stackers.get(this.operator.stxAddress)!; return ( - operator.lockedAddresses.length > 0 && - this.rewardCycleIndex >= 0 && - operator.amountToCommit > 0 + hasLockedStackers(operator) && + isPositive(this.rewardCycleIndex) && + isATCPositive(operator) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand_Err.ts new file mode 100644 index 0000000000..26fc49eb60 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackAggregationIncreaseCommand_Err.ts @@ -0,0 +1,143 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl, cvToJSON } from "@stacks/transactions"; +import { bufferFromHex } from "@stacks/transactions/dist/cl"; +import { currentCycle } from "./pox_Commands.ts"; + +type CheckFunc = ( + this: StackAggregationIncreaseCommand_Err, + model: Readonly, +) => boolean; + +export class StackAggregationIncreaseCommand_Err implements PoxCommand { + readonly operator: Wallet; + readonly rewardCycleIndex: number; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackAggregationIncreaseCommand_Err` to commit partially + * stacked STX to a PoX address which has already received some STX. + * + * @param operator - Represents the `Operator`'s wallet. + * @param rewardCycleIndex - The cycle index to increase the commit for. + * @param authId - Unique `auth-id` for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + operator: Wallet, + rewardCycleIndex: number, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.operator = operator; + this.rewardCycleIndex = rewardCycleIndex; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + + const operatorWallet = model.stackers.get(this.operator.stxAddress)!; + const committedAmount = operatorWallet.amountToCommit; + + const existingEntryCV = real.network.getMapEntry( + "ST000000000000000000002AMW42H.pox-4", + "reward-cycle-pox-address-list", + Cl.tuple({ + index: Cl.uint(this.rewardCycleIndex), + "reward-cycle": Cl.uint(currentRewCycle + 1), + }), + ); + + const totalStackedBefore = + cvToJSON(existingEntryCV).value.value["total-ustx"].value; + const maxAmount = committedAmount + Number(totalStackedBefore); + + const signerSig = this.operator.stackingClient.signPoxSignature({ + // The signer key being authorized. + signerPrivateKey: this.operator.signerPrvKey, + // The reward cycle for which the authorization is valid. + // For stack-stx and stack-extend, this refers to the reward cycle + // where the transaction is confirmed. For stack-aggregation-commit, + // this refers to the reward cycle argument in that function. + rewardCycle: currentRewCycle + 1, + // For stack-stx, this refers to lock-period. For stack-extend, + // this refers to extend-count. For stack-aggregation-commit, this is + // u1. + period: 1, + // A string representing the function where this authorization is valid. + // Either stack-stx, stack-extend, stack-increase, agg-commit or agg-increase. + topic: Pox4SignatureTopic.AggregateIncrease, + // The PoX address that can be used with this signer key. + poxAddress: this.operator.btcAddress, + // The unique auth-id for this authorization. + authId: this.authId, + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. + maxAmount: maxAmount, + }); + + // 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(currentRewCycle + 1), + // (reward-cycle-index uint)) + Cl.uint(this.rewardCycleIndex), + // (signer-sig (optional (buff 65))) + Cl.some(bufferFromHex(signerSig)), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.operator.signerPubKey), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.operator.stxAddress, + ); + + // Assert + expect(stackAggregationIncrease.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.operator.label}`, + "stack-agg-increase", + "amount committed", + committedAmount.toString(), + "cycle index", + this.rewardCycleIndex.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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 index ${this.rewardCycleIndex}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts index fa796673ea..203bef86b9 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand.ts @@ -1,5 +1,18 @@ import { poxAddressToTuple } from "@stacks/stacking"; -import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { + hasPoolMembers, + isAmountLockedPositive, + isPeriodWithinMax, + isDelegating, + isStacking, + isStackingSolo, + isStackingMinimumCalculated, + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel"; import { currentCycle, FIRST_BURNCHAIN_BLOCK_HEIGHT, @@ -16,7 +29,7 @@ export class StackExtendAuthCommand implements PoxCommand { readonly currentCycle: number; /** - * Constructs a `StackExtendAuthCommand` to lock uSTX for stacking. + * Constructs a `StackExtendAuthCommand` to extend an active stacking lock. * * This command calls `stack-extend` using an `authorization`. * @@ -51,9 +64,10 @@ export class StackExtendAuthCommand implements PoxCommand { // - The new lock period must be less than or equal to 12. const stacker = model.stackers.get(this.wallet.stxAddress)!; - const firstRewardCycle = stacker.firstLockedRewardCycle < this.currentCycle - ? this.currentCycle - : stacker.firstLockedRewardCycle; + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); const firstExtendCycle = Math.floor( (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / REWARD_CYCLE_LENGTH, @@ -62,13 +76,13 @@ export class StackExtendAuthCommand implements PoxCommand { const totalPeriod = lastExtendCycle - firstRewardCycle + 1; return ( - model.stackingMinimum > 0 && - stacker.isStacking && - stacker.isStackingSolo && - !stacker.hasDelegated && - stacker.amountLocked > 0 && - stacker.poolMembers.length === 0 && - totalPeriod <= 12 + isStackingMinimumCalculated(model) && + isStacking(stacker) && + isStackingSolo(stacker) && + !isDelegating(stacker) && + isAmountLockedPositive(stacker) && + !hasPoolMembers(stacker) && + isPeriodWithinMax(totalPeriod) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand_Err.ts new file mode 100644 index 0000000000..46b8ce173e --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendAuthCommand_Err.ts @@ -0,0 +1,127 @@ +import { poxAddressToTuple } from "@stacks/stacking"; +import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { currentCycle } from "./pox_Commands"; +import { Cl } from "@stacks/transactions"; +import { expect } from "vitest"; +import { tx } from "@hirosystems/clarinet-sdk"; + +type CheckFunc = ( + this: StackExtendAuthCommand_Err, + model: Readonly, +) => boolean; + +export class StackExtendAuthCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly extendCount: number; + readonly authId: number; + readonly currentCycle: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackExtendAuthCommand_Err` to extend an active stacking lock. + * + * This command calls `stack-extend` using an `authorization`. + * + * @param wallet - Represents the Stacker's wallet. + * @param extendCount - Represents the cycles to extend the stack with. + * @param authId - Unique auth-id for the authorization. + * @param currentCycle - Represents the current PoX reward cycle. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + extendCount: number, + authId: number, + currentCycle: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.extendCount = extendCount; + this.authId = authId; + this.currentCycle = currentCycle; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + // Include the authorization and the `stack-extend` transactions in a single + // block. This way we ensure both the authorization and the stack-extend + // transactions are called during the same reward cycle, so the authorization + // currentRewCycle param is relevant for the upcoming stack-extend call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (period uint) + Cl.uint(this.extendCount), + // (reward-cycle uint) + Cl.uint(currentRewCycle), + // (topic (string-ascii 14)) + Cl.stringAscii("stack-extend"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(stacker.amountLocked), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-extend", + [ + // (extend-count uint) + Cl.uint(this.extendCount), + // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + poxAddressToTuple(this.wallet.btcAddress), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(stacker.amountLocked), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + ]); + + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "stack-extend-auth", + "extend-count", + this.extendCount.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} stack-extend auth extend-count ${this.extendCount}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand.ts index 56848d9448..b937b61207 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand.ts @@ -1,5 +1,18 @@ import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; -import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { + hasPoolMembers, + isAmountLockedPositive, + isPeriodWithinMax, + isDelegating, + isStacking, + isStackingSolo, + isStackingMinimumCalculated, + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel"; import { currentCycle, FIRST_BURNCHAIN_BLOCK_HEIGHT, @@ -15,7 +28,7 @@ export class StackExtendSigCommand implements PoxCommand { readonly currentCycle: number; /** - * Constructs a `StackExtendSigCommand` to lock uSTX for stacking. + * Constructs a `StackExtendSigCommand` to extend an active stacking lock. * * This command calls `stack-extend` using a `signature`. * @@ -50,9 +63,10 @@ export class StackExtendSigCommand implements PoxCommand { // - The new lock period must be less than or equal to 12. const stacker = model.stackers.get(this.wallet.stxAddress)!; - const firstRewardCycle = stacker.firstLockedRewardCycle < this.currentCycle - ? this.currentCycle - : stacker.firstLockedRewardCycle; + const firstRewardCycle = Math.max( + stacker.firstLockedRewardCycle, + this.currentCycle, + ); const firstExtendCycle = Math.floor( (stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) / REWARD_CYCLE_LENGTH, @@ -61,13 +75,13 @@ export class StackExtendSigCommand implements PoxCommand { const totalPeriod = lastExtendCycle - firstRewardCycle + 1; return ( - model.stackingMinimum > 0 && - stacker.isStacking && - stacker.isStackingSolo && - !stacker.hasDelegated && - stacker.amountLocked > 0 && - stacker.poolMembers.length === 0 && - totalPeriod <= 12 + isStackingMinimumCalculated(model) && + isStacking(stacker) && + isStackingSolo(stacker) && + !isDelegating(stacker) && + isAmountLockedPositive(stacker) && + !hasPoolMembers(stacker) && + isPeriodWithinMax(totalPeriod) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand_Err.ts new file mode 100644 index 0000000000..9c37b96a60 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackExtendSigCommand_Err.ts @@ -0,0 +1,121 @@ +import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; +import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { currentCycle } from "./pox_Commands"; +import { Cl } from "@stacks/transactions"; +import { expect } from "vitest"; + +type CheckFunc = ( + this: StackExtendSigCommand_Err, + model: Readonly, +) => boolean; + +export class StackExtendSigCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly extendCount: number; + readonly authId: number; + readonly currentCycle: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackExtendSigCommand_Err` to extend an active stacking lock. + * + * This command calls `stack-extend` using a `signature`. + * + * @param wallet - Represents the Stacker's wallet. + * @param extendCount - Represents the cycles to extend the stack with. + * @param authId - Unique auth-id for the authorization. + * @param currentCycle - Represents the current PoX reward cycle. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + extendCount: number, + authId: number, + currentCycle: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.extendCount = extendCount; + this.authId = authId; + this.currentCycle = currentCycle; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const signerSig = this.wallet.stackingClient.signPoxSignature({ + // The signer key being authorized. + signerPrivateKey: this.wallet.signerPrvKey, + // The reward cycle for which the authorization is valid. + // For `stack-stx` and `stack-extend`, this refers to the reward cycle + // where the transaction is confirmed. For `stack-aggregation-commit`, + // this refers to the reward cycle argument in that function. + rewardCycle: currentRewCycle, + // For `stack-stx`, this refers to `lock-period`. For `stack-extend`, + // this refers to `extend-count`. For `stack-aggregation-commit`, this is + // `u1`. + period: this.extendCount, + // A string representing the function where this authorization is valid. + // Either `stack-stx`, `stack-extend`, `stack-increase` or `agg-commit`. + topic: Pox4SignatureTopic.StackExtend, + // The PoX address that can be used with this signer key. + poxAddress: this.wallet.btcAddress, + // The unique auth-id for this authorization. + authId: this.authId, + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. + maxAmount: stacker.amountLocked, + }); + + const stackExtend = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-extend", + [ + // (extend-count uint) + Cl.uint(this.extendCount), + // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + poxAddressToTuple(this.wallet.btcAddress), + // (signer-sig (optional (buff 65))) + Cl.some(Cl.bufferFromHex(signerSig)), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(stacker.amountLocked), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ); + + expect(stackExtend.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "stack-extend-sig", + "extend-count", + this.extendCount.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} stack-extend sig extend-count ${this.extendCount}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand.ts index 127ea1d984..d819a82215 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand.ts @@ -1,5 +1,18 @@ import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; -import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { + isAmountLockedPositive, + isIncreaseAmountGTZero, + isIncreaseByWithinUnlockedBalance, + isDelegating, + isStacking, + isStackingSolo, + isStackingMinimumCalculated, + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel"; import { currentCycle } from "./pox_Commands"; import { Cl, cvToJSON } from "@stacks/transactions"; import { expect } from "vitest"; @@ -25,7 +38,7 @@ export class StackIncreaseAuthCommand implements PoxCommand { readonly authId: number; /** - * Constructs a `StackIncreaseAuthCommand` to increase lock uSTX for stacking. + * Constructs a `StackIncreaseAuthCommand` to increase the locked uSTX amount. * * @param wallet - Represents the Stacker's wallet. * @param increaseBy - Represents the locked amount to be increased by. @@ -47,13 +60,13 @@ export class StackIncreaseAuthCommand implements PoxCommand { const stacker = model.stackers.get(this.wallet.stxAddress)!; return ( - model.stackingMinimum > 0 && - stacker.isStacking && - stacker.isStackingSolo && - !stacker.hasDelegated && - stacker.amountLocked > 0 && - this.increaseBy <= stacker.amountUnlocked && - this.increaseBy >= 1 + isStackingMinimumCalculated(model) && + isStacking(stacker) && + isStackingSolo(stacker) && + !isDelegating(stacker) && + isAmountLockedPositive(stacker) && + isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) && + isIncreaseAmountGTZero(this.increaseBy) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand_Err.ts new file mode 100644 index 0000000000..5722b50236 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseAuthCommand_Err.ts @@ -0,0 +1,133 @@ +import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; +import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { currentCycle } from "./pox_Commands"; +import { Cl, cvToJSON } from "@stacks/transactions"; +import { expect } from "vitest"; +import { tx } from "@hirosystems/clarinet-sdk"; + +type CheckFunc = ( + this: StackIncreaseAuthCommand_Err, + model: Readonly, +) => boolean; + +export class StackIncreaseAuthCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly increaseBy: number; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackIncreaseAuthCommand_Err` to increase the locked uSTX amount. + * + * @param wallet - Represents the Stacker's wallet. + * @param increaseBy - Represents the locked amount to be increased by. + * @param authId - Unique auth-id for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + increaseBy: number, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.increaseBy = increaseBy; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + // Get the lock period from the stacking state. This will be used for correctly + // issuing the authorization. + const stackingStateCV = real.network.getMapEntry( + "ST000000000000000000002AMW42H.pox-4", + "stacking-state", + Cl.tuple({ stacker: Cl.principal(this.wallet.stxAddress) }), + ); + const period = cvToJSON(stackingStateCV).value.value["lock-period"].value; + + const maxAmount = stacker.amountLocked + this.increaseBy; + + // Act + + // Include the authorization and the `stack-increase` transactions in a single + // block. This way we ensure both the authorization and the stack-increase + // transactions are called during the same reward cycle and avoid the clarity + // error `ERR_INVALID_REWARD_CYCLE`. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (period uint) + Cl.uint(period), + // (reward-cycle uint) + Cl.uint(currentRewCycle), + // (topic (string-ascii 14)) + Cl.stringAscii(Pox4SignatureTopic.StackIncrease), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-increase", + [ + // (increase-by uint) + Cl.uint(this.increaseBy), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + ]); + + // Assert + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "stack-increase-auth", + "increase-by", + this.increaseBy.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} stack-increase auth increase-by ${this.increaseBy}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand.ts index ec51e3d7e4..899be8900e 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand.ts @@ -1,5 +1,18 @@ import { Pox4SignatureTopic } from "@stacks/stacking"; -import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { + isAmountLockedPositive, + isIncreaseAmountGTZero, + isIncreaseByWithinUnlockedBalance, + isDelegating, + isStacking, + isStackingSolo, + isStackingMinimumCalculated, + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel"; import { Cl, ClarityType, @@ -30,7 +43,7 @@ export class StackIncreaseSigCommand implements PoxCommand { readonly authId: number; /** - * Constructs a `StackIncreaseSigCommand` to lock uSTX for stacking. + * Constructs a `StackIncreaseSigCommand` to increase the locked uSTX amount. * * @param wallet - Represents the Stacker's wallet. * @param increaseBy - Represents the locked amount to be increased by. @@ -53,13 +66,13 @@ export class StackIncreaseSigCommand implements PoxCommand { const stacker = model.stackers.get(this.wallet.stxAddress)!; return ( - model.stackingMinimum > 0 && - stacker.isStacking && - stacker.isStackingSolo && - !stacker.hasDelegated && - stacker.amountLocked > 0 && - this.increaseBy <= stacker.amountUnlocked && - this.increaseBy >= 1 + isStackingMinimumCalculated(model) && + isStacking(stacker) && + isStackingSolo(stacker) && + !isDelegating(stacker) && + isAmountLockedPositive(stacker) && + isIncreaseByWithinUnlockedBalance(stacker, this.increaseBy) && + isIncreaseAmountGTZero(this.increaseBy) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand_Err.ts new file mode 100644 index 0000000000..4d0297b624 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackIncreaseSigCommand_Err.ts @@ -0,0 +1,143 @@ +import { Pox4SignatureTopic } from "@stacks/stacking"; +import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel"; +import { + Cl, + ClarityType, + ClarityValue, + cvToJSON, + cvToValue, + isClarityType, +} from "@stacks/transactions"; +import { assert, expect } from "vitest"; + +type CheckFunc = ( + this: StackIncreaseSigCommand_Err, + model: Readonly, +) => boolean; + +export class StackIncreaseSigCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly increaseBy: number; + readonly authId: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackIncreaseSigCommand_Err` to increase the locked uSTX amount. + * + * @param wallet - Represents the Stacker's wallet. + * @param increaseBy - Represents the locked amount to be increased by. + * @param authId - Unique auth-id for the authorization. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + increaseBy: number, + authId: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.increaseBy = increaseBy; + this.authId = authId; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const stacker = model.stackers.get(this.wallet.stxAddress)!; + + const maxAmount = stacker.amountLocked + this.increaseBy; + + const burnBlockHeightCV = real.network.runSnippet("burn-block-height"); + const burnBlockHeight = Number( + cvToValue(burnBlockHeightCV as ClarityValue), + ); + + const { result: rewardCycleNextBlockCV } = real.network.callReadOnlyFn( + "ST000000000000000000002AMW42H.pox-4", + "burn-height-to-reward-cycle", + [Cl.uint(burnBlockHeight + 1)], + this.wallet.stxAddress, + ); + assert(isClarityType(rewardCycleNextBlockCV, ClarityType.UInt)); + + const rewardCycleNextBlock = cvToValue(rewardCycleNextBlockCV); + + // Get the lock period from the stacking state. This will be used for correctly + // issuing the authorization. + const stackingStateCV = real.network.getMapEntry( + "ST000000000000000000002AMW42H.pox-4", + "stacking-state", + Cl.tuple({ stacker: Cl.principal(this.wallet.stxAddress) }), + ); + const period = cvToJSON(stackingStateCV).value.value["lock-period"].value; + + const signerSig = this.wallet.stackingClient.signPoxSignature({ + // The signer key being authorized. + signerPrivateKey: this.wallet.signerPrvKey, + // The reward cycle for which the authorization is valid. + // For `stack-stx` and `stack-extend`, this refers to the reward cycle + // where the transaction is confirmed. For `stack-aggregation-commit`, + // this refers to the reward cycle argument in that function. + rewardCycle: rewardCycleNextBlock, + // For `stack-stx`, this refers to `lock-period`. For `stack-extend`, + // this refers to `extend-count`. For `stack-aggregation-commit`, this is + // `u1`. + period: period, + // A string representing the function where this authorization is valid. + // Either `stack-stx`, `stack-extend`, `stack-increase` or `agg-commit`. + topic: Pox4SignatureTopic.StackIncrease, + // The PoX address that can be used with this signer key. + poxAddress: this.wallet.btcAddress, + // The unique auth-id for this authorization. + authId: this.authId, + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. + maxAmount: maxAmount, + }); + + const stackIncrease = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-increase", + [ + // (increase-by uint) + Cl.uint(this.increaseBy), + // (signer-sig (optional (buff 65))) + Cl.some(Cl.bufferFromHex(signerSig)), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ); + + expect(stackIncrease.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "stack-increase-sig", + "increase-by", + this.increaseBy.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} stack-increase sig increase-by ${this.increaseBy}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts index 53f34ca0bb..de3bc96964 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand.ts @@ -1,4 +1,7 @@ import { + isDelegating, + isStacking, + isStackingMinimumCalculated, logCommand, PoxCommand, Real, @@ -67,7 +70,9 @@ export class StackStxAuthCommand implements PoxCommand { const stacker = model.stackers.get(this.wallet.stxAddress)!; return ( - model.stackingMinimum > 0 && !stacker.isStacking && !stacker.hasDelegated + isStackingMinimumCalculated(model) && + !isStacking(stacker) && + !isDelegating(stacker) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand_Err.ts new file mode 100644 index 0000000000..37f32a5458 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxAuthCommand_Err.ts @@ -0,0 +1,147 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { poxAddressToTuple } from "@stacks/stacking"; +import { expect } from "vitest"; +import { Cl, ClarityValue, cvToValue } from "@stacks/transactions"; +import { currentCycle } from "./pox_Commands.ts"; +import { tx } from "@hirosystems/clarinet-sdk"; + +type CheckFunc = ( + this: StackStxAuthCommand_Err, + model: Readonly, +) => boolean; + +export class StackStxAuthCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly authId: number; + readonly period: number; + readonly margin: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackStxAuthCommand_Err` to lock uSTX for stacking. + * + * @param wallet - Represents the Stacker's wallet. + * @param authId - Unique auth-id for the authorization. + * @param period - Number of reward cycles to lock uSTX. + * @param margin - Multiplier for minimum required uSTX to stack so that each + * Stacker locks a different amount of uSTX across test runs. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + authId: number, + period: number, + margin: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.authId = authId; + this.period = period; + this.margin = margin; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const currentRewCycle = currentCycle(real.network); + + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. 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. + const maxAmount = model.stackingMinimum * this.margin; + const amountUstx = maxAmount; + + const burnBlockHeightCV = real.network.runSnippet("burn-block-height"); + const burnBlockHeight = Number( + cvToValue(burnBlockHeightCV as ClarityValue), + ); + + // Act + + // Include the authorization and the `stack-stx` transactions in a single + // block. This way we ensure both the authorization and the stack-stx + // transactions are called during the same reward cycle, so the authorization + // currentRewCycle param is relevant for the upcoming stack-stx call. + const block = real.network.mineBlock([ + tx.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "set-signer-key-authorization", + [ + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (period uint) + Cl.uint(this.period), + // (reward-cycle uint) + Cl.uint(currentRewCycle), + // (topic (string-ascii 14)) + Cl.stringAscii("stack-stx"), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (allowed bool) + Cl.bool(true), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ), + tx.callPublicFn("ST000000000000000000002AMW42H.pox-4", "stack-stx", [ + // (amount-ustx uint) + Cl.uint(amountUstx), + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (start-burn-ht uint) + Cl.uint(burnBlockHeight), + // (lock-period uint) + Cl.uint(this.period), + // (signer-sig (optional (buff 65))) + Cl.none(), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], this.wallet.stxAddress), + ]); + + // Assert + expect(block[0].result).toBeOk(Cl.bool(true)); + expect(block[1].result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "stack-stx-auth", + "lock-amount", + amountUstx.toString(), + "period", + this.period.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} stack-stx auth auth-id ${this.authId} and period ${this.period}`; + } +} diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts index 100d84a6e0..d397297037 100644 --- a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand.ts @@ -1,4 +1,7 @@ import { + isDelegating, + isStacking, + isStackingMinimumCalculated, logCommand, PoxCommand, Real, @@ -66,7 +69,9 @@ export class StackStxSigCommand implements PoxCommand { const stacker = model.stackers.get(this.wallet.stxAddress)!; return ( - model.stackingMinimum > 0 && !stacker.isStacking && !stacker.hasDelegated + isStackingMinimumCalculated(model) && + !isStacking(stacker) && + !isDelegating(stacker) ); } diff --git a/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand_Err.ts b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand_Err.ts new file mode 100644 index 0000000000..919fa56c76 --- /dev/null +++ b/contrib/boot-contracts-stateful-prop-tests/tests/pox-4/pox_StackStxSigCommand_Err.ts @@ -0,0 +1,169 @@ +import { + logCommand, + PoxCommand, + Real, + Stub, + Wallet, +} from "./pox_CommandModel.ts"; +import { Pox4SignatureTopic, poxAddressToTuple } from "@stacks/stacking"; +import { assert, expect } from "vitest"; +import { + Cl, + ClarityType, + ClarityValue, + cvToValue, + isClarityType, +} from "@stacks/transactions"; +import { currentCycle } from "./pox_Commands.ts"; + +type CheckFunc = ( + this: StackStxSigCommand_Err, + model: Readonly, +) => boolean; + +export class StackStxSigCommand_Err implements PoxCommand { + readonly wallet: Wallet; + readonly authId: number; + readonly period: number; + readonly margin: number; + readonly checkFunc: CheckFunc; + readonly errorCode: number; + + /** + * Constructs a `StackStxSigCommand_Err` to lock uSTX for stacking. + * + * @param wallet - Represents the Stacker's wallet. + * @param authId - Unique auth-id for the authorization. + * @param period - Number of reward cycles to lock uSTX. + * @param margin - Multiplier for minimum required uSTX to stack so that each + * Stacker locks a different amount of uSTX across test runs. + * @param checkFunc - A function to check constraints for running this command. + * @param errorCode - The expected error code when running this command. + */ + constructor( + wallet: Wallet, + authId: number, + period: number, + margin: number, + checkFunc: CheckFunc, + errorCode: number, + ) { + this.wallet = wallet; + this.authId = authId; + this.period = period; + this.margin = margin; + this.checkFunc = checkFunc; + this.errorCode = errorCode; + } + + check = (model: Readonly): boolean => this.checkFunc.call(this, model); + + run(model: Stub, real: Real): void { + const burnBlockHeightCV = real.network.runSnippet("burn-block-height"); + const burnBlockHeight = Number( + cvToValue(burnBlockHeightCV as ClarityValue), + ); + const currentRewCycle = currentCycle(real.network); + + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. 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. + const maxAmount = model.stackingMinimum * this.margin; + + const signerSig = this.wallet.stackingClient.signPoxSignature({ + // The signer key being authorized. + signerPrivateKey: this.wallet.signerPrvKey, + // The reward cycle for which the authorization is valid. + // For `stack-stx` and `stack-extend`, this refers to the reward cycle + // where the transaction is confirmed. For `stack-aggregation-commit`, + // this refers to the reward cycle argument in that function. + rewardCycle: currentRewCycle, + // For `stack-stx`, this refers to `lock-period`. For `stack-extend`, + // this refers to `extend-count`. For `stack-aggregation-commit`, this is + // `u1`. + period: this.period, + // A string representing the function where this authorization is valid. + // Either `stack-stx`, `stack-extend`, `stack-increase` or `agg-commit`. + topic: Pox4SignatureTopic.StackStx, + // The PoX address that can be used with this signer key. + poxAddress: this.wallet.btcAddress, + // The unique auth-id for this authorization. + authId: this.authId, + // The maximum amount of uSTX that can be used (per tx) with this signer + // key. + maxAmount: maxAmount, + }); + + // The amount of uSTX to be locked in the reward cycle. For this test, we + // will use the maximum amount of uSTX that can be used (per tx) with this + // signer key. + const amountUstx = maxAmount; + + // Act + const stackStx = real.network.callPublicFn( + "ST000000000000000000002AMW42H.pox-4", + "stack-stx", + [ + // (amount-ustx uint) + Cl.uint(amountUstx), + // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + poxAddressToTuple(this.wallet.btcAddress), + // (start-burn-ht uint) + Cl.uint(burnBlockHeight + 1), + // (lock-period uint) + Cl.uint(this.period), + // (signer-sig (optional (buff 65))) + Cl.some(Cl.bufferFromHex(signerSig)), + // (signer-key (buff 33)) + Cl.bufferFromHex(this.wallet.signerPubKey), + // (max-amount uint) + Cl.uint(maxAmount), + // (auth-id uint) + Cl.uint(this.authId), + ], + this.wallet.stxAddress, + ); + + const { result: rewardCycle } = real.network.callReadOnlyFn( + "ST000000000000000000002AMW42H.pox-4", + "burn-height-to-reward-cycle", + [Cl.uint(burnBlockHeight)], + this.wallet.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.wallet.stxAddress, + ); + assert(isClarityType(unlockBurnHeight, ClarityType.UInt)); + + // Assert + expect(stackStx.result).toBeErr(Cl.int(this.errorCode)); + + // 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( + `₿ ${model.burnBlockHeight}`, + `✗ ${this.wallet.label}`, + "stack-stx-sig", + "lock-amount", + amountUstx.toString(), + "period", + this.period.toString(), + ); + + // Refresh the model's state if the network gets to the next reward cycle. + model.refreshStateForNextRewardCycle(real); + } + + 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.label} stack-stx sig auth-id ${this.authId} and period ${this.period}`; + } +}