Skip to content
Security
Skill

/external-call-safety

Detects unsafe external call patterns and token integration vulnerabilities in smart contracts. Covers unchecked call/delegatecall/staticcall return values, fee-on-transfer tokens, rebasing tokens, tokens with missing return values (USDT), ERC-777 callback risks, unsafe approve

BOOST
From plugin
quillshield-skills
12911 skills
Install
$ npx -y skills add quillai-network/quillshield_skills --skill external-call-safety --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/external-call-safety

Context preview

The summary Claude sees to decide when to auto-load this skill.

Detects unsafe external call patterns and token integration vulnerabilities in smart contracts. Covers unchecked call/delegatecall/staticcall return values, fee-on-transfer tokens, rebasing tokens, tokens with missing return values (USDT), ERC-777 callback risks, unsafe approve

SKILL.md

external-call-safety.SKILL.md
name: external-call-safety
description: Detects unsafe external call patterns and token integration vulnerabilities in smart contracts. Covers unchecked call/delegatecall/staticcall return values, fee-on-transfer tokens, rebasing tokens, tokens with missing return values (USDT), ERC-777 callback risks, unsafe approve race conditions, return data bombs, gas stipend limitations, and push vs pull payment patterns. Use when auditing contracts that interact with external contracts, integrate arbitrary ERC20 tokens, distribute payments, or make low-level calls.

External Call Safety

Detect vulnerabilities arising from **unsafe interactions with external contracts** and **non-standard token behaviors** that break protocol assumptions. Covers OWASP SC06 (Unchecked External Calls) plus the entire "weird ERC20" problem space.

When to Use

  • Auditing any contract that calls external contracts (token transfers, cross-contract interactions)
  • Reviewing protocols that support arbitrary/user-supplied ERC20 tokens
  • Analyzing ETH payment distribution logic (airdrops, reward distribution, refunds)
  • Verifying low-level call safety (`call`, `delegatecall`, `staticcall`)
  • When a protocol claims to support "any ERC20 token"

When NOT to Use

  • Reentrancy-specific analysis (use reentrancy-pattern-analysis — though there is overlap)
  • Oracle/price feed analysis (use oracle-flashloan-analysis)
  • Pure access control review (use semantic-guard-analysis)

Part 1: External Call Safety

Vulnerability Class 1: Unchecked Return Values

Low-level calls (`call`, `delegatecall`, `staticcall`) return a boolean indicating success. If unchecked, failed calls are silently ignored.

// VULNERABLE: Return value not checked
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    payable(msg.sender).call{value: amount}(""); // Can fail silently!
    // User's balance decreased but ETH not sent
}

// SAFE: Check return value
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Transfer failed");
}

**Detection Algorithm:**

For each low-level call expression:
  1. Is the return value captured? (bool success, bytes memory data) = ...
  2. Is the success boolean checked? require(success) or if(!success) revert
  3. If not captured or not checked → UNCHECKED RETURN VALUE

Severity:
  - ETH transfer unchecked → CRITICAL (funds lost)
  - Token operation unchecked → HIGH (state desync)
  - Non-financial call unchecked → MEDIUM

Vulnerability Class 2: Gas Stipend Limitations

// DANGEROUS: transfer() and send() forward only 2300 gas
payable(recipient).transfer(amount); // Reverts if recipient needs > 2300 gas
payable(recipient).send(amount);     // Returns false, often unchecked

// SAFE: Use call() with gas
(bool success, ) = payable(recipient).call{value: amount}("");
require(success, "Transfer failed");

**Why 2300 gas is dangerous:**

  • Contracts with `receive()` or `fallback()` that do more than emit an event will fail
  • EIP-1884 changed `SLOAD` gas cost, breaking some existing contracts
  • Multi-sig wallets and smart contract wallets often need more gas

Vulnerability Class 3: Return Data Bomb

A malicious contract can return extremely large data to consume the caller's gas.

// Vulnerable to return data bomb
(bool success, bytes memory data) = untrustedContract.call(calldata);
// If untrustedContract returns 1MB of data, copying it costs massive gas

// SAFE: Limit return data or ignore it
(bool success, ) = untrustedContract.call(calldata); // Ignore return data
// Or use assembly to limit return data size

Vulnerability Class 4: Delegatecall to Untrusted Contract

// CRITICAL: delegatecall executes untrusted code in OUR storage context
function execute(address target, bytes calldata data) external {
    target.delegatecall(data); // Untrusted code can overwrite ANY storage
}

// delegatecall should ONLY be used with trusted, immutable targets

Part 2: Token Integration Safety ("Weird ERC20" Tokens)

Issue 1: Fee-on-Transfer Tokens

Some tokens deduct a fee during `transfer()` and `transferFrom()`. The recipient receives less than the specified amount.

// VULNERABLE: Assumes received amount equals input amount
function deposit(uint256 amount) external {
    token.transferFrom(msg.sender, address(this), amount);
    balances[msg.sender] += amount; // Credits MORE than actually received!
}

// SAFE: Check actual balance change
function deposit(uint256 amount) external {
    uint256 balanceBefore = token.balanceOf(address(this));
    token.transferFrom(msg.sender, address(this), amount);
    uint256 balanceAfter = token.balanceOf(address(this));
    uint256 actualReceived = balanceAfter - balanceBefore;
    balances[msg.sender] += actualReceived; // Credits actual amount
}

**Known fee-on-transfer tokens:** STA, PAXG, USDT (fee currently 0 but can be activated), RFI/SAFEMOON forks.

Issue 2: Rebasing Tokens

Rebasing tokens change all balances proportionally without transfers. Protocol's accounting desynchronizes from actual balances.

// VULNERABLE: Stores absolute balance amounts
function deposit(uint256 amount) external {
    token.transferFrom(msg.sender, address(this), amount);
    userDeposit[msg.sender] = amount; // After rebase, actual balance differs!
}

// Mitigation options:
// 1. Store shares instead of amounts
// 2. Wrap rebasing token (wstETH pattern)
// 3. Explicitly state: "rebasing tokens not supported"

**Known rebasing tokens:** stETH, AMPL, OHM, YAM, BASED.

Issue 3: Missing Return Values

Some tokens don't return a boolean from `transfer()`/`transferFrom()`/`approve()`, breaking the ERC20 standard.

// VULNERABLE: Assumes return value exists
bool success = token.transfer(recipient, amount); // Reverts
Read more
Ships withquillshield-skills

AI agent skills for advanced smart contract security auditing. These skills teach AI agents (Claude, Cursor) the QuillShield methodology for detecting vulnerabilities that traditional static analysis tools miss.

Get the whole plugin

Other skills on quillshield-skills.