Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Prepares and sends a smart contract audit request to Slack (Sujith or burrasec team); use when a PR is ready for external audit review.
.claude/skills/lifinance-request-audit/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 350% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 144% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 184% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 181% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 174% | 0% |
> Usage: /request-audit <PR_NUMBER_OR_URL> [--urgent]
Fetch a PR, extract scope + context, draft an audit request (parent post + thread reply), let the user pick the channel(s) and optionally edit, post to Slack only after explicit confirmation, then move the linked Linear ticket to In Audit.
| Alias | Channel | Mention ID | Display name (manual fallback) | Greeting position | |---|---|---|---|---| | sujith | #dev-sc-audit | <@U05GN6XH57T> | @Sujith Somraaj | greeting before PR/Commit | | burrasec | #dev-sc-audit-burrasec | <@U094M720QDP> | @Josip Koncurat | PR/Commit before greeting |
Convention: a channel #X is posted to via env var WEBHOOK_X (uppercase, hyphens → underscores). URLs live in 1Password vault Developers Smart Contract, item Webhooks SC Channels.
WEBHOOK_DEV_SC_AUDIT=https://hooks.slack.com/services/...
WEBHOOK_DEV_SC_AUDIT_BURRASEC=https://hooks.slack.com/services/...If a var is unset, that channel falls back to a manual file (see Step 6, exit 2).
bashgh pr view <PR_NUMBER> --repo lifinance/contracts \ --json number,title,body,url,headRefName,commits,files,labels,state
Extract: number, url, title, body, commits[].oid (use the last entry as latest_commit_oid — re-run the command with --json commits if the array came back empty), files[], labels[].
PRs in this repo are usually linked to a Linear ticket in the EXSC team (https://linear.app/lifi-linear/team/EXSC/all); the description is high-value audit context (motivation, design decisions, scope boundaries). Extract an EXSC-\d+ identifier from, in order:
EXSC-\d+ token, or a linear.app/lifi-linear/issue/EXSC-\d+/… URL.EXSC-\d+ pattern.headRefName — branch like feat/exsc-1234-… (case-insensitive).If found, fetch with mcp__claude_ai_Linear__get_issue (id = EXSC-1234). Treat the description and comments as another input source for the context paragraph (Step 3). If the ticket links a Slack thread, read it for context (Step 1b).
If no identifier is found, skip silently — don't ask the user.
Don't stop to ask for a Slack thread — the PR plus the Linear ticket (Step 1a) are enough by default. Pull Slack context only when it's free:
paragraph (Step 3).
Parse a Slack URL to channel_id + message_ts, then read with slack_read_thread:
https://lifi-protocol.slack.com/archives/{channel_id}/p{ts_without_dot}
→ message_ts: insert dot after 10th digit, e.g. p1776070807528139 → 1776070807.528139Scope (contract names + versions). Try in order; prefer 1–3 over 4:
[GenericSwapFacetV3 v2.0.0, WithdrawablePeriphery v2.0.0, …]| Contract | Version | … |Scope: line.sol filenames from files[] under src/Facets|Periphery|Helpers|Libraries|Security/Context — extract from PR body: Summary / What's in this PR (2–5 sentences, drop implementation trivia), Why / motivation, NOT in this PR (exclusions matter as much as inclusions), and merge-order / multi-PR sequencing if step N of M.
Urgency — flag as urgent if any of: --urgent flag passed, PR title/body/labels mention "urgent" / "time pressure" / "blocker" / "ASAP" / "today" or carry an urgency label, the Linear ticket has priority: 1 (Urgent), or the Linear ticket's dueDate is within ~3 days.
Audit: {scope_summary} {urgency_suffix} :thread:scope_summary — plain text, no backticks. 2–3 contracts: list all comma-separated. 4+contracts: {first} + N more, e.g. GenericSwapFacetV3 v2.0.0 + 6 more.
urgency_suffix — (urgent) if urgent, else omit the placeholder entirely.Examples:
Audit: DeBridgeDlnFacet v1.1.0 :thread:
Audit: GenericSwapFacetV3 v2.0.0 + 6 more :thread:
Audit: Patcher v1.0.1 (urgent) :thread:Both channels use the same blocks — greeting (with mention), PR + Commit, Scope, Context — in two orderings:
Sujith (greeting first):
Hey <@U05GN6XH57T>! {greeting_line}
PR: {pr_url}
Commit: {pr_url}/commits/{latest_commit_oid}
Scope: {full_scope_list}
{context}Burrasec (PR/Commit first):
PR: {pr_url}
Commit: {pr_url}/commits/{latest_commit_oid}
Hey <@U094M720QDP>. {greeting_line}
Scope: {full_scope_list}
{context}Fields:
greeting_line — natural variation, e.g. "I have a new audit for you." / "We have a newcontract change to audit." Append urgency: "It's a bit urgent — are you able to review this today?"
full_scope_list — comma-separated, every contract with its version. Backticks per thecode-style rule below when posting via webhook; plain text in the manual-fallback file.
context — see next section.The PR body defines what is in this PR. The Linear ticket and Slack thread explain why. Never let either expand the described scope beyond what the PR body states.
Merge all available sources into one coherent paragraph:
If the PR body has an explicit "NOT in this PR" / "Explicitly excluded" section, reflect those boundaries — auditors need to know what's out of scope as much as what's in.
3–7 sentences total, written as prose (not bullets) so it reads naturally in Slack. If the Linear ticket or Slack thread had a long discussion, extract only the decision-relevant parts; drop back-and-forth, emoji reactions, administrative replies.
Code style (applies to webhook posts; the manual-fallback file deliberately uses plain text — see Step 6 exit 2). Wrap in backticks every function, variable, contract, repo, error, constant, selector, and version string — transfer(), safeTransfer, LibAsset.transferERC20, LibAsset.sol, contracts-tron, TransferFailed, contractSelectorIsAllowed, v2.1.3. Plain prose words (the, this, PR, etc.) are never wrapped.
Show the full preview using markdown code blocks so the user sees exactly what will be posted:
`## Audit Request Preview **PR:** #{pr_number} — {pr_title_truncated_to_80_chars} **Commit:** `{latest_commit_oid}` **Urgent:** {Yes / No} **Linear:** {EXSC-1234 → In Audit on send, or "none linked"} --- ### Option 1: Sujith (#dev-sc-audit) **Parent message:**
{parent_message}
**Thread reply:**{thread_reply_sujith}
---
### Option 2: Burrasec (#dev-sc-audit-burrasec)
**Parent message:**{parent_message}
**Thread reply:**{thread_reply_burrasec}
---Then ask:
Where should I send this?
1 — Sujith only
2 — Burrasec only
3 — Both channels
4 — Cancel
Reply 1/2/3/4, or suggest edits to the message text before I send.Stop. Wait for the reply.
Apply, show the updated message, confirm before sending:
Updated message:
[…]
Ready to send? (yes / cancel)For each chosen channel, independently (one channel failing must not abort siblings):
:thread: droppedfrom the parent — incoming webhooks can't thread, so we collapse to one self-contained message. Use the backtick code style.
/tmp/audit-{pr_number}-{channel}.txt
bash bunx tsx script/utils/send-slack-webhook-message.ts \ --channel {channel} \ --message-file /tmp/audit-{pr_number}-{channel}.txt
where {channel} is dev-sc-audit (sujith) or dev-sc-audit-burrasec (burrasec).
| Exit | Meaning | Action | |---|---|---| | 0 | sent | ✅ Sent to #{channel} | | 2 | WEBHOOK_* env var not set for that channel | Manual fallback for this channel only — see below | | 1 | Slack / network error | Report stderr, do not retry, do not write fallback |
Two transforms from the webhook version: display-name mentions instead of <@…> (Slack only resolves API mentions through its API, not when pasted), and plain text (no backticks — Slack doesn't render inline code from externally-pasted content).
Build the full file content first (one block per fallback channel; omit any channel that already posted), then write once to /tmp/audit-request-{pr_number}.md — never write one channel and overwrite with another. Format:
markdown# Audit Request — PR #{pr_number} ## #{channel_name} ### Parent message Post this as a new message in the channel: --- {parent message — display name mentions — plain text} --- ### Thread reply Reply to the parent message with this (click the reply icon on the parent): --- {thread reply — display name mentions — plain text} ---
Tell the user, naming only the channel(s) that fell back. Print one line per failed channel, substituting {webhook_var} with the exact missing env var (WEBHOOK_DEV_SC_AUDIT or WEBHOOK_DEV_SC_AUDIT_BURRASEC):
⚠️ {webhook_var} is not set in .env — wrote manual fallback for #{channel} to
/tmp/audit-request-{pr_number}.md. Set the env var (URL in 1Password -> Developers
Smart Contract -> Webhooks SC Channels) to post automatically next time.Only if Step 1a found a linked EXSC-\d+ ticket — otherwise skip silently. The EXSC team has a dedicated In Audit workflow state so the team can see at a glance which PRs are with the auditors; advance the ticket into it once the request is actually delivered.
0) — move it now: mcp__claude_ai_Linear__save_issue id = EXSC-1234 state = "In Audit"
Linear resolves the state by name. Report ✅ Moved EXSC-1234 to In Audit.
In Audit or a later state (In Review, Done, or anycompleted/canceled state) — leave it; never move a ticket backward. Say so and skip.
0) or the send errored — don't move itautomatically; the request isn't delivered yet. Add a reminder to the output instead: ↪️ Move EXSC-1234 to In Audit once you've posted the message manually.
If the save_issue call itself fails, report the error and tell the user to move the ticket by hand — the Slack post already succeeded, so never retry in a loop or treat it as fatal.
| Situation | Action | |---|---| | PR not found | Report and stop | | Scope can't be determined | List changed src/ files, ask user to confirm scope | | User replies with anything other than 1–4 at the confirm step | Ask again |
(Helper exit codes 0/1/2 are handled in Step 6.)
Input: /request-audit 1715
Step 1: gh pr view 1715 …. Latest commit OID is the last commits[] entry: 142d9d809e8184fff4a21605fcd41983ed2e0e4d.
Step 2 scope (from PR title brackets):
GenericSwapFacetV3 v2.0.0, WithdrawablePeriphery v2.0.0, LiFiDEXAggregator v1.13.0,
ReceiverAcrossV3 v1.2.0, ReceiverChainflip v1.1.0, ReceiverStargateV2 v1.2.0, TokenWrapper v1.3.0Step 3 parent: Audit: GenericSwapFacetV3 v2.0.0 + 6 more :thread:
Step 3 Sujith thread reply:
Hey <@U05GN6XH57T>! I have a new audit for you.
PR: https://github.com/lifinance/contracts/pull/1715
Commit: https://github.com/lifinance/contracts/pull/1715/commits/142d9d809e8184fff4a21605fcd41983ed2e0e4d
Scope: `GenericSwapFacetV3 v2.0.0`, `WithdrawablePeriphery v2.0.0`, `LiFiDEXAggregator v1.13.0`, `ReceiverAcrossV3 v1.2.0`, `ReceiverChainflip v1.1.0`, `ReceiverStargateV2 v1.2.0`, `TokenWrapper v1.3.0`
Tron's canonical USDT contract declares `transfer()` as `returns (bool)` but omits the actual `return true` — the EVM returns 32 zero bytes, which Solady's `safeTransfer` interprets as `false` and reverts. Rather than ship Tron-specific bytecode to 60+ EVM chains, the team maintains a `contracts-tron` fork where the actual bypass lives inside `LibAsset.transferERC20`. This PR is step 1 of a two-part audit sequence: it routes every periphery ERC20 transfer through `LibAsset` so that the single fork-level change covers all call sites automatically — `LibAsset.sol` is not modified here (stays at `v2.1.3`) and the PR contains no Tron-specific code. Once this PR is audited and merged, a second audit will follow for the `contracts-tron` fork PR #9 where `LibAsset` receives the Tron USDT bypass. The same audit cycle is also used to land two breaking changes: `GenericSwapFacetV3` migrates from the legacy `contractIsAllowed` + `selectorIsAllowed` pair to the stricter `contractSelectorIsAllowed`, and `WithdrawablePeriphery` adds a `ZeroAmount` revert plus routes native transfers through `LibAsset.transferAsset`. Approval and native-ETH paths are intentionally out of scope — the Tron USDT issue is `transfer()`-only.(Burrasec reply is structurally identical, just PR/Commit first, then Hey @Josip Koncurat. …, same scope and same context paragraph.)
Step 6: Helper sends to dev-sc-audit (WEBHOOK_DEV_SC_AUDIT); exit 0 → ✅. If unset → exit 2 → manual fallback for that channel only.
This example calibrates two things abstract rules can't: the prose density of the context paragraph (5–7 sentences of compressed reasoning, never bullet-listed) and the exact template layout for both auditors.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 14,139 | 4,540 | -68% | 1 | 1 | 0% | 1,779 | 4,591 | +158% | 0 | 0 | — |
case-02 | fail→fail | 13,886 | 4,930 | -64% | 1 | 1 | 0% | 2,243 | 4,595 | +105% | 0 | 0 | — |
case-03 | fail→fail | 10,878 | 11,683 | +7% | 1 | 1 | 0% | 1,726 | 4,571 | +165% | 0 | 0 | — |
case-04 | fail→pass | 5,771 | 2,951 | -49% | 1 | 1 | 0% | 1,080 | 4,860 | +350% | 0 | 0 | — |
case-05 | pass→fail | 5,328 | 3,272 | -39% | 1 | 1 | 0% | 847 | 4,944 | +484% | 0 | 0 | — |
case-06 | pass→pass | 4,014 | 3,289 | -18% | 1 | 1 | 0% | 743 | 5,083 | +584% | 0 | 0 | — |
case-07 | fail→pass | 13,332 | 4,590 | -66% | 1 | 1 | 0% | 2,125 | 5,188 | +144% | 0 | 0 | — |
case-08 | fail→pass | 12,843 | 5,471 | -57% | 1 | 1 | 0% | 1,878 | 5,334 | +184% | 0 | 0 | — |
case-09 | fail→fail | 8,034 | 2,370 | -71% | 1 | 1 | 0% | 1,325 | 4,777 | +261% | 0 | 0 | — |
case-10 | pass→pass | 11,336 | 3,267 | -71% | 1 | 1 | 0% | 1,679 | 4,949 | +195% | 0 | 0 | — |
case-11 | pass→pass | 6,574 | 3,107 | -53% | 1 | 1 | 0% | 942 | 4,855 | +415% | 0 | 0 | — |
case-12 | pass→pass | 8,985 | 3,552 | -60% | 1 | 1 | 0% | 1,679 | 4,986 | +197% | 0 | 0 | — |
case-13 | fail→pass | 10,280 | 2,220 | -78% | 1 | 1 | 0% | 1,671 | 4,700 | +181% | 0 | 0 | — |
case-14 | fail→pass | 12,096 | 2,713 | -78% | 1 | 1 | 0% | 1,752 | 4,798 | +174% | 0 | 0 | — |
case-15 | fail→pass | 10,160 | 2,414 | -76% | 1 | 1 | 0% | 1,587 | 4,752 | +199% | 0 | 0 | — |
case-16 | pass→pass | 6,372 | 3,009 | -53% | 1 | 1 | 0% | 975 | 4,837 | +396% | 0 | 0 | — |
case-17 | fail→pass | 9,616 | 7,126 | -26% | 1 | 1 | 0% | 1,598 | 5,376 | +236% | 0 | 0 | — |
case-18 | fail→fail | 10,705 | 2,243 | -79% | 1 | 1 | 0% | 1,586 | 4,664 | +194% | 0 | 0 | — |
case-19 | pass→pass | 8,737 | 10,082 | +15% | 1 | 1 | 0% | 686 | 5,433 | +692% | 0 | 0 | — |
case-20 | fail→fail | 5,730 | 5,904 | +3% | 1 | 1 | 0% | 834 | 5,063 | +507% | 0 | 0 | — |
case-21 | fail→fail | 16,381 | 15,395 | -6% | 1 | 1 | 0% | 3,130 | 6,864 | +119% | 0 | 0 | — |
case-22 | pass→pass | 3,213 | 1,711 | -47% | 1 | 1 | 0% | 464 | 4,612 | +894% | 0 | 0 | — |
case-23 | pass→pass | 4,982 | 2,196 | -56% | 1 | 1 | 0% | 894 | 4,756 | +432% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 19 counted toward the lift figure. The other 4 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +26 percentage points is the difference between those two pass rates over the 19 comparable cases. 3 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.