Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/vibenet/demos/catalogue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ describe('DEMOS', () => {
expect(new Set(hrefs).size).toBe(hrefs.length);
});

it('lists Validity Transactions as a top-level group', () => {
const validity = listedDemos().find((demo) => demo.href === '/vibenet/demos/validity');
it('keeps Validity Transactions reachable but hidden from the top-level grid', () => {
const validity = DEMOS.find((demo) => demo.href === '/vibenet/demos/validity');
expect(validity?.title).toBe('Validity Transactions');
expect(validity?.children?.map((demo) => demo.title)).toEqual(['Conditional Swaps', 'Race the Agent']);
expect(listedDemos()).not.toContain(validity);
});
});

Expand Down
1 change: 1 addition & 0 deletions app/vibenet/demos/catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const DEMOS: DemoEntry[] = [
'Build intent-like flows from ordinary account transactions',
],
available: true,
listed: false,
children: [
{
href: '/vibenet/demos/validity/conditional-swaps',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
"source": "../contracts/ConditionalWithdrawal.sol",
"abi": [
{"inputs":[{"internalType":"contract IERC20","name":"vibe","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},
{"inputs":[],"name":"ENABLED_SLOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"VIBE","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"enabled","outputs":[{"internalType":"bool","name":"value","type":"bool"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"flip","outputs":[{"internalType":"bool","name":"value","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"enabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"bool","name":"value","type":"bool"}],"name":"setEnabled","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}
],
"bytecode": "0x60a060405234801561000f575f80fd5b506040516103b03803806103b083398101604081905261002e9161003f565b6001600160a01b031660805261006c565b5f6020828403121561004f575f80fd5b81516001600160a01b0381168114610065575f80fd5b9392505050565b60805161032661008a5f395f8181608301526101d101526103265ff3fe608060405234801561000f575f80fd5b506004361061007a575f3560e01c8063328d8f7211610058578063328d8f72146101065780633ccfd60b14610127578063848606331461012f578063cde4efa91461013e575f80fd5b806304a3b7cd1461007e57806304c879d6146100c2578063238dafe0146100e4575b5f80fd5b6100a57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b6100d65f805160206102d183398151915281565b6040519081526020016100b9565b5f805160206102d18339815191525415155b60405190151581526020016100b9565b610125610114366004610293565b5f805160206102d183398151915255565b005b610125610158565b6100d6670de0b6b3a764000081565b5f805160206102d1833981519152805415908190556100f6565b5f805160206102d1833981519152546101ae5760405162461bcd60e51b81526020600482015260136024820152721dda5d1a191c985dd85b08191a5cd8589b1959606a1b60448201526064015b60405180910390fd5b60405163a9059cbb60e01b8152336004820152670de0b6b3a764000060248201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af115801561021f573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061024391906102b5565b6102815760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016101a5565b565b8015158114610290575f80fd5b50565b5f602082840312156102a3575f80fd5b81356102ae81610283565b9392505050565b5f602082840312156102c5575f80fd5b81516102ae8161028356fea91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129a264697066735822122086d03dd0ac93876dcc7d8450420e92071a584f9b6452b41d07282203bd45a11f64736f6c63430008180033"
"bytecode": "0x60a060405234801561000f575f80fd5b5060405161032e38038061032e83398101604081905261002e9161003f565b6001600160a01b031660805261006c565b5f6020828403121561004f575f80fd5b81516001600160a01b0381168114610065575f80fd5b9392505050565b6080516102a461008a5f395f8181605e015261016f01526102a45ff3fe608060405234801561000f575f80fd5b5060043610610055575f3560e01c806304a3b7cd14610059578063238dafe01461009d578063328d8f72146100b95780633ccfd60b146100db57806384860633146100e3575b5f80fd5b6100807f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b5f546100a99060ff1681565b6040519015158152602001610094565b6100d96100c7366004610231565b5f805460ff1916911515919091179055565b005b6100d9610100565b6100f2670de0b6b3a764000081565b604051908152602001610094565b5f5460ff1661014c5760405162461bcd60e51b81526020600482015260136024820152721dda5d1a191c985dd85b08191a5cd8589b1959606a1b60448201526064015b60405180910390fd5b60405163a9059cbb60e01b8152336004820152670de0b6b3a764000060248201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af11580156101bd573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906101e19190610253565b61021f5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610143565b565b801515811461022e575f80fd5b50565b5f60208284031215610241575f80fd5b813561024c81610221565b9392505050565b5f60208284031215610263575f80fd5b815161024c8161022156fea26469706673582212204d29e01f9634f2f7d9fe8f91c0bdd1dafbb7f3368af71bef06b019d1634aacf764736f6c63430008180033"
}
29 changes: 15 additions & 14 deletions app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decodeFunctionData, keccak256, toBytes } from 'viem';
import { decodeFunctionData } from 'viem';
import { describe, expect, it } from 'vitest';

import artifact from './artifacts/ConditionalWithdrawal.json';
Expand All @@ -14,7 +14,6 @@ import {
conditionalWithdrawalFundingAmount,
encodeConditionalWithdrawalFunding,
encodeConditionalWithdraw,
encodeFlipConditionalWithdrawal,
encodeSetConditionalWithdrawalEnabled,
predictConditionalWithdrawal,
} from './conditionalWithdrawal';
Expand All @@ -39,39 +38,41 @@ describe('conditional withdrawal contract', () => {
expect(CONDITIONAL_WITHDRAWAL_SALT).toBe(
'0x75dea569b8cc7d9ea45d7d95a5d6bed33e1e378a31715724342462f8226adc8b',
);
expect(predictConditionalWithdrawal(VIBE)).toBe('0x7AE1BFB6116D154a0a27961a5d19C544D02015a9');
expect(predictConditionalWithdrawal(VIBE)).toBe('0xC655E339224d57B087A02Df7827A4068ae69aba1');
expect(predictConditionalWithdrawal(OTHER_VIBE)).not.toBe(predictConditionalWithdrawal(VIBE));
});

it('pins the stable storage word and exact enabled=true EIP-8130 predicate', () => {
expect(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT).toBe(
keccak256(toBytes('vibenet.validity.conditional-withdrawal.enabled.v1')),
);
expect(CONDITIONAL_WITHDRAWAL_ENABLED_MASK).toBe((1n << 256n) - 1n);
it('reads bool public enabled from slot 0 in the EIP-8130 predicate', () => {
expect(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT).toBe(0n);
expect(CONDITIONAL_WITHDRAWAL_ENABLED_MASK).toBe(0xffn);
expect(conditionalWithdrawalEnabledPredicate(WITHDRAWAL)).toEqual({
type: 'storage',
params: {
address: WITHDRAWAL,
slot: CONDITIONAL_WITHDRAWAL_ENABLED_SLOT,
mask: toWord((1n << 256n) - 1n),
slot: toWord(0n),
mask: toWord(0xffn),
op: '=',
value: toWord(1n),
},
});
});

it('encodes condition and fixed-withdrawal calls exactly', () => {
const functionNames = artifact.abi
.filter((item) => item.type === 'function')
.map((item) => item.name);
expect(functionNames).not.toContain('ENABLED_SLOT');
expect(functionNames).not.toContain('flip');
expect(functionNames).toContain('enabled');
expect(functionNames).toContain('setEnabled');
expect(functionNames).toContain('withdraw');
expect(encodeSetConditionalWithdrawalEnabled(WITHDRAWAL, true)).toEqual({
to: WITHDRAWAL,
data: `0x328d8f72${'0'.repeat(63)}1`,
});
expect(encodeSetConditionalWithdrawalEnabled(WITHDRAWAL, false).data).toBe(
`0x328d8f72${'0'.repeat(64)}`,
);
expect(encodeFlipConditionalWithdrawal(WITHDRAWAL)).toEqual({
to: WITHDRAWAL,
data: '0xcde4efa9',
});
expect(encodeConditionalWithdraw(WITHDRAWAL)).toEqual({
to: WITHDRAWAL,
data: '0x3ccfd60b',
Expand Down
17 changes: 5 additions & 12 deletions app/vibenet/demos/validity/lib/conditionalWithdrawal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import type { StoragePredicate } from './types';
export const conditionalWithdrawalAbi = artifact.abi as Abi;
export const conditionalWithdrawalBytecode = artifact.bytecode as Hex;

export const CONDITIONAL_WITHDRAWAL_ENABLED_SLOT =
'0xa91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129' as Hex;
export const CONDITIONAL_WITHDRAWAL_ENABLED_MASK = (1n << 256n) - 1n;
/** `bool public enabled` is the contract's first storage variable. */
export const CONDITIONAL_WITHDRAWAL_ENABLED_SLOT = 0n;
export const CONDITIONAL_WITHDRAWAL_ENABLED_MASK = 0xffn;
export const CONDITIONAL_WITHDRAWAL_AMOUNT = WAD;
export const CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD = 1_000_000n * WAD;
export const CONDITIONAL_WITHDRAWAL_FUNDING_TARGET = 2_000_000n * WAD;
Expand Down Expand Up @@ -171,25 +171,18 @@ export function encodeSetConditionalWithdrawalEnabled(
};
}

export function encodeFlipConditionalWithdrawal(withdrawal: Address): { to: Address; data: Hex } {
return {
to: withdrawal,
data: encodeFunctionData({ abi: conditionalWithdrawalAbi, functionName: 'flip' }),
};
}

export function encodeConditionalWithdraw(withdrawal: Address): { to: Address; data: Hex } {
return {
to: withdrawal,
data: encodeFunctionData({ abi: conditionalWithdrawalAbi, functionName: 'withdraw' }),
};
}

/** EIP-8130 condition requiring the stable enabled bit to equal true. */
/** EIP-8130 condition requiring `bool public enabled` in storage slot 0 to be true. */
export function conditionalWithdrawalEnabledPredicate(withdrawal: Address): StoragePredicate {
return storagePredicate(
withdrawal,
BigInt(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT),
CONDITIONAL_WITHDRAWAL_ENABLED_SLOT,
CONDITIONAL_WITHDRAWAL_ENABLED_MASK,
'=',
1n,
Expand Down
28 changes: 4 additions & 24 deletions app/vibenet/demos/validity/lib/contracts/ConditionalWithdrawal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,23 @@ interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}

/// @notice Permissionless condition switch backed by one stable storage word.
/// @notice Permissionless VIBE withdrawal controlled by a boolean condition.
contract ConditionalWithdrawal {
/// @dev Stable forever: keccak256("vibenet.validity.conditional-withdrawal.enabled.v1").
bytes32 public constant ENABLED_SLOT =
0xa91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129;
uint256 public constant WITHDRAWAL_AMOUNT = 1 ether;

IERC20 public immutable VIBE;
bool public enabled;

constructor(IERC20 vibe) {
VIBE = vibe;
}

function enabled() public view returns (bool value) {
bytes32 slot = ENABLED_SLOT;
assembly {
value := iszero(iszero(sload(slot)))
}
}

function setEnabled(bool value) external {
bytes32 slot = ENABLED_SLOT;
assembly {
sstore(slot, value)
}
}

function flip() external returns (bool value) {
bytes32 slot = ENABLED_SLOT;
assembly {
value := iszero(sload(slot))
sstore(slot, value)
}
enabled = value;
}

function withdraw() external {
require(enabled(), "withdrawal disabled");
require(enabled, "withdrawal disabled");
require(VIBE.transfer(msg.sender, WITHDRAWAL_AMOUNT), "transfer failed");
}
}
102 changes: 102 additions & 0 deletions app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { VIBENET_EXPLORER_PATH, VIBENET_WS_URL } from '../../../library/config';
import { CopyableValue } from '../../../components/CopyableValue';
import { AccountDemoShell } from '../../_components/AccountDemoShell';
import { DemoHeader } from '../../_components/DemoHeader';
import { ChevronIcon } from '../../_shared/dropdown';
import { newCallRow } from '../../account/library/calls';
import type { StoredAccount } from '../../account/library/model';
import { aaReceiptSucceeded, type AaReceiptLike } from '../../account/library/receipt';
Expand Down Expand Up @@ -76,6 +77,28 @@ type Observation = { enabled: boolean; block: bigint; at: number };
type AgentPhase = 'Waiting' | 'Scheduling' | 'Opening' | 'Closing' | 'Retrying';

const EMPTY_ATTEMPT: Attempt = { status: 'idle' };
const CONTRACT_SNIPPET = `interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}

contract ConditionalWithdrawal {
uint256 public constant WITHDRAWAL_AMOUNT = 1 ether;
IERC20 public immutable VIBE;
bool public enabled;

constructor(IERC20 vibe) {
VIBE = vibe;
}

function setEnabled(bool value) external {
enabled = value;
}

function withdraw() external {
require(enabled, "withdrawal disabled");
require(VIBE.transfer(msg.sender, WITHDRAWAL_AMOUNT), "transfer failed");
}
}`;

function rootAccount(account: StoredAccount, accounts: StoredAccount[]): StoredAccount {
let current = account;
Expand Down Expand Up @@ -778,6 +801,9 @@ function RaceTheAgentDemoInner() {
});
const validityAttempts = attemptHistoryRows(validity, validityHistory);
const manualAttempts = attemptHistoryRows(manual, manualHistory);
const predicateSnippet = withdrawal
? JSON.stringify(conditionalWithdrawalEnabledPredicate(withdrawal), null, 2)
: 'Resolving the conditional withdrawal address…';

return (
<AccountDemoShell
Expand Down Expand Up @@ -940,6 +966,26 @@ function RaceTheAgentDemoInner() {
</div>
</section>

<details className="group rounded-2xl border border-bds-gray-10 bg-background dark:border-white/10 dark:bg-white/[.04]">
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 marker:content-none sm:px-6 [&::-webkit-details-marker]:hidden">
<div>
<Text variant="caption" tone="muted">Advanced details</Text>
<Text variant="headline" className="mt-1">Contract and validity predicate</Text>
</div>
<ChevronIcon className="shrink-0 duration-150 group-open:rotate-180" />
</summary>
<div className="border-t border-bds-gray-10 px-5 py-5 sm:px-6 dark:border-white/10">
<Text variant="label.regular" tone="muted" className="max-w-2xl">
Solidity stores the first state variable, <code className="font-mono text-foreground">enabled</code>,
in slot 0. The validity transaction reads that slot directly and becomes eligible only when the boolean is true.
</Text>
<div className="mt-5 grid min-w-0 gap-4 lg:grid-cols-2">
<CodeSnippet label="ConditionalWithdrawal.sol" code={CONTRACT_SNIPPET} language="solidity" />
<CodeSnippet label="Withdrawal validity predicate" code={predicateSnippet} language="json" />
</div>
</div>
</details>

<section className="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(320px,.9fr)]">
<Card className="min-w-0 overflow-hidden bg-background p-5 sm:p-6 dark:bg-white/[.04]">
<Text variant="caption" tone="muted">Observed chain state</Text>
Expand Down Expand Up @@ -1102,6 +1148,62 @@ function AttemptHistoryCard({ title, attempts }: { title: string; attempts: Atte
);
}

type CodeLanguage = 'json' | 'solidity';
type CodeToken = { text: string; kind: 'comment' | 'key' | 'keyword' | 'literal' | 'number' | 'plain' | 'string' | 'type' };

const CODE_TOKEN_CLASS: Record<CodeToken['kind'], string> = {
comment: 'text-bds-gray-50 dark:text-[#7f8c98]',
key: 'text-base-blue dark:text-[#7eb8ff]',
keyword: 'text-purple-700 dark:text-[#c792ea]',
literal: 'text-bds-orange-70 dark:text-[#ff9d76]',
number: 'text-bds-orange-70 dark:text-[#f5c542]',
plain: 'text-bds-gray-80 dark:text-[#d6deeb]',
string: 'text-bds-green-70 dark:text-[#7ee0a8]',
type: 'text-base-blue dark:text-[#82aaff]',
};

function tokenizeCode(source: string, language: CodeLanguage): CodeToken[] {
const pattern = language === 'json'
? /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?|[{}[\]:,])/g
: /(\/\/[^\n]*|"(?:\\.|[^"\\])*"|\b(?:contract|interface|function|external|public|immutable|constant|returns|return|require|bool|uint256|address)\b|\b(?:IERC20|ConditionalWithdrawal)\b|\b\d+(?:\s+ether)?\b)/g;
const tokens: CodeToken[] = [];
let last = 0;
for (const hit of source.matchAll(pattern)) {
const text = hit[0];
const index = hit.index ?? 0;
if (index > last) tokens.push({ text: source.slice(last, index), kind: 'plain' });
let kind: CodeToken['kind'] = 'plain';
if (language === 'json') {
if (text.startsWith('"')) kind = text.endsWith(':') ? 'key' : 'string';
else if (text === 'true' || text === 'false' || text === 'null') kind = 'literal';
else if (/^-?\d/.test(text)) kind = 'number';
} else if (text.startsWith('//')) kind = 'comment';
else if (text.startsWith('"')) kind = 'string';
else if (/^\d/.test(text)) kind = 'number';
else if (text === 'IERC20' || text === 'ConditionalWithdrawal') kind = 'type';
else kind = 'keyword';
tokens.push({ text, kind });
last = index + text.length;
}
if (last < source.length) tokens.push({ text: source.slice(last), kind: 'plain' });
return tokens;
}

function CodeSnippet({ label, code, language }: { label: string; code: string; language: CodeLanguage }) {
return (
<Card className="min-w-0 overflow-hidden bg-background p-4 dark:bg-white/[.04]">
<Text variant="caption" tone="muted">{label}</Text>
<pre className="mt-3 min-h-80 overflow-auto rounded-xl bg-bds-gray-5 p-4 font-mono text-[11px] leading-5 dark:bg-[#0b0d12]">
<code>
{tokenizeCode(code, language).map((token, index) => (
<span key={`${index}-${token.text}`} className={CODE_TOKEN_CLASS[token.kind]}>{token.text}</span>
))}
</code>
</pre>
</Card>
);
}

function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0">
Expand Down
Loading