High severity
Arbitrary from in transferFrom
Updated 2026-10-12
ERC-20’s transferFrom(from, to, amount) moves tokens out of the from address, drawing on an allowance that from granted earlier with approve. The allowance is what makes it safe: a contract can only pull tokens a wallet has already permitted it to spend. The bug is passing a caller-supplied address as from. When the contract lets whoever calls the function choose whose tokens to pull, the allowance protects the contract instead of the user. Everyone who has ever approved the contract — a normal thing to do before a deposit, a swap, or a stake — has left a standing permission, and now any stranger can call the function with that victim’s address in from and their own address in to, draining the approved amount. This is not theoretical: it is one of the most common ways live DeFi contracts have lost user funds, because the broken function usually looks like an ordinary helper and the approvals it abuses are invisible in the contract’s own code.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
contract Vault {
IERC20 public token;
constructor(IERC20 token_) { token = token_; }
// `from` is whatever the caller passes in. Anyone who has approved
// this Vault can have their tokens pulled by a stranger who simply
// names them as `from` and names themselves as the recipient.
function deposit(address from, uint256 amount) external {
token.transferFrom(from, msg.sender, amount);
}
}The fix
Pull tokens only from msg.sender. The caller can authorise spending their own balance, never anyone else’s, so the allowance goes back to protecting the user. Drop from from the signature entirely rather than leaving it as a parameter a caller might set to someone else’s address, and credit the deposit to the sender. If a contract genuinely needs to move tokens on a third party’s behalf — a router or a relayer — it must prove that party consented in this same transaction, by taking their EIP-2612 permit signature or an explicit, access-controlled authorisation, not by trusting the from argument. Use OpenZeppelin’s SafeERC20.safeTransferFrom so a token that returns false instead of reverting cannot slip a failed pull through.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract Vault {
using SafeERC20 for IERC20;
IERC20 public immutable token;
mapping(address => uint256) public balanceOf;
constructor(IERC20 token_) { token = token_; }
// Pulls only from the caller's own balance.
function deposit(uint256 amount) external {
token.safeTransferFrom(msg.sender, address(this), amount);
balanceOf[msg.sender] += amount;
}
}A note on scope
The detector flags a transferFrom(address,address,uint256) (or a library safeTransferFrom) whose from argument does not trace back, by data dependency, to msg.sender or address(this). If the token call sits next to a permit(…) on the same token, Slither reports it under the separate arbitrary-send-erc20-permit detector at medium confidence instead, because the permit may be the caller’s own; a plain arbitrary from with no permit is arbitrary-send-erc20 at high confidence. The analysis is per function and follows data dependencies within the contract, so an allowance checked off-chain, a from that is set through assembly, or consent enforced by a mechanism the analyser cannot read as a dependency on msg.sender can all affect whether a given call is a true finding. That is why pactlint runs extra triage on this class, described below, rather than flagging every transferFrom with a non-sender from.
Scan your contract for this
pactlint flags arbitrary-send-erc20 and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon or Solana with x402,
no signup; the first few scans each day are free.
An arbitrary from in transferFrom is detected by Slither’s stock
arbitrary-send-erc20 detector, rated high impact at high confidence, which pactlint maps to high severity
(its policy layer neither suppresses nor downgrades this detector). pactlint then runs worker-side triage before
reporting it: it drops the finding to info when the function is reachable only after a signature, an
allowance from that same address, an access-control check or a one-shot initializer, or when the from
address in fact traces to msg.sender or the contract itself at every call site; it lowers it to
medium when the address is pulled from a party that was first called back and had to agree in the same
transaction (a flash-loan-style callback), or when the call lives in an internal helper whose exposure depends on
callers the stock detector does not follow. A genuine public entry point that lets the caller choose from
stays High. Run it in CI with the
pactlint GitHub Action.
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.