Skip to content

Commit 28bcf57

Browse files
committed
fix(managed-wallet): enforce warn<=block config and match bid owner
Address review: validate WARN_MULTIPLIER <= BLOCK_MULTIPLIER in the config schema so a misconfiguration can't silently disable the warn tier, and include the deployment owner in the accepted-bid match so a lease referencing a foreign owner is rejected.
1 parent b06ef17 commit 28bcf57

4 files changed

Lines changed: 83 additions & 61 deletions

File tree

apps/api/src/billing/config/env.config.ts

Lines changed: 62 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,67 @@ import { AUDITOR } from "@src/deployment/config/provider.config";
55

66
dotenv.config({ path: "env/.env.funding-wallet-index" });
77

8-
export const envSchema = z.object({
9-
OLD_MASTER_WALLET_MNEMONIC: z.string().optional(),
10-
FUNDING_WALLET_MNEMONIC: z.string().optional(),
11-
FUNDING_WALLET_MNEMONIC_V1: z.string().optional(),
12-
FUNDING_WALLET_MNEMONIC_V2: z.string().optional(),
13-
DERIVATION_WALLET_MNEMONIC: z.string().optional(),
14-
DERIVATION_WALLET_MNEMONIC_V1: z.string().optional(),
15-
DERIVATION_WALLET_MNEMONIC_V2: z.string().optional(),
16-
NETWORK: z.enum(["mainnet", "testnet", "sandbox"]),
17-
RPC_NODE_ENDPOINT: z.string(),
18-
TRIAL_ALLOWANCE_EXPIRATION_DAYS: z.number({ coerce: true }).default(30),
19-
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
20-
TRIAL_FEES_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
21-
TRIAL_DEPLOYMENT_CLEANUP_HOURS: z.number({ coerce: true }).default(24),
22-
DEPLOYMENT_GRANT_DENOM: z.enum(["uakt", "uact"]),
23-
GAS_SAFETY_MULTIPLIER: z.number({ coerce: true }).default(1.8),
24-
AVERAGE_GAS_PRICE: z.number({ coerce: true }).default(0.025),
25-
FEE_ALLOWANCE_REFILL_THRESHOLD: z.number({ coerce: true }),
26-
FEE_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }),
27-
DEPLOYMENT_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }),
28-
ALLOWANCE_REFILL_BATCH_SIZE: z.number({ coerce: true }).default(10),
29-
WALLET_BATCHING_INTERVAL_MS: z.number().optional().default(1000),
30-
STRIPE_SECRET_KEY: z.string(),
31-
STRIPE_PRODUCT_ID: z.string(),
32-
STRIPE_WEBHOOK_SECRET: z.string(),
33-
STRIPE_ENABLE_COUPONS: z.enum(["true", "false"]).default("false"),
34-
CONSOLE_WEB_PAYMENT_LINK: z.string(),
35-
MANAGED_WALLET_LEASE_ALLOWED_AUDITORS: z
36-
.string()
37-
.default(AUDITOR)
38-
.transform(val => (val ? val.split(",").map(addr => addr.trim()) : [])),
39-
MANAGED_WALLET_TRIAL_MIN_TOP_UP_AMOUNT: z.number({ coerce: true }).min(20).default(100),
40-
MANAGED_WALLET_TRIAL_BLOCKED_GPU_MODELS: z
41-
.string()
42-
.default("nvidia/b300,nvidia/b200,nvidia/h200,nvidia/h100,nvidia/pro6000se,nvidia/pro6000we,nvidia/a100,nvidia/rtx5090,nvidia/rtx4090,nvidia/rtx3090")
43-
.transform(val =>
44-
val
45-
? val
46-
.split(",")
47-
.map(entry => entry.trim().toLowerCase())
48-
.filter(Boolean)
49-
: []
50-
)
51-
.refine(entries => entries.every(entry => /^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(entry)), {
52-
message: "MANAGED_WALLET_TRIAL_BLOCKED_GPU_MODELS entries must be in 'vendor/model' format"
53-
}),
54-
MASTER_WALLET_TARGET_ACT_BALANCE: z.number({ coerce: true }).default(10_000_000_000),
55-
MANAGED_WALLET_BID_PRICE_GUARD_ENABLED: z
56-
.enum(["true", "false"])
57-
.default("true")
58-
.transform(val => val === "true"),
59-
MANAGED_WALLET_BID_PRICE_WARN_MULTIPLIER: z.number({ coerce: true }).positive().default(5),
60-
MANAGED_WALLET_BID_PRICE_BLOCK_MULTIPLIER: z.number({ coerce: true }).positive().default(10),
61-
MANAGED_WALLET_BID_PRICE_ABSOLUTE_MAX_UAKT: z.number({ coerce: true }).positive().optional(),
62-
MANAGED_WALLET_BID_PRICE_ABSOLUTE_MAX_UACT: z.number({ coerce: true }).positive().optional(),
63-
TX_SIGNER_BASE_URL: z.string()
64-
});
8+
export const envSchema = z
9+
.object({
10+
OLD_MASTER_WALLET_MNEMONIC: z.string().optional(),
11+
FUNDING_WALLET_MNEMONIC: z.string().optional(),
12+
FUNDING_WALLET_MNEMONIC_V1: z.string().optional(),
13+
FUNDING_WALLET_MNEMONIC_V2: z.string().optional(),
14+
DERIVATION_WALLET_MNEMONIC: z.string().optional(),
15+
DERIVATION_WALLET_MNEMONIC_V1: z.string().optional(),
16+
DERIVATION_WALLET_MNEMONIC_V2: z.string().optional(),
17+
NETWORK: z.enum(["mainnet", "testnet", "sandbox"]),
18+
RPC_NODE_ENDPOINT: z.string(),
19+
TRIAL_ALLOWANCE_EXPIRATION_DAYS: z.number({ coerce: true }).default(30),
20+
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
21+
TRIAL_FEES_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
22+
TRIAL_DEPLOYMENT_CLEANUP_HOURS: z.number({ coerce: true }).default(24),
23+
DEPLOYMENT_GRANT_DENOM: z.enum(["uakt", "uact"]),
24+
GAS_SAFETY_MULTIPLIER: z.number({ coerce: true }).default(1.8),
25+
AVERAGE_GAS_PRICE: z.number({ coerce: true }).default(0.025),
26+
FEE_ALLOWANCE_REFILL_THRESHOLD: z.number({ coerce: true }),
27+
FEE_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }),
28+
DEPLOYMENT_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }),
29+
ALLOWANCE_REFILL_BATCH_SIZE: z.number({ coerce: true }).default(10),
30+
WALLET_BATCHING_INTERVAL_MS: z.number().optional().default(1000),
31+
STRIPE_SECRET_KEY: z.string(),
32+
STRIPE_PRODUCT_ID: z.string(),
33+
STRIPE_WEBHOOK_SECRET: z.string(),
34+
STRIPE_ENABLE_COUPONS: z.enum(["true", "false"]).default("false"),
35+
CONSOLE_WEB_PAYMENT_LINK: z.string(),
36+
MANAGED_WALLET_LEASE_ALLOWED_AUDITORS: z
37+
.string()
38+
.default(AUDITOR)
39+
.transform(val => (val ? val.split(",").map(addr => addr.trim()) : [])),
40+
MANAGED_WALLET_TRIAL_MIN_TOP_UP_AMOUNT: z.number({ coerce: true }).min(20).default(100),
41+
MANAGED_WALLET_TRIAL_BLOCKED_GPU_MODELS: z
42+
.string()
43+
.default("nvidia/b300,nvidia/b200,nvidia/h200,nvidia/h100,nvidia/pro6000se,nvidia/pro6000we,nvidia/a100,nvidia/rtx5090,nvidia/rtx4090,nvidia/rtx3090")
44+
.transform(val =>
45+
val
46+
? val
47+
.split(",")
48+
.map(entry => entry.trim().toLowerCase())
49+
.filter(Boolean)
50+
: []
51+
)
52+
.refine(entries => entries.every(entry => /^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(entry)), {
53+
message: "MANAGED_WALLET_TRIAL_BLOCKED_GPU_MODELS entries must be in 'vendor/model' format"
54+
}),
55+
MASTER_WALLET_TARGET_ACT_BALANCE: z.number({ coerce: true }).default(10_000_000_000),
56+
MANAGED_WALLET_BID_PRICE_GUARD_ENABLED: z
57+
.enum(["true", "false"])
58+
.default("true")
59+
.transform(val => val === "true"),
60+
MANAGED_WALLET_BID_PRICE_WARN_MULTIPLIER: z.number({ coerce: true }).positive().default(5),
61+
MANAGED_WALLET_BID_PRICE_BLOCK_MULTIPLIER: z.number({ coerce: true }).positive().default(10),
62+
MANAGED_WALLET_BID_PRICE_ABSOLUTE_MAX_UAKT: z.number({ coerce: true }).positive().optional(),
63+
MANAGED_WALLET_BID_PRICE_ABSOLUTE_MAX_UACT: z.number({ coerce: true }).positive().optional(),
64+
TX_SIGNER_BASE_URL: z.string()
65+
})
66+
.refine(config => config.MANAGED_WALLET_BID_PRICE_WARN_MULTIPLIER <= config.MANAGED_WALLET_BID_PRICE_BLOCK_MULTIPLIER, {
67+
message: "MANAGED_WALLET_BID_PRICE_WARN_MULTIPLIER must be less than or equal to MANAGED_WALLET_BID_PRICE_BLOCK_MULTIPLIER",
68+
path: ["MANAGED_WALLET_BID_PRICE_WARN_MULTIPLIER"]
69+
});
6570

6671
export type BillingConfig = z.infer<typeof envSchema>;

apps/api/src/billing/lib/lease-messages/lease-messages.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ export async function resolveLeaseBids(messages: EncodeObject[], owner: string,
5050
return leaseBidIds.map(bidId => {
5151
const orderBids = bidsByDseq.get(bidId.dseq.toString()) ?? [];
5252
const accepted = orderBids.find(
53-
b => b.bid.id.gseq === bidId.gseq && b.bid.id.oseq === bidId.oseq && b.bid.id.provider === bidId.provider && b.bid.id.bseq === bidId.bseq
53+
b =>
54+
b.bid.id.owner === bidId.owner &&
55+
b.bid.id.gseq === bidId.gseq &&
56+
b.bid.id.oseq === bidId.oseq &&
57+
b.bid.id.provider === bidId.provider &&
58+
b.bid.id.bseq === bidId.bseq
5459
);
5560
assert(
5661
accepted,

apps/api/src/billing/services/lease-bid-price-guard/lease-bid-price-guard.service.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { mockConfigService } from "@test/mocks/config-service.mock";
1414
import { createBid } from "@test/seeders/bid.seeder";
1515
import { createUserWallet } from "@test/seeders/user-wallet.seeder";
1616

17+
const OWNER = "akash1owner";
1718
const ACCEPTED = { dseq: "111", gseq: 1, oseq: 1, bseq: 1, provider: "akash1prov" };
1819

1920
describe(LeaseBidPriceGuardService.name, () => {
@@ -125,6 +126,17 @@ describe(LeaseBidPriceGuardService.name, () => {
125126
});
126127
});
127128

129+
it("rejects with 403 when the bid owner does not match the lease message owner", async () => {
130+
const foreignBid = createBid({ owner: "akash1someoneelse", ...ACCEPTED });
131+
foreignBid.bid.price = { denom: "uakt", amount: "100" };
132+
const { service } = setup({ bids: [foreignBid] });
133+
134+
await expect(service.validateLeaseBidPrices([leaseMessage(ACCEPTED)], createUserWallet())).rejects.toMatchObject({
135+
status: 403,
136+
message: expect.stringContaining("Referenced lease bid not found")
137+
});
138+
});
139+
128140
it("evaluates each order independently and blocks the offending one", async () => {
129141
const cheapOrder = { dseq: "111", gseq: 1, oseq: 1, bseq: 1, provider: "akash1prov" };
130142
const absurdOrder = { dseq: "111", gseq: 2, oseq: 1, bseq: 1, provider: "akash1prov" };
@@ -144,15 +156,15 @@ describe(LeaseBidPriceGuardService.name, () => {
144156
});
145157

146158
function pricedBid(ids: { dseq: string; gseq: number; oseq: number; bseq: number; provider: string }, amount: number, denom = "uakt"): Bid {
147-
const bid = createBid(ids);
159+
const bid = createBid({ owner: OWNER, ...ids });
148160
bid.bid.price = { denom, amount: amount.toString() };
149161
return bid;
150162
}
151163

152164
function leaseMessage(bidId: { dseq: string; gseq: number; oseq: number; bseq: number; provider: string }): EncodeObject {
153165
return {
154166
typeUrl: `/${MsgCreateLease.$type}`,
155-
value: MsgCreateLease.fromPartial({ bidId: { owner: "akash1owner", ...bidId } })
167+
value: MsgCreateLease.fromPartial({ bidId: { owner: OWNER, ...bidId } })
156168
};
157169
}
158170

apps/api/src/billing/services/trial-validation/trial-validation.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe(TrialValidationService.name, () => {
175175
model: string,
176176
ids: { dseq: string; gseq: number; oseq: number; bseq: number; provider: string }
177177
): ReturnType<typeof createBid> {
178-
const bid = createBid({ dseq: ids.dseq, gseq: ids.gseq, oseq: ids.oseq, bseq: ids.bseq, provider: ids.provider });
178+
const bid = createBid({ owner: "akash1owner", dseq: ids.dseq, gseq: ids.gseq, oseq: ids.oseq, bseq: ids.bseq, provider: ids.provider });
179179
bid.bid.resources_offer[0].resources.gpu = {
180180
units: { val: "1" },
181181
attributes: [{ key: `vendor/${vendor}/model/${model}`, value: "true" }]

0 commit comments

Comments
 (0)