Free · No signup · Runs in your browser

Is this TRC-20 contract safe? Paste the address, find out in seconds.

No install, no code to read. This checks any TRON contract — a presale, a token, anything — against 9 structural red flags in plain language, live against the real on-chain ABI.

Try it on AuditX's own AUDX presale contract, or any TRC-20 sale/token address you're considering.

This isn't a generic "best practices" list. It's the exact set of checks that come from actually designing and shipping a presale contract honestly — each one maps to a specific, real way a presale contract can be built to work against the buyer instead of for them. AuditX's own AUDX presale contract is used below as a live, checkable example of what "passes" looks like.

Why the ABI matters more than the marketing

Every TRC-20/ERC-20-style contract exposes an ABI — a JSON list of every function it has. Unlike a website's claims, the ABI can't lie: if a function exists to move funds somewhere, it's in there. The fastest due-diligence step for any presale is reading the ABI before reading anything the team wrote about themselves.

The 8 checks

1. Withdraw destination — parameterized or fixed?

Look for a function like withdrawUsdt(uint256 amount) vs withdrawUsdt(uint256 amount, address to). If the destination is a function parameter, the owner can send collected funds anywhere, any time — including a fresh wallet you've never seen. If it's fixed at deployment, the owner is structurally limited to one address; check what that address actually is.

2. Unsold-token reclaim destination — same question.

Whatever function returns unsold tokens after the sale closes — a parameterized destination here is just as much of a red flag as on the withdraw function.

3. Can the price change after deployment?

Search the ABI for setPrice(), updateRate(), or configure(). A presale's entire value proposition is a fixed price — if the owner can change it after you've committed to buy, the "fixed price" claim is misleading.

4. Can the sale cap or pool size change?

setCap(), increaseSupply(), extendPool() — a pool that can grow after launch dilutes everyone who already bought in, with no way to know it happened until they check.

5. Who holds ownership, and can it be transferred?

transferOwnership() / renounceOwnership(). Ownership isn't automatically bad — a presale needs one to close the sale and withdraw proceeds. What matters is whether it's transferable to a new, unknown address at any time.

6. Can the owner pause or blacklist during an active sale?

pause(), blacklist(), setTradingEnabled() — mechanisms for selectively freezing specific buyers or the whole contract mid-sale.

7. Who can close the sale, and is there any refund path?

Check for a close() function and who can call it — should be owner-only. Then check honestly whether there's ANY refund mechanism if the sale closes early with money already collected. Most don't; that's normal, but it means reading the docs before sending money matters more, not less.

8. Is the contract even verified?

If you can't get a real ABI — source never published/verified on a block explorer — that's not a technical check, that's the whole due-diligence process failing before it starts. Treat an unverified sale contract as maximum risk regardless of what anyone tells you.

The script

Pulls a verified ABI straight from TronScan's public API (no key required) and flags any of the patterns above automatically. If the contract isn't verified, that failure IS the finding — the script says so instead of guessing.

#!/usr/bin/env python3
"""
presale_audit.py — TRC-20 presale contract red-flag checker.

Usage:
  python3 presale_audit.py <contract_address> [--abi path/to/abi.json]

If --abi is omitted, the script pulls the contract's ABI live from the chain
via TronGrid. On TRON, a contract's ABI is written on-chain at deployment
time — unlike Ethereum, no separate "source verification" step is needed to
expose it. If a contract has no ABI on-chain at all, that failure IS the
finding — the script says so instead of guessing.

No API key required for the endpoints this script uses.
"""
import sys
import json
import argparse
import urllib.request

TRONSCAN_CONTRACT_API = "https://apilist.tronscanapi.com/api/contract"
TRONGRID_GETCONTRACT_API = "https://api.trongrid.io/wallet/getcontract"

RISKY_PATTERNS = [
    ("withdraw", "to", "Withdraw function takes a destination address as a parameter — "
                        "funds are NOT limited to one fixed wallet."),
    ("reclaim", "to", "Unsold-token reclaim takes a destination address as a parameter — "
                       "same risk as an unlocked withdraw."),
    ("setprice", None, "Price can be changed after deployment — the 'fixed price' claim may not hold."),
    ("updaterate", None, "Rate/price appears changeable after deployment."),
    ("setcap", None, "Sale cap/pool size appears changeable after deployment."),
    ("increasesupply", None, "Contract can increase its own token supply after deployment."),
    ("pause", None, "Owner can pause the contract — funds/withdrawals could be frozen selectively."),
    ("blacklist", None, "Owner can blacklist addresses — could block specific buyers' refunds."),
    ("mint", None, "Contract (or its underlying token) has a mint function — supply is not fixed."),
]

_B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


def _base58_decode(s):
    num = 0
    for char in s:
        num = num * 58 + _B58_ALPHABET.index(char)
    n_bytes = (num.bit_length() + 7) // 8
    combined = num.to_bytes(n_bytes, "big") if n_bytes else b""
    n_pad = len(s) - len(s.lstrip("1"))
    return b"\x00" * n_pad + combined


def tron_address_to_hex(address):
    """T... base58check address -> 21-byte hex (0x41 prefix + 20-byte address)."""
    raw = _base58_decode(address)
    return raw[:21].hex()


def fetch_contract_info(address):
    """Cosmetic metadata only (creation date, balance) — best-effort, not load-bearing."""
    url = f"{TRONSCAN_CONTRACT_API}?contract={address}"
    try:
        with urllib.request.urlopen(url, timeout=15) as r:
            data = json.load(r)
    except Exception:
        return None
    entries = data.get("data", [])
    return entries[0] if entries else None


def fetch_contract_abi(address):
    """The real ABI, straight from the chain via TronGrid's wallet/getcontract."""
    hex_addr = tron_address_to_hex(address)
    body = json.dumps({"value": hex_addr}).encode()
    req = urllib.request.Request(
        TRONGRID_GETCONTRACT_API, data=body,
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as r:
            data = json.load(r)
    except Exception:
        return None
    return data.get("abi", {}).get("entrys", [])


def load_abi(path):
    with open(path) as f:
        return json.load(f)


def scan_abi(abi):
    findings = []
    fn_names = [f.get("name", "") for f in abi
                if f.get("type", "").lower() == "function" and f.get("name")]
    fn_lower = {n.lower(): n for n in fn_names}

    for keyword, needs_addr_param, message in RISKY_PATTERNS:
        for lower_name, real_name in fn_lower.items():
            if keyword in lower_name:
                if needs_addr_param:
                    entry = next(f for f in abi if f.get("name") == real_name)
                    params = [p.get("type", "") for p in entry.get("inputs", [])]
                    if "address" not in params:
                        continue
                findings.append((real_name, message))

    if "transferownership" in fn_lower:
        findings.append((fn_lower["transferownership"],
                          "Ownership is transferable — today's team may not control it tomorrow."))

    return findings, fn_names


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("address")
    ap.add_argument("--abi", help="path to a local ABI JSON file")
    args = ap.parse_args()

    print(f"=== Presale audit: {args.address} ===\n")

    info = fetch_contract_info(args.address)
    if info:
        print(f"Contract created: {info.get('date_created', 'unknown')}")
        print(f"Balance: {info.get('balance', 0)}")
    print()

    abi = None
    if args.abi:
        abi = load_abi(args.abi)
        print(f"Loaded ABI from {args.abi} ({len(abi)} entries)\n")
    else:
        abi = fetch_contract_abi(args.address)
        if abi:
            print(f"Loaded ABI live from chain via TronGrid ({len(abi)} entries)\n")

    if not abi:
        print("NO ABI FOUND ON-CHAIN. On TRON, a contract's ABI is normally written")
        print("on-chain at deploy time — no separate 'verification' step is needed to")
        print("expose it. This means you cannot audit what you cannot read. Do not send")
        print("funds to a sale contract you can't inspect, and separately check whether")
        print("its source is verified on a block explorer like TronScan.")
        sys.exit(1)

    findings, fn_names = scan_abi(abi)

    print(f"Functions found ({len(fn_names)}): {', '.join(sorted(fn_names))}\n")

    if not findings:
        print("No red flags matched from this checklist. This does NOT mean the contract")
        print("is safe — it means it passed these structural checks. Still read the")
        print("actual source code before sending money.")
    else:
        print(f"{len(findings)} finding(s):\n")
        for fn, msg in findings:
            print(f"  ⚠ {fn}(): {msg}")

    print("\nThis script checks structure, not intent. A contract can pass every check")
    print("here and still be malicious in ways only a full manual code read will catch.")


if __name__ == "__main__":
    main()
A clean report is not a green light — it just means the contract didn't match these 8 specific patterns. It can't catch a subtly wrong fee calculation, a backdoor hidden inside a legitimately-named function, or a team that's honest today and disappears tomorrow. Treat this as the first ten minutes of due diligence, not the whole process. No automated script replaces a professional smart contract audit for anything beyond a small amount of money.
A real, checkable example of the "fixed destination" pattern: AuditX's own AUDX presale contract is public and verifiable on TronScan — its withdrawUsdt(uint256 amount) and reclaimUnsold() functions take no address parameter at all; destination wallets are fixed at deployment and can't be changed by anyone, including the owner. That's not a claim, it's checkable directly against the contract's own ABI, the same way this script checks any other contract.

More free security tools

Browse the full free library →