Agent skill
sep41-token-safety
Trigger Pattern SEP-41 token patterns detected (approve/transfer/transfer_from/allowance/balance) - Inject Into Breadth agents, depth-token-flow, depth-edge-case
Install this agent skill to your Project
npx add-skill https://github.com/PlamenTSV/plamen/tree/main/agents/skills/soroban/sep41-token-safety
SKILL.md
SEP41_TOKEN_SAFETY Skill (Soroban)
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.
1. Approve Race Condition
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:
- Owner approves spender for 100 tokens
- Owner submits new approval for 50 tokens
- Spender front-runs the new approval and spends the 100-token allowance
- New 50-token approval is set
- Spender spends 50 more tokens — net: 150 tokens spent, 50 intended
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:
- Does
approveallow setting non-zero over non-zero without an intermediate zero-set step? - Does the protocol documentation warn users about the race condition?
- Are there any off-chain tools (like
increaseAllowance/decreaseAllowanceequivalents) to safely adjust allowances?
2. Allowance Expiry
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:
- Does the calling contract handle the case where a previously valid allowance has expired and
transfer_fromnow fails? - Is
expiration_ledgervalidated to be in the future whenapproveis called? Settingexpiration_ledgerin the past silently creates an already-expired allowance - For protocol-controlled allowances (e.g., a contract that approves tokens on behalf of users): does the contract re-approve if the allowance has expired?
Edge 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.
3. Transfer Auth Propagation
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:
- A contract calling
token.transfer(user, contract, amount)on behalf of a user — does it calluser.require_auth()first, or does it rely on the token contract to enforce it? - Sub-invocation auth propagation: if a function calls
token.transferinside ainvoke_contractchain, is theAuthorizedInvocationtree constructed to include the token transfer? transfer_fromwhere the spender is the calling contract itself (valid for vault patterns) — is the allowance actually set, or does the contract assume it already is?
4. SAC Interaction
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:
- Trust line authorization: A classic Stellar account can have its trust line for an asset revoked by the issuer. After revocation,
transferto that account fails silently or panics. Contracts that assume all transfers succeed need to handle this. - Clawback: Some Stellar assets have clawback enabled. An issuer can call
clawbackto reduce a Soroban contract's balance without any Soroban transaction. This means a contract'sbalance()can decrease between two Soroban transactions with no on-Soroban event. - Balance source of truth: SAC reads balances from the underlying Stellar ledger state. If a classic operation changes the balance, the Soroban contract sees the new balance immediately — this can break invariants if the contract caches balances.
- Token address permanence: On Stellar, classic assets can be re-issued or migrated, changing their effective on-chain identity. Contracts that derive storage keys, registry lookups, or deployment salts from token addresses may become unreachable if a token's address changes. Verify whether token address references can be updated or if the protocol documents this as a known limitation.
5. Balance Verification
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:
- SAC-wrapped assets with clawback can reduce effective received amount
- Custom token implementations may deviate from the standard
- Protocols that accept arbitrary SEP-41 tokens should always read balance before and after transfer
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
6. Burn Authorization
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:
burnwithoutrequire_authonfrom→ anyone can burn any user's tokensburn_fromwithout allowance check → bypasses the approval mechanism- Protocol contracts that call
burnto destroy tokens: verify they callfrom.require_auth()or thatfromis the calling contract itself (self-burn is always permitted) burncombined with minting: verify the net supply change is intentional and correctly accounted
Finding Template
**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]
Step Execution Checklist (MANDATORY)
| 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 |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
integration-hazard-research
Protocol Type Trigger NAMED_EXTERNAL_PROTOCOL (detected when recon finds import/interface for an identifiable external protocol — not standard libraries). Researches known integration hazards of the target protocol.
outcome-determinism
Protocol Type Trigger outcome_determinism - detected when EITHER of these code patterns are present - - Selection from finite depletable pool with fallback behavior (while(full)...
governance-attack-vectors
Protocol Type Trigger governance (detected when Governor, Timelock, voting, proposal, quorum, delegate patterns found) - Inject Into Breadth agents, depth-external, depth-edge-case
vault-accounting
Protocol Type Trigger vault (detected in recon TASK 0 Step 1) - Inject Into Core state agent OR economic design agent (merge via M4 hierarchy)
lending-protocol-security
Protocol Type Trigger lending (detected when recon finds liquidate|borrow|repay|collateral|lend|loan|LTV|healthFactor|interestRate|debtToken) - Inject Into Breadth agents, depth...
dex-integration-security
Protocol Type Trigger dex_integration (detected when recon finds swap|addLiquidity|removeLiquidity|IUniswapV2Router|ISwapRouter|amountOutMin|amountOutMinimum|slippage - AND the...
Didn't find tool you were looking for?