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.
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
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.
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.
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.
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.
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.
pause(), blacklist(), setTradingEnabled() — mechanisms for selectively freezing specific buyers or the whole contract mid-sale.
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.
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()
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.