पाठ 17 / 25

Common Vulnerabilities

Recognise access-control bugs, front-running, signature replay and other common flaws.

A catalogue of costly mistakes

Beyond reentrancy, common vulnerability classes include: missing or broken access control (an unprotected initialize, mint or setOwner function); using tx.origin for authorisation (a malicious contract the user calls can act on their behalf; use msg.sender); front-running and MEV, where pending transactions in the public mempool are seen and exploited by others who pay higher fees, for example sandwiching swaps (use slippage limits, deadlines, commit-reveal or private transaction relays); oracle and price manipulation with flash loans; signature replay (missing nonces, chain ids or contract addresses in signed data; use EIP-712 and track used nonces); precision loss from dividing before multiplying, and rounding in the user's favour in vault share calculations, including the first-depositor inflation attack on ERC-4626 vaults; unchecked return values of low-level calls and non-standard tokens; denial of service through unbounded loops or a reverting recipient in a push-payment loop; uninitialised proxies and storage collisions; and logic errors in business rules, the most common cause of losses. The OWASP Smart Contract Top 10 and the SWC registry catalogue many of these.

tx.origin phishing and replay-safe signatures

Authorise with msg.sender; bind signatures to a nonce, deadline and domain.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract Wallet {
    address public owner = msg.sender;

    // VULNERABLE: if the owner calls a malicious contract, it can call this and pass the check
    function transferBad(address payable to, uint256 amount) external {
        require(tx.origin == owner, "not owner");
        to.transfer(amount);
    }

    // SAFE: the immediate caller must be the owner
    function transferGood(address payable to, uint256 amount) external {
        require(msg.sender == owner, "not owner");
        (bool ok, ) = to.call{value: amount}("");
        require(ok, "transfer failed");
    }
}

contract Vouchers is EIP712("Vouchers", "1") {
    bytes32 private constant CLAIM_TYPEHASH = keccak256("Claim(address to,uint256 amount,uint256 nonce,uint256 deadline)");
    address public immutable signer;
    mapping(address => uint256) public nonces;
    mapping(address => uint256) public credits;

    constructor(address signer_) { signer = signer_; }

    function claim(uint256 amount, uint256 deadline, bytes calldata sig) external {
        require(block.timestamp <= deadline, "expired");
        bytes32 structHash = keccak256(abi.encode(CLAIM_TYPEHASH, msg.sender, amount, nonces[msg.sender]++, deadline));
        bytes32 digest = _hashTypedDataV4(structHash);          // includes chain id and this contract's address
        require(ECDSA.recover(digest, sig) == signer, "bad signature");
        credits[msg.sender] += amount;
    }
}

A forged signature on a cheque

Signature replay is like a shop accepting a photocopy of a signed cheque again and again. Nonces number each cheque so it can be cashed once, deadlines make it expire, and the domain (chain and contract) names the bank it is valid at.

त्वरित जाँच: Why is using tx.origin for authorisation dangerous?

  • It costs more gas
  • A malicious contract called by the user can pass a tx.origin check and act on the user's behalf
  • tx.origin is always zero
  • It only works on testnets
Answer

A malicious contract called by the user can pass a tx.origin check and act on the user's behalf — tx.origin is the original EOA, even when an intermediate malicious contract makes the call.