Skip to content

Commit 0c342bf

Browse files
committed
fix(app): restrict guided policies to mainnet-whitelisted selectors
1 parent 3084edf commit 0c342bf

4 files changed

Lines changed: 47 additions & 46 deletions

File tree

app/src/components/SessionWizard/steps/SelectPolicy.tsx

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,16 @@ const INTENT_ORDER: BuiltInSessionPolicyPresetId[] = [
3030
'payments',
3131
'trading',
3232
'contract_write',
33-
'deploy',
3433
'signing',
35-
'full_app_control',
3634
];
3735

3836
const INTENT_DESCRIPTIONS: Record<BuiltInSessionPolicyPresetId, string> = {
3937
payments: 'Send funds for payouts and transfers.',
4038
trading: 'Buy, sell, and claim in selected apps.',
4139
gaming: 'Repeat gameplay actions in supported apps.',
4240
contract_write: 'Run advanced app actions in selected apps.',
43-
deploy: 'Create and launch contracts.',
4441
signing: 'Sign messages and transactions when needed.',
42+
deploy: 'Create and launch contracts.',
4543
full_app_control: 'Give broad control in selected apps.',
4644
};
4745

@@ -106,6 +104,9 @@ function buildCombinedPolicyPreview(
106104
}
107105

108106
const selectedIntents = INTENT_ORDER.filter(intent => intentIds.includes(intent));
107+
if (selectedIntents.length === 0) {
108+
throw new Error('Select at least one supported intent.');
109+
}
109110
const defaultLimits = selectedIntents.map(intent => BUILT_IN_POLICY_PRESETS[intent].defaultLimits);
110111

111112
const expiresInSeconds = Math.max(...defaultLimits.map(limits => limits.expiresInSeconds));
@@ -176,16 +177,18 @@ export default function SelectPolicy() {
176177
} = useSessionWizardState();
177178

178179
const [stage, setStage] = useState<PolicyStage>('intent');
180+
const isSelectableIntent = (intent: BuiltInSessionPolicyPresetId): boolean => INTENT_ORDER.includes(intent);
181+
179182
const [selectedIntentIds, setSelectedIntentIds] = useState<BuiltInSessionPolicyPresetId[]>(() => {
180-
if (selectedPreset !== 'custom' && selectedPreset !== 'gaming') {
183+
if (selectedPreset !== 'custom' && selectedPreset !== 'gaming' && isSelectableIntent(selectedPreset)) {
181184
return [selectedPreset];
182185
}
183186
return ['payments'];
184187
});
185188

186189
useEffect(() => {
187190
setPolicyMode('guided');
188-
if (selectedPreset === 'custom' || selectedPreset === 'gaming') {
191+
if (selectedPreset === 'custom' || selectedPreset === 'gaming' || !isSelectableIntent(selectedPreset)) {
189192
selectPreset('payments');
190193
}
191194
}, [selectedPreset, selectPreset, setPolicyMode]);
@@ -213,7 +216,6 @@ export default function SelectPolicy() {
213216

214217
const selectedIntentPresets = selectedIntentIds.map(intentId => BUILT_IN_POLICY_PRESETS[intentId]);
215218
const policyWarnings = composite.preview?.policyPayload.policyMeta?.warnings ?? [];
216-
const riskReasons = composite.risk?.reasons ?? [];
217219
const requiresDangerAcknowledgement =
218220
selectedIntentPresets.some(preset => preset.requiresDangerAcknowledgement) ||
219221
(composite.risk?.requiresConfirmation ?? false) ||
@@ -376,38 +378,14 @@ export default function SelectPolicy() {
376378
})}
377379
</div>
378380
<p className={styles.appScopeSubhint}>More options coming soon.</p>
379-
{composite.risk ? (
380-
<div className={styles.riskSummary}>
381-
<p className={styles.riskHeading}>Risk: {composite.risk.level.toUpperCase()}</p>
382-
{riskReasons.length === 0 ? (
383-
<p className={styles.riskMuted}>No elevated-risk signals were detected for this selection.</p>
384-
) : (
385-
<ul className={styles.riskList}>
386-
{riskReasons.map(reason => (
387-
<li key={reason}>{reason}</li>
388-
))}
389-
</ul>
390-
)}
391-
{policyWarnings.length > 0 ? (
392-
<>
393-
<p className={styles.warningHeading}>Policy warnings</p>
394-
<ul className={styles.warningList}>
395-
{policyWarnings.map(warning => (
396-
<li key={warning}>{warning}</li>
397-
))}
398-
</ul>
399-
</>
400-
) : null}
401-
</div>
402-
) : null}
403381
{requiresDangerAcknowledgement ? (
404382
<label className={styles.dangerAck}>
405383
<input
406384
type="checkbox"
407385
checked={dangerAcknowledged}
408386
onChange={event => setDangerAcknowledged(event.target.checked)}
409387
/>
410-
<span>I understand these permissions are high-risk and can move real funds.</span>
388+
<span>I understand these permissions can move real funds.</span>
411389
</label>
412390
) : null}
413391
</div>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { compileGuidedPolicy } from '../policy-compiler';
3+
4+
describe('policy compiler', () => {
5+
it('builds selector-scoped call policies for curated app selectors', () => {
6+
const compiled = compileGuidedPolicy({
7+
presetId: 'trading',
8+
expiresInSeconds: 3600,
9+
feeLimit: '2000000000000000',
10+
maxValuePerUse: '5000000000000000',
11+
selectedAppIds: ['136'],
12+
transferTargets: [],
13+
});
14+
15+
expect(compiled.sessionConfig.callPolicies).toEqual([
16+
{ target: '0x3272596F776470D2D7C3f7dfF3dc50888b7D8967', selector: '0x5d7a2f89' },
17+
{ target: '0x3272596F776470D2D7C3f7dfF3dc50888b7D8967', selector: '0x379607f5' },
18+
{ target: '0x3272596F776470D2D7C3f7dfF3dc50888b7D8967', selector: '0x83a84ba9' },
19+
{ target: '0xe6765C9cb1B42D3CC36Fcd3D2B4fc938db456EaD', selector: '0x4a5eafef' },
20+
]);
21+
expect(compiled.sessionConfig.callPolicies.every(policy => Boolean(policy.selector))).toBe(true);
22+
});
23+
24+
it('does not emit unscoped call policies for legacy high-risk presets', () => {
25+
const compiled = compileGuidedPolicy({
26+
presetId: 'full_app_control',
27+
expiresInSeconds: 3600,
28+
feeLimit: '2000000000000000',
29+
maxValuePerUse: '5000000000000000',
30+
selectedAppIds: ['136'],
31+
transferTargets: [],
32+
});
33+
34+
expect(compiled.sessionConfig.callPolicies.every(policy => Boolean(policy.selector))).toBe(true);
35+
});
36+
});

app/src/lib/app-registry.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,7 @@ export const APP_REGISTRY: ReadonlyArray<AppRegistryEntry> = [
7979
address: '0xe6765C9cb1B42D3CC36Fcd3D2B4fc938db456EaD',
8080
label: 'Batch purchase',
8181
verified: true,
82-
selectors: [],
83-
},
84-
{
85-
address: '0xe90e33162d31004996F14ED6463EA1F610d4d3Ab',
86-
label: 'Ticket Drop',
87-
verified: true,
88-
selectors: [],
82+
selectors: [{ selector: '0x4a5eafef', label: 'Purchase', enabledByDefault: true }],
8983
},
9084
],
9185
},

app/src/lib/policy-compiler.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ function buildCallPolicies(
8080
}
8181

8282
const calls: SessionCallPolicy[] = [];
83-
const allowUnscopedByPreset = presetId === 'full_app_control' || presetId === 'deploy';
8483

8584
for (const app of selectedApps) {
8685
if (!app.verified) {
@@ -90,16 +89,10 @@ function buildCallPolicies(
9089
}
9190

9291
for (const contract of app.contracts) {
93-
if (allowUnscopedByPreset) {
94-
calls.push({ target: contract.address });
95-
continue;
96-
}
97-
9892
const defaultSelectors = contract.selectors.filter(selector => selector.enabledByDefault);
9993
if (defaultSelectors.length === 0) {
100-
calls.push({ target: contract.address });
10194
warnings.push(
102-
`${app.name} / ${contract.label} has no curated selectors. Added contract-level call scope.`,
95+
`${app.name} / ${contract.label} has no mainnet-approved selectors. Skipped from call policies.`,
10396
);
10497
continue;
10598
}

0 commit comments

Comments
 (0)