agents/skills/soroban/sep41-token-safety/SKILL.md
Trigger Pattern SEP-41 token patterns detected (approve/transfer/transfer_from/allowance/balance) - Inject Into Breadth agents, depth-token-flow, depth-edge-case
npx skillsauth add plamentsv/plamen sep41-token-safetyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Trigger Pattern: SEP-41 token patterns detected (
approve/transfer/transfer_from/allowance/balance) Inject Into: Breadth agents, depth-token-flow, depth-edge-case Finding prefix:[ST-N]Rules referenced: R4, R5, R10, R11, R13, R15
SEP-41 is Soroban's token interface standard, analogous to ERC-20. It introduces Soroban-specific behaviors that differ from ERC-20: allowances use Temporary storage with expiration_ledger, the approve function can be front-run similarly to ERC-20, and the Stellar Asset Contract (SAC) bridges Stellar classic assets into Soroban with additional accounting complexity.
The approve(from, spender, amount, expiration_ledger) function overwrites the current allowance without checking the existing value. This is the ERC-20 approve race condition, present identically in SEP-41:
| Token Contract | approve Guarded? | Guard Type | Effective? |
|---------------|-------------------|-----------|-----------|
| {contract} | YES/NO | require!(current == 0) / none | YES/NO |
Attack sequence:
Soroban-specific note: Soroban does not have a traditional mempool — transaction ordering is determined by validators, not fee-based priority. However, multi-operation transactions and DEX routing can create ordering dependencies. The race condition is lower likelihood than on EVM but the vulnerability exists.
Check for:
approve allow setting non-zero over non-zero without an intermediate zero-set step?increaseAllowance / decreaseAllowance equivalents) to safely adjust allowances?SEP-41 allowances include an expiration_ledger parameter stored alongside the allowance amount. When the ledger passes expiration_ledger, the allowance is treated as zero. Verify expiry is handled correctly:
| Contract | Allowance Expiry Checked Before Use? | Expired Allowance Returns 0 or Panics? | Protocol Communicates Expiry to Users? |
|---------|--------------------------------------|---------------------------------------|---------------------------------------|
| {contract} | YES/NO | {behavior} | YES/NO |
Allowance storage: SEP-41 standard stores allowances in Temporary storage with TTL linked to expiration_ledger. When expiration_ledger passes, the Temporary entry may be pruned, and the allowance() call returns zero automatically.
Checks:
transfer_from now fails?expiration_ledger validated to be in the future when approve is called? Setting expiration_ledger in the past silently creates an already-expired allowanceEdge case: expiration_ledger = 0 behavior — verify whether the token contract treats 0 as "no expiry" or "expired at ledger 0" (already expired). Inconsistency here could cause silent allowance failures.
SEP-41 transfer(from, to, amount) requires from.require_auth(). transfer_from(spender, from, to, amount) requires spender.require_auth() and checks the allowance. Trace auth through the contract's transfer chains:
| Calling Function | Transfer Type | Auth Address | require_auth Called? | Correct Subject? |
|-----------------|--------------|-------------|----------------------|-----------------|
| {fn} | transfer / transfer_from | {from or spender} | YES/NO | YES/NO |
Patterns to check:
token.transfer(user, contract, amount) on behalf of a user — does it call user.require_auth() first, or does it rely on the token contract to enforce it?token.transfer inside a invoke_contract chain, is the AuthorizedInvocation tree constructed to include the token transfer?transfer_from where the spender is the calling contract itself (valid for vault patterns) — is the allowance actually set, or does the contract assume it already is?The Stellar Asset Contract (SAC) wraps Stellar classic XLM and issued assets as SEP-41 tokens. Contracts that interact with SAC face unique considerations:
| Concern | Addressed? | Evidence |
|---------|-----------|----------|
| SAC balance includes trust line state (frozen/unauthorized) | YES/NO | {fn:line or NONE} |
| Classic Stellar operations affecting SAC balance not reflected in Soroban state | YES/NO | {fn:line or NONE} |
| Clawback feature of issued assets handled | YES/NO | {fn:line or NONE} |
| Issuer authorization revocation handled | YES/NO | {fn:line or NONE} |
| Token address permanence (classic re-issuance / migration) | YES/NO | {fn:line or NONE} |
SAC-specific risks:
transfer to that account fails silently or panics. Contracts that assume all transfers succeed need to handle this.clawback to reduce a Soroban contract's balance without any Soroban transaction. This means a contract's balance() can decrease between two Soroban transactions with no on-Soroban event.When a contract accepts token deposits, it should verify the actual balance received rather than trusting the amount parameter:
| Function | Reads Balance After Transfer? | Trusts Amount Parameter? | Fee-on-Transfer Risk? |
|----------|------------------------------|--------------------------|----------------------|
| {fn} | YES/NO | YES/NO | YES/NO |
Note: Standard Soroban tokens (non-SAC) do not support fee-on-transfer. However:
Safe pattern:
let balance_before = token.balance(env.current_contract_address());
token.transfer_from(&spender, &from, &env.current_contract_address(), &amount);
let balance_after = token.balance(env.current_contract_address());
let received = balance_after - balance_before;
// Use `received` not `amount` for accounting
burn(from, amount) and burn_from(spender, from, amount) must enforce proper authorization:
| Function | Auth Check | Auth Subject | Correct? |
|----------|-----------|-------------|---------|
| burn call at {file:line} | from.require_auth() | from | YES/NO |
| burn_from call at {file:line} | spender.require_auth() + allowance check | spender | YES/NO |
Checks:
burn without require_auth on from → anyone can burn any user's tokensburn_from without allowance check → bypasses the approval mechanismburn to destroy tokens: verify they call from.require_auth() or that from is the calling contract itself (self-burn is always permitted)burn combined with minting: verify the net supply change is intentional and correctly accounted**ID**: [ST-N]
**Severity**: [Critical if token theft, High if allowance bypass or burn exploit, Medium if expiry/SAC risk, Low if approve race]
**Step Execution**: ✓1,2,3,4,5,6 | ✗(reasons) | ?(uncertain)
**Rules Applied**: [R4:✓/✗, R5:✓/✗, R10:✓/✗, R11:✓/✗, R13:✓/✗, R15:✓/✗]
**Location**: src/{contract}.rs:LineN
**Title**: {Approve race / Expired allowance / Auth bypass / SAC risk / Burn exploit} in `{fn_name}`
**Description**: [Specific SEP-41 interaction flaw with code reference]
**Impact**: [Token theft / silent transfer failure / incorrect accounting / unauthorized burn]
| Section | Required | Completed? | Notes |
|---------|----------|------------|-------|
| 1. Approve Race Condition | YES | ✓/✗/? | All approve call sites |
| 2. Allowance Expiry | YES | ✓/✗/? | All transfer_from paths and approve expiration_ledger handling |
| 3. Transfer Auth Propagation | YES | ✓/✗/? | All transfer and transfer_from invocations |
| 4. SAC Interaction | IF SAC tokens are involved | ✓/✗(N/A)/? | Trust line, clawback, balance caching |
| 5. Balance Verification | IF contract accepts deposits | ✓/✗(N/A)/? | Balance-before vs balance-after pattern |
| 6. Burn Authorization | IF burn or burn_from present | ✓/✗(N/A)/? | Auth on all burn call sites |
development
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project documentation checks. Generates a scored Audit Readiness Report and optionally runs static analysis. Trigger on: "prepare for audit", "audit readiness", "pre-audit check", "audit prep", "NatSpec check", or any request to review a Solidity codebase before a security review.
development
Launch the Plamen deterministic Web3 security audit pipeline
development
Run the Plamen smart-contract audit wizard in Codex
testing
Launch the Plamen deterministic L1 infrastructure audit pipeline