Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Work through an external auditor's GitHub issues for a contracts PR — discover the audit repo from Slack, load every finding, triage fix-vs-acknowledge in one gate, implement each fix as its own commit on a remediation branch, reply to each issue with "fixed <commit>" or "acknowledged <reason>", then post a Slack wrap-up nudging the auditor and pinging @smartcontract_core to re-review. Use when an audit is completed and the findings live as issues in the auditor's repo.
.claude/skills/lifinance-resolve-audit-issues/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 214% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 582% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 467% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 288% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 223% | 0% |
> Usage: /resolve-audit-issues <PR_NUMBER_OR_URL_OR_FACET> [--audit-repo <owner/repo>]
An external auditor (Sujith, Burrasec) delivers findings as GitHub issues in their own repo, one issue per finding, severity-labelled. This command drives the response end to end: discover that repo, read every finding, decide per issue whether to fix or acknowledge, implement fixes one-commit-each on a fresh remediation branch, reply to each issue with a commit link or a reason, and post a Slack wrap-up nudging the auditor + pinging the SC team.
Control model — one gate. The command analyses all issues and presents a single triage table. You approve/edit the whole plan once. Then it executes autonomously, pausing only for (a) genuinely ambiguous findings it flagged, and (b) the mandatory pre-post review in Step 8.
One commit per fix (LI.FI convention: each finding fixed or acknowledged in isolation). Acknowledged findings produce no commit.
audit/auditLog.json → that's /add-audit, run later oncethe auditor signs off on the fixes.
/multisig-rollout./request-audit.| Auditor | Channel | webhook --channel | env var | Auditor mention | Repo owner (typical) | |---|---|---|---|---|---| | Sujith Somraaj | #dev-sc-audit | dev-sc-audit | WEBHOOK_DEV_SC_AUDIT | <@U05GN6XH57T> | sujithsomraaj | | Burrasec (Josip Koncurat) | #dev-sc-audit-burrasec | dev-sc-audit-burrasec | WEBHOOK_DEV_SC_AUDIT_BURRASEC | <@U094M720QDP> | burrasec / firm org |
The owner column is a hint, not a rule — always confirm the discovered repo with the user before trusting it (Step 1). SC-team review tag: @smartcontract_core MUST be sent as <!subteam^S096X6MCB0C> — a plain @… does not notify.
The argument is a PR number, a lifinance/contracts PR URL, or a facet name.
bashgh pr view <PR> --repo lifinance/contracts \ --json number,title,body,url,headRefName,commits,files,labels,state
Capture: number, url, headRefName, the last commits[].oid (the audited head — the commit the auditor reviewed; re-run with --json commits if the array came back empty), the facet/contract names and versions from the title brackets (e.g. MayanFacet v2.0.0 → facet = MayanFacet, version = v2.0.0 — the version is needed for the Step 9 wrap-up), and a ref-safe slug of the primary facet (lowercase, non-[a-z0-9._-] → -) for the branch name in Step 4. Extract any EXSC-\d+ ticket from body / title / branch for later PR linkage.
If given a facet name with no PR, find the PR: gh pr list --repo lifinance/contracts --search "<Facet>" --state all. If ambiguous, list matches and ask.
If --audit-repo <owner/repo> was passed, use it and skip the Slack search — but still identify the auditor (and therefore the channel + mention needed for Step 9) from the repo owner via the Auditors table; if the owner doesn't map to a known auditor, ask the user which auditor/channel it is. Never leave the auditor unresolved just because discovery was skipped.
Otherwise search Slack for the auditor's repo link tied to this facet. The audit-completion message names the findings repo (e.g. github.com/<auditor>/lifi-<facet>-v<version>):
github.com/… link:slack_search_public_and_private scoped to #dev-sc-audit / #dev-sc-audit-burrasec with the facet name; look for the most recent message from the auditor containing a repo URL that is not lifinance/contracts.
auditor's "the audit is completed, findings here: <repo>" reply is the highest-signal source.
Parse owner/repo from the link. Present the match and confirm before trusting it:
textAudit repo: <owner>/<repo> (from <auditor> in #<channel>, <date>) Findings PR/commit audited: <pr_url>/commits/<audited_oid> Proceed with this repo? (yes / paste the correct owner/repo)
If Slack yields nothing, ask the user for the repo directly — don't guess.
The auditor's findings repo is private; you need collaborator access.
bashgh api /user/repository_invitations \ --jq '.[] | {id, repo: .repository.full_name}'
→ accept it automatically. This is pre-authorised because /request-audit already confirmed the auditor and their findings repo before the audit began; accepting here just materialises access the team already agreed to grant.
bash gh api -X PATCH /user/repository_invitations/<invitation_id>
Report ✅ Accepted invite to <owner>/<repo>.
auditor → do not accept it; surface it and ask the user before doing anything.
Load every open issue — do not cap. gh issue list --limit N silently truncates at N, so page through the REST API instead:
bashgh api --paginate repos/<owner>/<repo>/issues \ -f state=open --jq '.[] | select(.pull_request | not) | {number, title}'
(--paginate follows every page; the pull_request filter drops PRs, which the issues endpoint also returns.) If you do use gh issue list, set a limit well above the issue count and fail loudly if the returned count equals the limit — that means results were truncated.
For each issue, pull the full body (and any existing comments, so you don't re-answer a finding already resolved in a prior run):
bashgh issue view <n> --repo <owner>/<repo> --json number,title,body,labels,state,comments
Extract per issue: severity (from the Severity: … label), title, description, the exact code location(s) it references, and the auditor's suggested remediation if any. Skip issues that already carry a fixed/acknowledged reply from you unless the user asks to redo them.
Fixes land on a fresh remediation branch based off the audited PR's head (not the existing PR branch), so remediation history is isolated and gets its own PR.
Create a dedicated worktree off the audited branch so the diamond invariants and tests build cleanly and you never touch the main checkout:
Use the ref-safe <slug> from Step 0 (never the raw facet name) for both the worktree path and the branch, so spaces/slashes can't break branch creation:
bash# Create the worktree and the remediation branch in one step, based on the audited head: git worktree add ../<slug>-remediation <audited_oid_or_pr_head> -b audit/<slug>-remediation cd ../<slug>-remediation git submodule update --init --recursive bun install && bun typechain:incremental # avoid the fresh-worktree lint-staged trap ln -s <main-checkout>/.env .env # framework/deploy scripts read the shared .env
(If you have a personal worktree helper it's a convenience, not a requirement — the raw command above is the portable idiom and works on any clean checkout.)
Verify the base is right: git log <pr_head>..HEAD should be empty (you branched from the PR head, not from stale main). See the worktree memories for the submodule / typechain / prettier traps.
For every issue, map the finding to the code, form a recommendation, and assess residual risk. Follow the Solidity rules that apply to the touched files (100-solidity-basics, 101-solidity-contracts, 102-facets, 104-receiver-contracts, 105-security) — a fix must not weaken diamond/selector/storage invariants or governance flows.
Present one table, most-severe first. It must be self-contained — the reader should understand each finding and our response without opening the issue. Keep a Finding column (a neutral 1–2 sentence summary of what the auditor actually reported — the observation + the affected code/path, in your words, not a copy of the title) separate from Rationale (our decision reasoning + residual risk):
text| # | Sev | Title | Finding (what the auditor reported) | Proposal | Rationale / residual risk | |---|------|--------------------------------------|----------------------------------------------------------------------|-------------|-----------------------------------------| | 3 | Med | Native swap calldata reused … | `swapAndForwardEth` forwards the original `swapData` built for the | Fix | Bind realized amount into calldata; … | | | | | quoted amount, so a different realized input reverts or strands … | | | | 2 | Med | Mayan refund recipient not validated | Facet validates the destination receiver but never the order's own | Acknowledge | Forwarder is trusted+immutable; … | | | | | refund identity (`trader`/`refundAddr`), so refunds can go elsewhere | | | |
The Finding column is a faithful summary of the auditor's issue body, not an assessment — never soften or pre-judge it there; your judgement belongs in Rationale. Keep it tight but complete enough that the fix/acknowledge decision is legible on its own.
For each Acknowledge, the rationale must be a real reason the finding is a non-issue or an accepted trade-off (trust boundary, gas cost, out-of-scope by design) — never "won't fix" with no basis. For anything you cannot confidently classify, add it to a short Questions for you list below the table rather than guessing.
Then stop:
textApprove this plan? Reply: • "go" to execute as-is • edits, e.g. "#7 fix not acknowledge", "#5 acknowledge because <reason>" • answers to the questions above
Wait for approval. Re-render the table if edited, until the user says go.
Process issues independently. For each Fix:
src/…. Reuse existing libraries/helpers; keep facets thin.bash forge build forge test --match-contract <Facet>Test # or the closest matching suite
(Reproduce under the deploy toolchain if the finding touches a stack-depth-sensitive path — see the london/0.8.17 memory.)
text fix(<Facet>): <short finding title> (audit <owner>/<repo>#<n>)
Record the full commit SHA for the reply. The pre-commit hook runs Solhint + Prettier (lint-staged auto-fixes formatting into the commit); if it reports a Solhint error, fix the code — never bypass the hook with --no-verify.
For each Acknowledge: no code, no commit — just carry the approved rationale to Step 8.
If a fix turns out larger/riskier than triage assumed, stop and re-surface that one issue rather than ballooning the diff.
One publish gate for everything external. Before anything leaves the machine, render together in a single review: the full diff set (all fix commits), the drafted reply for every issue (Step 8), and the drafted Slack wrap-up (Step 9). The user gives one approval that covers all external posts — there is no second gate later. This is a content check; the triage gate already approved the plan.
On go:
.github/workflows/** — see the workflow-scope memory)..github/pull_request_template.md, linking the EXSC-\d+ticket. Title e.g. fix(<Facet>): address <auditor> audit findings. Open as draft if the change trips the audit/version CI gate (it will — struct/selector/version churn) so the audit-verification workflow skips until the auditor re-signs. Leave the reviewer checklist unchecked.
https://github.com/lifinance/contracts/commit/<full_sha>
Posting happens only after the single Step 7 approval. Then, per issue, and tracking the result of each (you'll need it to gate Step 9):
bash# fixed gh issue comment <n> --repo <owner>/<repo> \ --body-file /tmp/reply-<n>.md # body: "fixed https://github.com/lifinance/contracts/commit/<full_sha>" # acknowledged gh issue comment <n> --repo <owner>/<repo> \ --body-file /tmp/reply-<n>.md # body: "acknowledged — <reason>"
Reply body format (exact wording the team uses):
fixed <commit_permalink> — optionally one sentence on what changed.acknowledged — <reason> — the approved rationale from triage.Leave issues open — the auditor closes each one on verification. Do not close them yourself unless the user says so.
If any gh issue comment failed, keep that issue in a pending set — it gates Step 9.
Gate: only run this if every issue reply from Step 8 succeeded. If any reply is still pending, do not post the wrap-up — it would claim "all issues are addressed" while some never got a reply. Instead report which issues failed and stop; the user re-runs after those replies land.
When all replies are confirmed, post one message (already approved in Step 7) to the auditor's audit channel: nudge the auditor that all findings are addressed, and ping @smartcontract_core to review the findings and our responses. Webhooks can't thread, so this is one self-contained channel message (not a thread reply).
Before writing the message file, validate that no {...} placeholder is unresolved — {Facet}, {version}, {auditor_mention}, {webhook_channel}, the counts, and {remediation_pr_url} must all have real values (from Step 0 and the auditor identified in Step 1). If any is missing, stop and ask rather than posting a half-filled message.
Draft (backtick code style per the auditor-facing convention — wrap contract/version/PR refs):
textAudit remediation for `{Facet}` `{version}` is ready for re-review. Findings: {N} total — {fixed_count} fixed, {ack_count} acknowledged. Remediation PR: {remediation_pr_url} Hey {auditor_mention} — all issues are addressed; please take another look when you get a chance 🙏 <!subteam^S096X6MCB0C> please review the audit findings and our responses.
Resolve {auditor_mention} and the channel from the Auditors table (the auditor identified in Step 1). Write the message to a temp file and post:
bashbunx tsx script/utils/send-slack-webhook-message.ts \ --channel {webhook_channel} \ --message-file /tmp/audit-wrapup-<pr>.txt
Interpret the exit code (same convention as request-audit):
| Exit | Meaning | Action | |---|---|---| | 0 | posted | ✅ Posted wrap-up to #{channel} | | 2 | WEBHOOK_* env var not set | Print the drafted message and tell the user to post it manually; name the missing env var (URL in 1Password → Developers Smart Contract → Webhooks SC Channels) | | 1 | Slack / network error | Report stderr, do not retry |
Then report the final summary. The Slack line must reflect the actual webhook result — not a fixed string: posted to #<channel> (exit 0), manual fallback — post it yourself (exit 2), or failed — <error> (exit 1):
textRemediation PR: <new_pr_url> (draft) Replies posted to <owner>/<repo>: fixed: #1 #3 #8 … (N commits) acknowledged: #2 #6 … (M) Slack: posted to #<channel> — nudged <auditor> + pinged @smartcontract_core Awaiting auditor re-review.
| Situation | Action | |---|---| | Audit repo not found in Slack | Ask the user for owner/repo; don't guess | | No repo access and no pending invite | Ask the auditor to add you; stop | | Issue references code not on the audited head | Flag in triage; the finding may target a different commit — confirm the audited commit with the user | | A fix breaks tests | Keep the issue in "Fix" but stop and surface it; don't commit a red fix | | gh issue comment fails for one issue | Report it, continue with the rest; the others already posted |
audit gate isolated from the original PR, and gives the auditor a clean PR to re-review.
judgement on fix-vs-acknowledge, but re-approving each mechanical fix is friction — so the judgement is front-loaded into one table and execution runs unattended.
action; the pre-post review is non-negotiable regardless of the triage approval.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | fail→fail | 11,522 | 4,789 | -58% | 1 | 1 | 0% | 2,004 | 5,309 | +165% | 0 | 0 | — |
case-01 | fail→pass | 9,969 | 6,391 | -36% | 1 | 1 | 0% | 1,669 | 5,238 | +214% | 0 | 0 | — |
case-02 | fail→pass | 5,421 | 3,915 | -28% | 1 | 1 | 0% | 778 | 5,308 | +582% | 0 | 0 | — |
case-03 | fail→pass | 6,390 | 4,772 | -25% | 1 | 1 | 0% | 963 | 5,461 | +467% | 0 | 0 | — |
case-04 | fail→fail | 7,986 | 5,124 | -36% | 1 | 1 | 0% | 1,151 | 4,892 | +325% | 0 | 0 | — |
case-05 | fail→fail | 5,571 | 5,787 | +4% | 1 | 1 | 0% | 277 | 4,927 | +1679% | 0 | 0 | — |
case-06 | fail→fail | 18,852 | 4,773 | -75% | 1 | 1 | 0% | 2,421 | 4,906 | +103% | 0 | 0 | — |
case-07 | fail→fail | 7,444 | 2,968 | -60% | 1 | 1 | 0% | 1,022 | 5,172 | +406% | 0 | 0 | — |
case-08 | fail→pass | 7,969 | 2,716 | -66% | 1 | 1 | 0% | 1,308 | 5,069 | +288% | 0 | 0 | — |
case-09 | pass→pass | 12,193 | 3,707 | -70% | 1 | 1 | 0% | 1,931 | 5,294 | +174% | 0 | 0 | — |
case-10 | fail→pass | 9,995 | 5,682 | -43% | 1 | 1 | 0% | 1,670 | 5,390 | +223% | 0 | 0 | — |
case-11 | fail→pass | 12,470 | 3,410 | -73% | 1 | 1 | 0% | 1,916 | 5,255 | +174% | 0 | 0 | — |
case-12 | pass→fail | 13,583 | 4,775 | -65% | 1 | 1 | 0% | 2,275 | 5,359 | +136% | 0 | 0 | — |
case-13 | fail→pass | 10,766 | 2,686 | -75% | 1 | 1 | 0% | 1,584 | 5,101 | +222% | 0 | 0 | — |
case-14 | fail→pass | 7,625 | 3,089 | -59% | 1 | 1 | 0% | 1,155 | 5,153 | +346% | 0 | 0 | — |
case-15 | fail→pass | 14,525 | 2,729 | -81% | 1 | 1 | 0% | 2,250 | 5,027 | +123% | 0 | 0 | — |
case-16 | fail→pass | 7,597 | 2,484 | -67% | 1 | 1 | 0% | 1,139 | 4,971 | +336% | 0 | 0 | — |
case-17 | fail→pass | 8,746 | 3,718 | -57% | 1 | 1 | 0% | 1,376 | 5,242 | +281% | 0 | 0 | — |
case-18 | fail→pass | 10,798 | 4,733 | -56% | 1 | 1 | 0% | 1,788 | 5,433 | +204% | 0 | 0 | — |
case-20 | pass→pass | 14,730 | 5,065 | -66% | 1 | 1 | 0% | 2,446 | 5,596 | +129% | 0 | 0 | — |
case-21 | fail→fail | 9,623 | 3,803 | -60% | 1 | 1 | 0% | 1,501 | 5,219 | +248% | 0 | 0 | — |
case-22 | fail→pass | 6,995 | 2,931 | -58% | 1 | 1 | 0% | 1,134 | 5,161 | +355% | 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. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 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 +55 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.