High severity
Functions that send Ether to arbitrary destinations
A function that sends ETH to an address it takes from the caller — or to a destination any caller can set — is a drain waiting to happen when nothing restricts who may call it or how much they may take. The textbook case is a withdraw that forwards ETH to an address argument without checking that the caller is entitled to the money. Because the destination and the amount are both attacker-chosen and the function is unprotected, anyone can call it and move the contract's whole balance to themselves. The same shape appears in “emergency” sweep functions, miswired fee collectors, and refund helpers that trust their arguments.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public balance;
function deposit() external payable {
balance[msg.sender] += msg.value;
}
// Sends ETH to a caller-supplied address, with no access control and no
// check that the caller is owed `amount`. Anyone can drain the contract
// to any address of their choosing.
function withdraw(address payable to, uint256 amount) external {
to.transfer(amount);
}
receive() external payable {}
}The fix
Pay only the caller, and only what the caller is owed. Drop the arbitrary destination, debit the caller's own tracked balance, and apply the effect before the external call so a reentrant recipient cannot re-enter with a stale balance. If a contract genuinely must send to an address chosen at the call site — an owner-run payout, say — then gate the function with access control (onlyOwner or a role) instead, so an arbitrary caller cannot reach it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public balance;
function deposit() external payable {
balance[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balance[msg.sender] >= amount, "insufficient");
balance[msg.sender] -= amount; // effect before interaction
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "transfer failed"); // pay only the caller
}
receive() external payable {}
}Scan your contract for this
pactlint flags arbitrary-send-eth and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base or Polygon with x402,
no signup; the first few scans each day are free.
Sending Ether to an arbitrary destination is detected by Slither's stock arbitrary-send-eth detector,
rated high impact at medium confidence, which pactlint surfaces at high severity — it keeps Slither's own
impact-and-confidence rating, with no extra downgrade or triage. Run it in CI with the
pactlint GitHub Action.
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.