Skip to content

Commit e4abcb8

Browse files
authored
Merge pull request #66 from szhygulin/release/0.5.2
release/0.5.2
2 parents 0686e8b + 18c59d3 commit e4abcb8

7 files changed

Lines changed: 61 additions & 21 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ This is an **agent-driven portfolio management** tool, not a wallet replacement.
3636

3737
## Security model
3838

39+
**VaultPilot assumes the AI agent can be compromised, the MCP server can be compromised, and your host computer can be compromised. Only your Ledger device is trusted.** Every transaction is cryptographically bound across every layer so that tampering anywhere — a swapped recipient, a rewritten swap route, a smuggled approval — produces a visible mismatch on the device screen, giving you the chance to reject before anything is signed.
40+
3941
Private keys never leave the Ledger device. Every state-changing transaction is prepared read-only by the server, previewed in human-readable form, and approved on the device's own screen — the only display in the pipeline that isn't filtered through the agent.
4042

4143
```
4244
user-intent ──► agent ──► MCP server ──► WalletConnect / USB-HID ──► Ledger Live / host ──► Ledger device
4345
```
4446

45-
VaultPilot layers defenses so most single-layer compromises are caught by at least one cross-check, and the cases that aren't are called out honestly. The layers include a server-side prepare↔send fingerprint, an independent 4byte.directory selector cross-check, an agent-side ABI decode and pair-consistency pre-sign hash recomputation that auto-run at `preview_send` and are reported in a `CHECKS PERFORMED` block (with a swiss-knife decoder URL as a suggested fallback when the agent's ABI decode is low-confidence), a Ledger blind-sign hash the user matches on-device, a verbatim `PREPARE RECEIPT` of the args the agent actually passed, a `previewToken` + `userDecision` gate against accidental preview-step collapse, and — for skeptical users on high-value flows — a `get_verification_artifact` that routes bytes to a second, independent LLM for cross-verification.
47+
VaultPilot layers defenses so most single-layer compromises are caught by at least one cross-check, and the cases that aren't are called out honestly. The layers include a server-side prepare↔send fingerprint, an independent 4byte.directory selector cross-check, an agent-side ABI decode and pair-consistency pre-sign hash recomputation that auto-run at `preview_send` and are reported in a `CHECKS PERFORMED` block (with a swiss-knife decoder URL as a suggested fallback when the agent's ABI decode is low-confidence), an on-device final check — in blind-sign mode the user matches a Ledger-displayed hash against the one the server returned; in clear-sign mode (Aave, Lido, 1inch, LiFi, approve plugins) the user checks decoded fields (function name, amount, recipient, spender) against the compact summary shown in chat — a verbatim `PREPARE RECEIPT` of the args the agent actually passed, a `previewToken` + `userDecision` gate against accidental preview-step collapse, a WalletConnect session-topic cross-check (the agent surfaces the last 8 chars of the WC session `topic` and asks the user to confirm a matching session exists in Ledger Live → Settings → Connected Apps, catching peer impersonation any self-reported name/URL can't), and — for skeptical users on high-value flows — a `get_verification_artifact` that routes bytes to a second, independent LLM for cross-verification.
4648

4749
**See [SECURITY.md](./SECURITY.md)** for the full defenses table, threat → catches-it mapping, honest limits, the `payloadFingerprint` verification recipe, and the second-agent verification flow.
4850

@@ -88,7 +90,7 @@ Meta:
8890

8991
Execution (Ledger-signed):
9092

91-
- `pair_ledger_live` (WalletConnect, EVM), `pair_ledger_tron` (USB HID, TRON), `get_ledger_status` — session management and account discovery; `get_ledger_status` returns per-chain EVM exposure (`accountDetails[]` with `address`, `chainIds`, `chains`) so duplicate-looking addresses across chains are disambiguated, and a `tron: [{ address, path, appVersion, accountIndex }, …]` array (one entry per paired TRON account) when `pair_ledger_tron` has been called. Pass `accountIndex: 1` (2, 3, …) to pair additional TRON accounts.
93+
- `pair_ledger_live` (WalletConnect, EVM), `pair_ledger_tron` (USB HID, TRON), `get_ledger_status` — session management and account discovery; `get_ledger_status` returns per-chain EVM exposure (`accountDetails[]` with `address`, `chainIds`, `chains`) so duplicate-looking addresses across chains are disambiguated, the WalletConnect session `topic` (the agent is instructed to surface its last 8 chars and ask the user to verify a matching session in Ledger Live → Settings → Connected Apps before the first `send_transaction` — any WC peer can self-report "Ledger Wallet" / `wc.apps.ledger.com`, but the session topic is unique per pairing), and a `tron: [{ address, path, appVersion, accountIndex }, …]` array (one entry per paired TRON account) when `pair_ledger_tron` has been called. Pass `accountIndex: 1` (2, 3, …) to pair additional TRON accounts.
9294
- `prepare_aave_supply` / `_withdraw` / `_borrow` / `_repay`
9395
- `prepare_compound_supply` / `_withdraw` / `_borrow` / `_repay`
9496
- `prepare_morpho_supply` / `_withdraw` / `_borrow` / `_repay` / `_supply_collateral` / `_withdraw_collateral`

SECURITY.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ agent prepare transactions against your Ledger device.
88
For the product overview, install instructions, and tool reference, see the
99
main [README](./README.md).
1010

11+
## Why trust VaultPilot?
12+
13+
VaultPilot assumes the AI agent can be compromised, the MCP server can be
14+
compromised, and your host computer can be compromised. Only your Ledger
15+
hardware is trusted. Every transaction is cryptographically bound across
16+
every layer so that tampering at any point — a swapped recipient, a
17+
rewritten swap route, a smuggled approval — produces a visible mismatch
18+
on your Ledger screen, giving you the chance to reject before anything
19+
is signed.
20+
21+
In practice: the agent relays a hash it computed locally, the MCP relays
22+
the bytes it intends to broadcast, and the Ledger re-derives its own hash
23+
from the bytes it actually receives. You compare the two on the device's
24+
own screen — the one display in the pipeline that no software on the host
25+
can forge. No layer in between can fake a match it doesn't have.
26+
1127
## Trust boundaries
1228

1329
The signing pipeline crosses several independent trust boundaries, each of

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vaultpilot-mcp",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"mcpName": "io.github.szhygulin/vaultpilot-mcp",
55
"description": "MCP server for AI agents (Claude Code, Claude Desktop, Cursor) to manage a self-custodial crypto portfolio through a Ledger hardware wallet. Reads on-chain wallet balances, ENS, token prices, and DeFi positions across Ethereum/Arbitrum/Polygon/Base (Aave V3, Compound V3, Morpho Blue, Uniswap V3 LP, Lido stETH, EigenLayer), surfaces liquidation/health-factor alerts and protocol risk scores, then prepares unsigned EVM transactions (supply, borrow, repay, withdraw, stake, unstake, native/ERC-20 send, and LiFi-routed swaps and cross-chain bridges) that the user signs on their Ledger device via WalletConnect — private keys never leave the hardware wallet.",
66
"type": "module",

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "io.github.szhygulin/vaultpilot-mcp",
44
"title": "VaultPilot MCP",
55
"description": "Self-custodial crypto portfolio: read EVM DeFi, sign on Ledger via WalletConnect.",
6-
"version": "0.5.1",
6+
"version": "0.5.2",
77
"websiteUrl": "https://github.com/szhygulin/vaultpilot-mcp",
88
"repository": {
99
"url": "https://github.com/szhygulin/vaultpilot-mcp",
@@ -14,7 +14,7 @@
1414
"registryType": "npm",
1515
"registryBaseUrl": "https://registry.npmjs.org",
1616
"identifier": "vaultpilot-mcp",
17-
"version": "0.5.1",
17+
"version": "0.5.2",
1818
"transport": { "type": "stdio" },
1919
"environmentVariables": [
2020
{

src/modules/execution/index.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,13 @@ async function sendTronTransaction(args: SendTransactionArgs): Promise<{
381381
const rehash = tronPayloadFingerprint(tx.rawDataHex);
382382
if (rehash !== tx.verification.payloadHash) {
383383
throw new Error(
384-
`TRON payload hash mismatch at send time. Previewed ${tx.verification.payloadHash}, ` +
384+
`SECURITY: TRON payload hash mismatch at send time. Previewed ${tx.verification.payloadHash}, ` +
385385
`about to sign ${rehash}. The rawDataHex changed between preview and send — refusing ` +
386-
`to forward to the Ledger.`,
386+
`to forward to the Ledger. Do NOT retry this handle. Re-prepare the transaction from ` +
387+
`scratch (call the prepare_* tool again) and compare the new preview carefully — a ` +
388+
`drift here means the bytes mutated inside the MCP process between the moment the user ` +
389+
`reviewed them and the moment they would have been signed, which is not a normal ` +
390+
`operating condition and may indicate a compromised intermediary.`,
387391
);
388392
}
389393
}
@@ -522,9 +526,13 @@ async function runEvmPreSignGuards(tx: UnsignedTx): Promise<void> {
522526
});
523527
if (rehash !== tx.verification.payloadHash) {
524528
throw new Error(
525-
`Payload hash mismatch at preview/send time. Previewed ${tx.verification.payloadHash}, ` +
526-
`about to sign ${rehash}. The transaction bytes changed between prepare and preview — ` +
527-
`refusing to proceed.`,
529+
`SECURITY: payload hash mismatch at preview/send time. Previewed ${tx.verification.payloadHash}, ` +
530+
`about to sign ${rehash}. The transaction bytes (chain/to/value/data) changed between ` +
531+
`prepare and preview — refusing to proceed. Do NOT retry this handle. Re-prepare the ` +
532+
`transaction from scratch and compare the new preview against user intent carefully: ` +
533+
`this drift means the bytes mutated inside the MCP process after the user reviewed ` +
534+
`them, which is not a normal operating condition and may indicate a compromised ` +
535+
`intermediary swapping bytes at send time.`,
528536
);
529537
}
530538
}
@@ -719,11 +727,14 @@ export async function sendTransaction(args: SendTransactionArgs): Promise<{
719727
}
720728
if (args.previewToken !== stashed.previewToken) {
721729
throw new Error(
722-
"`previewToken` does not match the current pin on this handle. This usually means " +
723-
"preview_send was re-called with `refresh: true` after you captured the token — the " +
724-
"new pin has a new token (and a new preSignHash the user must re-match on-device). " +
725-
"Call preview_send again, surface the fresh hash + EXTRA CHECKS menu to the user, and " +
726-
"retry with the new token.",
730+
"SECURITY: `previewToken` does not match the current pin on this handle. The benign " +
731+
"explanation is that preview_send was re-called with `refresh: true` after the token " +
732+
"was captured — in that case, the new pin has a new token AND a new preSignHash the " +
733+
"user MUST re-match on-device. Do NOT retry with the old token: call preview_send " +
734+
"again, surface the fresh CHECKS PERFORMED block and the new blind-sign hash to the " +
735+
"user, and pass the new token. If the user did not ask for a refresh and the hash on " +
736+
"their Ledger screen no longer matches the one they were shown, reject on-device — a " +
737+
"token drift without a user-initiated refresh is not expected.",
727738
);
728739
}
729740
const tx = consumeHandle(args.handle);

src/signing/tron-usb-signer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,14 @@ export async function signTronTxOnLedger(
238238
const { address } = await app.getAddress(path, false);
239239
if (address !== req.expectedFrom) {
240240
throw new Error(
241-
`Ledger device address (${address}) does not match the prepared tx's \`from\` ` +
242-
`(${req.expectedFrom}). Either connect the Ledger that holds keys for \`from\`, ` +
243-
`or re-prepare the tx for the Ledger-derived address (\`pair_ledger_tron\`).`
241+
`SECURITY: Ledger device address (${address}) does not match the prepared tx's \`from\` ` +
242+
`(${req.expectedFrom}). Do NOT retry until you know which of these two is the cause: ` +
243+
`(1) the wrong Ledger is connected, or (2) the \`from\` field in the prepared tx was ` +
244+
`tampered with between prepare and send. Check the device — if the address it derives ` +
245+
`on-screen is the one you expected (${address}), the tx's \`from\` was altered: abort, ` +
246+
`re-prepare from scratch, and compare the new preview's \`from\` against user intent. ` +
247+
`If the address on-screen is not your expected account, connect the correct Ledger or ` +
248+
`re-prepare the tx for the Ledger-derived address via \`pair_ledger_tron\`.`
244249
);
245250
}
246251
const signature = await app.signTransaction(
@@ -251,7 +256,13 @@ export async function signTronTxOnLedger(
251256
// Ledger returns the signature as a hex string (65 bytes: r || s || v).
252257
if (!/^[0-9a-fA-F]{130}$/.test(signature)) {
253258
throw new Error(
254-
`Ledger returned an unexpected signature shape (length ${signature.length}). Expected 130 hex chars.`
259+
`SECURITY: Ledger returned an unexpected signature shape (length ${signature.length}, ` +
260+
`expected 130 hex chars = 65 bytes r‖s‖v). Do NOT retry or broadcast this signature. ` +
261+
`A well-formed Ledger TRON signature is always 130 hex chars; anything else means the ` +
262+
`signature did not come from a healthy device exchange. Disconnect and reconnect the ` +
263+
`Ledger, reopen the TRON app, and re-prepare the transaction from scratch. If the ` +
264+
`error repeats on a clean reconnect, treat the host's USB/HID path as potentially ` +
265+
`compromised and stop using it for signing until investigated.`
255266
);
256267
}
257268
return { signature, signerAddress: address };

0 commit comments

Comments
 (0)