https://github.com/nikicat/cr2dep
PoC: TRON CREATE2 calldata-bound proxy deployer
https://github.com/nikicat/cr2dep
create2 deployer smart-contracts tron
Last synced: about 16 hours ago
JSON representation
PoC: TRON CREATE2 calldata-bound proxy deployer
- Host: GitHub
- URL: https://github.com/nikicat/cr2dep
- Owner: nikicat
- License: mit
- Created: 2026-05-20T21:05:27.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-20T21:20:45.000Z (about 1 month ago)
- Last Synced: 2026-05-21T02:58:19.411Z (about 1 month ago)
- Topics: create2, deployer, smart-contracts, tron
- Language: TypeScript
- Size: 58.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# cr2dep
PoC for one-shot, deterministic on-chain side-effects on TRON: every (target, calldata) pair gets its own CREATE2 address, and the contract at that address can only ever forward to *that* pair. Useful for use-cases like committing in advance to a future transfer/swap/contract interaction whose execution is then permissionless (anyone can pull the trigger, but only the bound action will fire).
## How it works
```
ProxyFactory.deploy(impl, salt, commitment)
│
▼ CREATE2 with init code = [10-byte constructor] ‖ [45-byte EIP-1167 stub→impl] ‖ [32-byte commitment]
│
▼
proxy at address A = keccak256(0x41 ‖ factory ‖ salt ‖ keccak256(initCode))[12:]
runtime = 45-byte EIP-1167 stub (commitment lives only in init code → binds A but isn't deployed)
anyone calls → A.execute(salt, target, data)
│
▼ delegatecall
BoundCaller.execute:
commitment = keccak256(abi.encode(target, data))
recompute A' = keccak256(0x41 ‖ FACTORY ‖ salt ‖ keccak256(initCode(SELF, commitment)))[12:]
require A' == address(this)
target.call{value: msg.value}(data)
```
Two contracts, deployed once per chain:
| Contract | Role |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `ProxyFactory` | Singleton. `deploy(impl, salt, commitment)` CREATE2s a 45-byte EIP-1167 proxy whose init code embeds `commitment`. |
| `BoundCaller` | Impl behind every proxy. Re-derives the proxy's own CREATE2 address from `(FACTORY, salt, initCode(SELF, keccak256(abi.encode(target, data))))` and refuses unless it matches `address(this)`. |
The address binding — not access control — is what makes each proxy single-purpose. `execute` is fully public; only the originally-bound (target, data) will pass the check.
Both contracts take a `PREFIX` immutable in their constructors. TRON uses `0x41` (the CREATE2 prefix replacement for EVM's `0xff`); the CLI sets this automatically. The EDR tests pass `0xff` so the same contracts can be tested end-to-end on a stock EVM.
Pairs are stored in a small SQLite DB; addresses are recomputed on demand and never persisted.
## Prerequisites
- Node ≥ 20
- `pnpm` (the repo declares `packageManager: pnpm@9.15.0` — Corepack will fetch it)
- A funded TRON account with TRX for fees. Either:
- **Browser wallet** (default): [TronLink](https://www.tronlink.org) installed in a browser. Nile testnet faucet: .
- **Local key** (opt-in, CI/unattended): a TRON private key in `PRIVATE_KEY`.
## Setup
```bash
pnpm install
pnpm compile
cp .env.example .env # set TRON_RPC; leave PRIVATE_KEY blank to use TronLink
pnpm cli init # creates cr2dep.db
```
### Signing
The CLI uses [`browser-tron-signer`](https://www.npmjs.com/package/browser-tron-signer) by default — every signing command spins up a small local HTTP server, opens a TronLink approval page in your browser, and waits for you to approve. No key ever touches the server.
If `PRIVATE_KEY` is set in the environment, it's used directly instead (handy for CI). When unset, browser signing is used.
Env vars:
| Var | Default | Notes |
| --------------- | ---------------------------- | -------------------------------------- |
| `PRIVATE_KEY` | unset | Opt-in fallback. If set, signs locally with this key instead of TronLink. Hex (with or without `0x`). |
| `TRON_RPC` | `https://nile.trongrid.io` | Full-node HTTP endpoint |
| `TRON_API_KEY` | unset | Trongrid API key (raises rate limits) |
| `TRON_NETWORK` | inferred from `TRON_RPC` | Browser-signer network: `mainnet` / `shasta` / `nile` |
| `TRON_MCP_PORT` | `3848` | Local HTTP port for the browser approval UI |
| `BROWSER` | OS default | Force a specific browser for the approval page (e.g. `firefox`, `brave-browser`). Useful when TronLink isn't installed in your default browser. |
## Deploy the on-chain singletons
You only do this once per network. Addresses are persisted to `.cr2dep.json`.
```bash
pnpm cli deploy-factory # ProxyFactory (no constructor args beyond PREFIX=0x41)
pnpm cli deploy-impl # BoundCaller(factoryAddress, PREFIX=0x41)
```
If you've already deployed these (e.g. shared between team members), skip the deploys and just point the CLI at them:
```bash
pnpm cli set-factory T...
pnpm cli set-impl T...
```
## Day-to-day usage
```bash
# Store a binding. Prints the predicted CREATE2 address right away.
pnpm cli add --target T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb \
--data 0xa9059cbb... \
--salt 0x00... # optional, defaults to bytes32(0)
pnpm cli list # all stored pairs with predicted addresses
pnpm cli address 1 # just the address for pair #1
pnpm cli deploy 1 # CREATE2-deploys the proxy bound to pair #1
pnpm cli execute 1 # call execute(salt, target, data) on the proxy — anyone can do this
```
`add` only writes to SQLite — no network call, no funds needed. `deploy-factory`, `deploy-impl`, `deploy`, and `execute` are the only commands that broadcast transactions; each will pop a TronLink approval page in your browser unless `PRIVATE_KEY` is set.
The same proxy address can be re-`execute`d as many times as you like; the address binding is what's enforced, not one-shot semantics. (If you want one-shot, gate it with a storage slot in `BoundCaller`.)
### Sample bindings (TRON mainnet)
The proxy only ever does its single bound action, so for sanity-checking the pipeline you want something cheap and harmless. Two options against USDT (`TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`):
```bash
# Read-only: balanceOf(0x0...0) — view call, return value discarded, proxy holds nothing.
pnpm cli add \
--target TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t \
--data 0x70a082310000000000000000000000000000000000000000000000000000000000000000
# Tiny write: approve(TPL66VK2gCXNCD7EJg9pgJRfqcRazjhUZY, 0) — observable on Tronscan,
# revokes any allowance from the proxy to that spender (zero before, zero after — no-op revoke).
pnpm cli add \
--target TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t \
--data 0x095ea7b3000000000000000000000000922953dcf6886cf3afe9faf80f93922e57b1b0fd0000000000000000000000000000000000000000000000000000000000000000
```
Mainnet deploys burn real TRX (rough order: 200–500 TRX per singleton, ~30 per proxy). To verify the wiring without spending mainnet TRX, point `TRON_RPC` at Nile (`https://nile.trongrid.io`) and use the [Nile faucet](https://nileex.io/join/getJoinPage).
## Tests
EDR (Hardhat 3's in-process EVM) is used as a stand-in for the TVM. It uses Ethereum's `0xff` CREATE2 prefix instead of TRON's `0x41`, so the tests deploy both contracts with `PREFIX=0xff` and exercise the full deploy + execute path end-to-end:
```bash
pnpm test # hardhat test nodejs
pnpm typecheck # tsc --noEmit
```
Covers init-code shape, off-chain ↔ on-chain address parity, deploy at predicted address, calldata + value forwarding, binding-mismatch reverts (wrong data / target / salt), and bubbled target reverts.
## TRON-specific notes
- **CREATE2 prefix:** TRON's `CREATE2` opcode uses `0x41` where Ethereum uses `0xff`. The contracts take this as a deploy-time immutable; the CLI's `src/create2.ts` defaults to `0x41`. If you deploy with the wrong prefix the system silently stops working — every `execute` will fail `BindingMismatch`.
- **PUSH0:** disabled (`evmVersion: "paris"`) so the compiled bytecode runs on any TVM hardfork still in the wild.
- **Address format:** the CLI accepts and prints `T…` base58 addresses (canonical TRON form) and `0x…` 20-byte hex interchangeably. The 20-byte form is what gets stored in SQLite and hashed.
## Layout
```
contracts/
ProxyFactory.sol singleton; CREATE2-deploys the proxies
BoundCaller.sol impl behind every proxy; verifies address↔(target,data) binding
test/TestTarget.sol tiny counter target used by tests only
src/
cli.ts commander CLI
signer.ts browser-tron-signer + PRIVATE_KEY fallback, as an AsyncDisposable
create2.ts commitmentOf / buildProxyInitCode / predictProxyAddress
artifacts.ts loads Hardhat 3 artifacts
tron.ts T-base58 ↔ 0x EVM hex (via TronWeb static utils)
db.ts better-sqlite3 — pairs(id, target, calldata, salt, deployed, tx_hash)
config.ts .cr2dep.json: { factoryAddress, implementationAddress }
test/cr2dep.test.ts EDR end-to-end tests (14 cases)
hardhat.config.ts solc 0.8.28, evmVersion paris, hardhat-toolbox-viem plugin
```
## Status
PoC. Not audited. Don't ship to mainnet without a review — in particular, anything that relies on the binding-check assembly or the CREATE2 prefix is the kind of thing that breaks silently if something upstream changes.