Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add allow-contract-caller command #4589

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -71,6 +71,8 @@ describe("PoX-4 invariant tests", () => {
amountLocked: 0,
amountUnlocked: initialUstxBalance,
unlockHeight: 0,
allowedContractCaller: '',
callerAllowedBy: []
};
});

Expand Down
@@ -0,0 +1,99 @@
import { PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel.ts";
import { expect } from "vitest";
import { boolCV, Cl, ClarityType, OptionalCV, UIntCV } from "@stacks/transactions";

/**
* The `AllowContractCallerComand` gives a `contract-caller` authorization to call stacking methods.
* Normally, stacking methods may only be invoked by direct transactions (i.e., the tx-sender
* issues a direct contract-call to the stacking methods).
* By issuing an allowance, the tx-sender may call stacking methods through the allowed contract.
*
* There are no constraints for running this command.
*/
export class AllowContractCallerCommand implements PoxCommand {
readonly wallet: Wallet;
readonly allowanceTo: Wallet;
readonly allowUntilBurnHt: OptionalCV<UIntCV>;

/**
* Constructs an `AllowContractCallerComand` that authorizes a `contract-caller` to call
* stacking methods.
*
* @param wallet - Represents the Stacker's wallet.
* @param allowanceTo - Represents the authorized `contract-caller` (i.e. a stacking pool)
* @param alllowUntilBurnHt - The burn block height until the authorization is valid.
*/

constructor(
wallet: Wallet,
allowanceTo: Wallet,
allowUntilBurnHt: OptionalCV<UIntCV>,
) {
this.wallet = wallet;
this.allowanceTo = allowanceTo;
this.allowUntilBurnHt = allowUntilBurnHt;
}

check(): boolean {
// There are no constraints for running this command.
return true;
}

run(model: Stub, real: Real): void {
// Act
const allowContractCaller = real.network.callPublicFn(
"ST000000000000000000002AMW42H.pox-4",
"allow-contract-caller",
[
// (caller principal)
Cl.principal(this.allowanceTo.stxAddress),
// (until-burn-ht (optional uint))
this.allowUntilBurnHt,
],
this.wallet.stxAddress,
);

// Assert
expect(allowContractCaller.result).toBeOk(boolCV(true));

// Get the wallets involved from the model and update it with the new state.
const wallet = model.wallets.get(this.wallet.stxAddress)!;
const callerToAllow = model.wallets.get(this.allowanceTo.stxAddress)!;
// Update model so that we know this wallet has authorized a contract-caller.

wallet.allowedContractCaller = this.allowanceTo.stxAddress;
callerToAllow.callerAllowedBy.push(wallet.stxAddress);

// Log to console for debugging purposes. This is not necessary for the
// test to pass but it is useful for debugging and eyeballing the test.
console.info(
`✓ ${
this.wallet.label.padStart(
8,
" ",
)
} ${
"allow-contract-caller".padStart(
34,
" ",
)
} ${this.allowanceTo.label.padStart(12, " ")} ${"until".padStart(53)} ${
optionalCVToString(this.allowUntilBurnHt).padStart(17)
}`,
);
}

toString() {
// fast-check will call toString() in case of errors, e.g. property failed.
// It will then make a minimal counterexample, a process called 'shrinking'
// https://github.com/dubzzz/fast-check/issues/2864#issuecomment-1098002642
return `${this.wallet.stxAddress} allow-contract-caller ${this.allowanceTo.stxAddress} until burn ht ${
optionalCVToString(this.allowUntilBurnHt)
}`;
}
}

const optionalCVToString = (optional: OptionalCV): string =>
optional.type === ClarityType.OptionalSome
? (optional.value as UIntCV).value.toString()
: "none";
2 changes: 2 additions & 0 deletions contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts
Expand Up @@ -32,6 +32,8 @@ export type Wallet = {
amountLocked: number;
amountUnlocked: number;
unlockHeight: number;
allowedContractCaller: StxAddress;
callerAllowedBy: StxAddress[];
};

export type PoxCommand = fc.Command<Stub, Real>;
54 changes: 36 additions & 18 deletions contrib/core-contract-tests/tests/pox-4/pox_Commands.ts
Expand Up @@ -6,8 +6,9 @@ import { StackStxCommand } from "./pox_StackStxCommand";
import { DelegateStxCommand } from "./pox_DelegateStxCommand";
import { DelegateStackStxCommand } from "./pox_DelegateStackStxCommand";
import { Simnet } from "@hirosystems/clarinet-sdk";
import { Cl, cvToValue } from "@stacks/transactions";
import { Cl, cvToValue, OptionalCV, UIntCV } from "@stacks/transactions";
import { RevokeDelegateStxCommand } from "./pox_RevokeDelegateStxCommand";
import { AllowContractCallerCommand } from "./pox_AllowContractCallerCommand";

export function PoxCommands(
wallets: Map<StxAddress, Wallet>, network: Simnet,
Expand Down Expand Up @@ -70,15 +71,9 @@ export function PoxCommands(
// RevokeDelegateStxCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
delegateTo: fc.constantFrom(...wallets.values()),
untilBurnHt: fc.integer({ min: 1 }),
amount: fc.bigInt({ min:0n, max: 100_000_000_000_000n }),
}).map((
r: {
wallet: Wallet;
delegateTo: Wallet;
untilBurnHt: number;
amount: bigint;
},
) =>
new RevokeDelegateStxCommand(
Expand All @@ -95,23 +90,46 @@ export function PoxCommands(
}),
period: fc.integer({ min: 1, max: 12 }),
amount: fc.bigInt({ min:0n, max: 100_000_000_000_000n }),
}).map((
r: {
operator: Wallet;
stacker: Wallet;
startBurnHt: number;
period: number;
amount: bigint;
},
) =>
}).chain((r) =>
fc.record({
unlockBurnHt: fc.constant(
currentCycleFirstBlock(network) + 1050 * (r.period + 1),
),
}).map((unlockBurnHtRecord) => ({
...r,
...unlockBurnHtRecord,
}))
).map((r) =>
new DelegateStackStxCommand(
r.operator,
r.stacker,
r.startBurnHt,
r.period,
r.amount
r.amount,
r.unlockBurnHt,
)
),
// AllowContractCallerCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
allowanceTo: fc.constantFrom(...wallets.values()),
alllowUntilBurnHt: fc.oneof(
fc.constant(Cl.none()),
fc.integer({ min: 1 }).map((value) => Cl.some(Cl.uint(value))),
),
})
.map(
(r: {
wallet: Wallet;
allowanceTo: Wallet;
alllowUntilBurnHt: OptionalCV<UIntCV>;
}) =>
new AllowContractCallerCommand(
r.wallet,
r.allowanceTo,
r.alllowUntilBurnHt,
),
),
// GetStxAccountCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
Expand Down Expand Up @@ -141,7 +159,7 @@ const currentCycle = (network: Simnet) =>
).result,
));

const currentCycleFirstBlock = (network: Simnet) =>
export const currentCycleFirstBlock = (network: Simnet) =>
Number(cvToValue(
network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
Expand Down
Expand Up @@ -16,17 +16,19 @@ import { Cl, ClarityType, isClarityType } from "@stacks/transactions";
* - The stacked STX amount should be less than or equal to the
* delegated amount.
* - The stacked uSTX amount should be less than or equal to the
* Stacker's balance
* Stacker's balance.
* - The stacked uSTX amount should be greater than or equal to the
* minimum threshold of uSTX
* minimum threshold of uSTX.
* - The Operator has to currently be delegated by the Stacker.
* - The Period has to fit the last delegation burn block height.
*/
export class DelegateStackStxCommand implements PoxCommand {
readonly operator: Wallet;
readonly stacker: Wallet;
readonly startBurnHt: number;
readonly period: number;
readonly amountUstx: bigint;
readonly unlockBurnHt: number;

/**
* Constructs a `DelegateStackStxCommand` to lock uSTX as a Pool Operator
Expand All @@ -38,46 +40,52 @@ export class DelegateStackStxCommand implements PoxCommand {
* @param period - Number of reward cycles to lock uSTX.
* @param amountUstx - The uSTX amount stacked by the Operator on behalf
* of the Stacker
* @param unlockBurnHt - The burn height at which the uSTX is unlocked.
*/
constructor(
operator: Wallet,
stacker: Wallet,
startBurnHt: number,
period: number,
amountUstx: bigint,
unlockBurnHt: number,
) {
this.operator = operator;
this.stacker = stacker;
this.startBurnHt = startBurnHt;
this.period = period;
this.amountUstx = amountUstx;
this.unlockBurnHt = unlockBurnHt;
}

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

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

return (
model.stackingMinimum > 0 &&
!stackerWallet.isStacking &&
stackerWallet.hasDelegated &&
stackerWallet.delegatedMaxAmount >= Number(this.amountUstx) &&
Number(this.amountUstx) <= stackerWallet.ustxBalance &&
Number(this.amountUstx) >= model.stackingMinimum &&
operatorWallet.hasPoolMembers.includes(stackerWallet.stxAddress)
operatorWallet.hasPoolMembers.includes(stackerWallet.stxAddress) &&
this.unlockBurnHt <= stackerWallet.delegatedUntilBurnHt
);
}

Expand Down