This is the CLI-first end-to-end demo hub for VaultGuard. It’s designed for:
- a smooth demo recording, and
- judges running the project without guessing missing steps.
It covers:
- CRE workflow orchestration (simulate + broadcast)
- Custom PoRFeed onchain verification + state updates
- VaultGuard RiskMode gating (NORMAL → LIMITED → PAUSED → NORMAL)
- ACE policy enforcement (allowlist + min ticket + max volume)
- Optional AI tightening (healthy + calm/panic)
- Sepolia + Tenderly VTN runners
Demo Tip
The demo uses two terminals:
- Terminal A — attestation server
- Terminal B — workflow execution and user transactions
The CLI demo demonstrates the following vault lifecycle:
- Healthy reserves → NORMAL
- ACE policies enforced
- Reserve degradation → LIMITED
- Attestation staleness → PAUSED
- Optional AI tightening
- Recovery → NORMAL
sequenceDiagram
participant Admin as Attestation Server
participant CRE as Chainlink CRE Workflow
participant PF as VaultGuardPoRFeed
participant Vault as VaultGuard ERC4626
participant ACE as Chainlink ACE
participant User as Vault User
Admin->>CRE: Serve signed EIP-712 attestation
CRE->>PF: Broadcast verified report
PF->>PF: Validate attestation<br/>Compute risk mode
PF->>Vault: update risk state
User->>Vault: deposit / withdraw / redeem
Vault->>ACE: Evaluate policies
ACE-->>Vault: Allow / Reject
Vault-->>User: Execute or revert
This flow shows how VaultGuard connects off-chain attestations, CRE orchestration, PoR state updates, and ACE policy enforcement into one end-to-end system.
- Foundry (
cast) - Installation guide - Bun (v1.3+) - Download here
- Chainlink CRE CLI (
cre) - Installation instructions - ETH on ETH Sepolia
Optional (only required if running the AI risk demo):
- OpenAI LLM API key - Get from OpenAI platform
git clone https://github.com/VitalR/cre-vaultguard.git
cd cre-vaultguardInstall workflow dependencies:
bun install --cwd ./workflowsInstall attestation server dependencies:
cd attestation-server && npm iCreate .env in the repo root (same folder as Makefile) with at least:
RPC + addresses
SEPOLIA_URLASSET_ADDRESSVAULT_ADDRESSPORFEED_ADDRESS
Demo actors
ALLOWLIST_ADDRESSALLOWLIST_PKNO_ALLOWLIST_ADDRESSNO_ALLOWLIST_PK
Operator key (used for onchain admin toggles during demo)
CRE_ETH_PRIVATE_KEY
Amounts
AMT_OK(a normal deposit amount that should pass policies in NORMAL)AMT_DUST(below min ticket; should revert)AMT_MIN_OK(small withdraw amount allowed under LIMITED)
Optional (recommended for max-volume policy demo)
AMT_OVER(a deposit amount that exceeds the ACE max-volume cap; should revert)
Tip: store private keys only in local
.env(never commit).
If you run Tenderly VTN:
RPC_URL(orTENDERLY_RPC)- same addresses/keys/amounts pattern as above
- optionally:
TENDERLY_MAX_AGE_SEC,TENDERLY_PAUSED_MAX_AGE_SEC
Terminal A:
make server-start
# or:
# cd attestation-server && npm run devUseful checks:
cd attestation-server && make debug
cd attestation-server && make latest
cd attestation-server && make state
cd attestation-server && make signalsTerminal B (repo root):
make demo
# or:
# bash scripts/demo.shThis script performs:
- baseline: healthy + AI OFF → NORMAL
- ACE: allowlist + min ticket reverts
- NORMAL deposit success
- LIMITED: deposit blocked, withdraw allowed
- PAUSED: deposits/withdraws blocked
- optional AI calm/panic tightening
- reset + redeem to clean state
make status
# and/or:
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLcast send $ASSET_ADDRESS "mint(address,uint256)" $ALLOWLIST_ADDRESS 1000000000 --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PK
cast send $ASSET_ADDRESS "mint(address,uint256)" $NO_ALLOWLIST_ADDRESS 1000000000 --rpc-url $SEPOLIA_URL --private-key $NO_ALLOWLIST_PK
cast send $ASSET_ADDRESS "approve(address,uint256)" $VAULT_ADDRESS 2000000000 --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PK
cast send $ASSET_ADDRESS "approve(address,uint256)" $VAULT_ADDRESS 2000000000 --rpc-url $SEPOLIA_URL --private-key $NO_ALLOWLIST_PKTerminal A:
make server-healthy
# (equivalent) cd attestation-server && make healthyTerminal B:
make workflow-broadcast
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLExpected: 0 (NORMAL).
cast send $VAULT_ADDRESS "deposit(uint256,address)" $AMT_OK $NO_ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $NO_ALLOWLIST_PKExpected: revert (AllowPolicy).
cast send $VAULT_ADDRESS "deposit(uint256,address)" $AMT_DUST $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PKExpected: revert (min ticket).
Set AMT_OVER in .env to exceed the configured ACE max-volume cap.
cast send $VAULT_ADDRESS "deposit(uint256,address)" $AMT_OVER $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PKExpected: revert (max volume policy).
cast send $VAULT_ADDRESS "deposit(uint256,address)" $AMT_OK $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PKTerminal A:
make server-limitedTerminal B:
make workflow-broadcast
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLExpected: 1 (LIMITED).
Deposit should revert:
cast send $VAULT_ADDRESS "deposit(uint256,address)" $AMT_OK $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PKWithdraw should succeed:
cast send $VAULT_ADDRESS "withdraw(uint256,address,address)" $AMT_MIN_OK $ALLOWLIST_ADDRESS $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PKTerminal A:
make server-staleTerminal B:
make workflow-broadcast
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLExpected: 2 (PAUSED). Deposits/withdrawals should revert.
make ai-on
# Equivalent explicit calls:
# cast send $PORFEED_ADDRESS "setAiEnabled(bool)" true --rpc-url $SEPOLIA_URL --private-key $CRE_ETH_PRIVATE_KEY
# cast send $VAULT_ADDRESS "setAiEnabled(bool)" true --rpc-url $SEPOLIA_URL --private-key $CRE_ETH_PRIVATE_KEYTerminal A:
make server-healthy-ai-calmTerminal B:
make workflow-broadcast
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLExpected: 0 (NORMAL).
Terminal A:
make server-healthy-ai-panicTerminal B:
make workflow-broadcast
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLExpected: 1 (LIMITED) (or whatever your “tighten” outcome is configured to be).
make ai-offWhen AI is disabled, workflow logs should include only:
[AI] AI is disabled, skipping AI signals + OpenAI call.
Terminal A:
make server-healthyTerminal B:
make workflow-broadcast
cast call $VAULT_ADDRESS "riskMode()(uint8)" --rpc-url $SEPOLIA_URLExpected: 0 (NORMAL).
Redeem (policy cap per tx may apply):
cast send $VAULT_ADDRESS "redeem(uint256,address,address)" 50000000000000000000 $ALLOWLIST_ADDRESS $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PK
REMAINING=$(cast call $VAULT_ADDRESS "balanceOf(address)(uint256)" $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL | awk '{print $1}')
cast send $VAULT_ADDRESS "redeem(uint256,address,address)" $REMAINING $ALLOWLIST_ADDRESS $ALLOWLIST_ADDRESS --rpc-url $SEPOLIA_URL --private-key $ALLOWLIST_PKUse the dedicated runner:
bash scripts/tenderly/demo.shNotes:
- Fork timestamps can make “stale” behave differently; the Tenderly script may lower
maxAgeSectemporarily to force PAUSED. - Ensure
deployments/tenderly-vtn.jsonand.env.tenderly-vtnagree on RPC + addresses.
If riskMode doesn’t change after broadcast:
- confirm server scenario:
cd attestation-server && make state - confirm PoRFeed nonce increments:
cast call $PORFEED_ADDRESS "lastNonce()(uint64)" --rpc-url $SEPOLIA_URL
- confirm correct workflow target:
--target=staging-settingsvs--target=tenderly-vtn - confirm AI flags:
cast call $PORFEED_ADDRESS "aiEnabled()(bool)" --rpc-url $SEPOLIA_URL cast call $VAULT_ADDRESS "aiEnabled()(bool)" --rpc-url $SEPOLIA_URL