Anonymous Dual-Key Payment Protocol — Built on Base Mainnet
Ghost Protocol is a privacy-first ERC-20 payment protocol that enables secure, consent-based token transfers between a public wallet (Hash1) and a fully private off-chain wallet (Hash2) — without exposing the private address on-chain in V1 (partial anonymity: sig2 visible in calldata, to = Hash2 visible on Basescan), fully solved in V2 via keccak256 key2Proof + stealth addresses.
- V1 = The vault — triple-validation secured (Pseudo2 + Key2 + Key1), anti-spam Guardian System, mathematically inviolable escrow
- V2 = The bank — full Hash2 anonymization via keccak256 key2Proof + stealth addresses, simplified UX
Built and deployed solo by Rayane Hila — RayTech R&D (Belgium)
| Field | Value |
|---|---|
| Contract Address | 0x747bb06a40F3f895F7d7BB8764C9B23a4bC11929 |
| Network | Base Mainnet — Chain ID: 8453 |
| Deploy Block | 41974756 |
| Compiler | Solidity v0.8.20+commit.a1b79de6 |
| Optimization | 200 runs — Paris EVM |
| Verification | ✓ Exact Match — 15 files — Standard Json-Input |
| Static Analysis | Slither — 0 High / 0 Medium |
| Author (NatSpec) | @author Rayane Hila - RayTech R&D |
┌─────────────────────────────────────────────────────────────┐
│ GHOST PROTOCOL V1 │
│ │
│ SENDER ──── sendToPseudo1() ────► ESCROW (contract) │
│ │ │
│ Guardian System │
│ senderBlocked[h1][s][t]=true │
│ │ │
│ Hash1 ◄──── approveReceive() ─── Pseudo2 + sig1 Key1 │
│ (public) rejectReceive() (line 282 + line 292) │
│ ▲ │
│ │ │
│ Hash2 ────── sendFromHash1() ─── Pseudo2 + sig2 Key2 │
│ (private) Triple Validation + sig1 Key1 │
│ L.475 / L.484 / L.496 │ │
│ ▼ │
│ RECIPIENT │
└─────────────────────────────────────────────────────────────┘
| Component | Role | Visibility |
|---|---|---|
Pseudo1 |
Public username (e.g. alice) — 1-32 bytes |
On-chain — public |
Hash1 |
Public wallet address — pays gas | On-chain — public by design |
Hash2 |
Cold private wallet — signs only, never on-chain | Off-chain — recoverable via ecrecover in V1 / unreachable in V2 |
Key1 |
ECDSA sig1 — required for approve / reject / send | Off-chain — signature required |
Key2 (V1) |
ECDSA sig2 — required for send only | Off-chain — address recoverable via ecrecover (V1 limitation) |
key2Secret (V2) |
keccak256 proof — replaces sig2 | Off-chain — Hash2 unreachable (V2 fix) |
Pseudo2 |
Shared secret — required for approve / reject / send (NOT for receiving) | Off-chain — keccak256(Pseudo2) stored on-chain only |
Escrow |
Smart contract vault | On-chain — hash1TokenBalances[hash1][token] private mapping |
External wallet → sendToPseudo1('alice', token, amount)
— CEI Pattern: Effects before Interactions —
→ senderBlocked[hash1][msg.sender][token] = true (Guardian — line 231)
→ pendingByFingerprint[hash2Fingerprint].push(...) (state — line 235)
→ safeTransferFrom(msg.sender, address(this), amount) (interaction LAST — line 246)
→ emit TransferPending(hash2Fingerprint, from, token, amount, index, timestamp)
Notes:
- Hash2 NEVER emitted — only fingerprint = keccak256(Hash2)
- TIMEOUT = 7 days (line 81) — transfer expires if not approved/rejected
- Pseudo2 NOT required at this stage
Hash1 → approveReceive(hash1, transferIndex, pseudo2Hash, sig1)
→ require(acc.pseudo2Hash == pseudo2Hash) (line 282)
→ message = keccak256("approve" + hash1 + index + nonce + address(this) + chainId)
→ verifySignature(message, sig1, acc.key1Hash) (line 292)
→ nonces[hash1]++ (line 297)
→ require(!transfer.processed) (line 311)
→ require(block.timestamp <= transfer.timestamp + TIMEOUT) (line 312)
→ transfer.approved = true; transfer.processed = true (lines 317-318)
→ hash1TokenBalances[hash1][token] += amount (line 325)
→ senderBlocked[hash1][from][token] = false (Guardian reset — line 328)
Hash1 → rejectReceive(hash1, transferIndex, pseudo2Hash, sig1)
→ Same validations with message = keccak256("reject" + ...)
→ nonces[hash1]++ (line 366)
→ transfer.processed = true (line 386 — CEI)
→ senderBlocked reset (line 389)
→ safeTransfer(transfer.from, transfer.amount) (line 396 — funds returned)
Anyone → autoRejectExpired(hash1, transferIndex)
→ No signature required — callable by anyone
→ require(block.timestamp > transfer.timestamp + TIMEOUT) (line 421)
→ transfer.processed = true (line 426 — CEI)
→ senderBlocked reset (line 429)
→ safeTransfer(transfer.from, transfer.amount) (line 436 — funds returned)
Hash2 + Hash1 → sendFromHash1(hash1, to, token, amount, deadline, pseudo2Hash, sig2, sig1)
→ require(block.timestamp <= deadline) (line 465)
→ (1) require(acc.pseudo2Hash == pseudo2Hash) (line 475)
→ (2) message2 = keccak256("send" + hash1 + to + token + amount + nonce + deadline + address(this) + chainId)
verifySignature(message2, sig2, acc.key2Hash) (lines 484-491)
→ (3) message1 = keccak256("send_unlock" + same params)
verifySignature(message1, sig1, acc.key1Hash) (lines 496-503)
→ nonces[hash1] = currentNonce + 1 (line 506 — CEI)
→ require(hash1TokenBalances[hash1][token] >= amount) (line 516)
→ hash1TokenBalances[hash1][token] -= amount (line 520 — CEI)
→ safeTransfer(to, amount) (line 523)
→ emit SentFromHash1(hash1, to, token, amount)
Hash2 NEVER emitted
| Protection | Implementation | Coverage |
|---|---|---|
ReentrancyGuard |
nonReentrant on 5 state functions: sendToPseudo1 (L.214), approveReceive (L.276), rejectReceive (L.350), autoRejectExpired (L.407), sendFromHash1 (L.463) |
Reentrancy attacks |
| CEI Pattern | Effects before Interactions on all state changes — senderBlocked before safeTransferFrom, transfer.processed before safeTransfer |
Flash loan / reentrancy |
| Anti-Replay ×3 | nonce + address(this) + block.chainid in every state-changing message (approve, reject, send, send_unlock) — address(this) + block.chainid in view message (get_pending, no nonce needed) |
Cross-contract / cross-chain replay |
| Deadline | require(block.timestamp ≤ deadline) in sendFromHash1 only (line 465) |
Delayed transaction attacks |
| TIMEOUT | TIMEOUT = 7 days (line 81) — transfers expire in approveReceive (L.312) and rejectReceive (L.381) |
Locked-funds attacks |
| SafeERC20 | safeTransferFrom (L.246) + safeTransfer (L.396, L.436, L.523) |
Non-standard tokens (USDT etc.) |
| Guardian System | senderBlocked[hash1][sender][token] — reset on approve (L.328), reject (L.389), autoRejectExpired (L.429) |
Spam / flood attacks |
| Triple Validation | Pseudo2 (L.475) + sig2 ECDSA Key2 (L.484) + sig1 ECDSA Key1 (L.496) — all 3 required simultaneously | Single-key or single-secret compromise insufficient |
| Anti Gas DoS | maxReturn = 100 in getPendingTransfers — count unprocessed → allocate exact → fill |
Unbounded loop attacks |
| EIP-712 | DOMAIN_SEPARATOR computed in constructor (line 95) — PROTOCOL_NAME + PROTOCOL_VERSION + chainId + address(this) |
Signature domain isolation |
Single-file Electron interface (index.html) using ethers.js with Ledger USB hardware wallet support.
Tab 1 — Configuration
- Hash1, Hash2, Pseudo2, contract address, RPC URL
- Config persisted in
localStorage - Connects via
JsonRpcProviderto Base mainnet (https://mainnet.base.org) - Computes
myFingerprint = keccak256(solidityPacked(['address'], [hash2]))for event filtering - Escrow balance check via
getHash1TokenBalance
Tab 2 — Create Account (createAccount)
- Pseudo1 input (1–32 bytes)
- Key1: private key or Ledger USB
- Key2: private key, address only (cold wallet mode), or Ledger USB
- Real-time derivation preview (Hash1, Hash2, pseudo2Hash, key1Hash, key2Hash, hash2Fingerprint)
- Hash derivations matching contract exactly:
key1Hash = keccak256(solidityPacked(['address'], [hash1])) key2Hash = keccak256(solidityPacked(['address'], [hash2])) hash2Fingerprint = keccak256(solidityPacked(['address'], [hash2])) pseudo2Hash = keccak256(toUtf8Bytes(pseudo2))
- Pre-flight checks: pseudo1 availability, hash1 uniqueness, ETH balance
- Config auto-saved on success — private keys cleared from form immediately
Tab 3 — Incoming Transfers (approve / reject)
- Real-time listener:
contract.on('TransferPending')filtered bymyFingerprint - Past event scan: chunks of 5000 blocks from deploy block
41974756 - Manual scan button for force-searching past pending transfers
- Auto-switches to Transfers tab on incoming event
- Approve:
approveReceive(hash1, index, pseudo2Hash, sig1)— Key1 + Pseudo2 - Reject:
rejectReceive(hash1, index, pseudo2Hash, sig1)— Key1 + Pseudo2 - Key1: private key or Ledger USB
Tab 4 — Send from Escrow (sendFromHash1)
- Key1 + Key2 + Pseudo2 required simultaneously
- Escrow balance check before any signing
- Deadline:
Math.floor(Date.now() / 1000) + 3600(1 hour window) - Signing order matching contract exactly:
// sig2 — Key2 signs (lines 484-485): keccak256("send" + hash1 + to + token + amount + nonce + deadline + contract + chainId) // sig1 — Key1 signs (lines 496-497): keccak256("send_unlock" + hash1 + to + token + amount + nonce + deadline + contract + chainId)
- Key1: private key or Ledger USB
- Key2: private key or Ledger USB
- Private keys cleared from form after transaction
Ghost Protocol V1 is fully deployed, verified, and operational on Base mainnet. This is not a testnet prototype — it is a production smart contract with real transactions.
All transactions are publicly verifiable on Basescan:
https://basescan.org/address/0x747bb06a40F3f895F7d7BB8764C9B23a4bC11929
| Change | V1 (current) | V2 (to build) | Impact |
|---|---|---|---|
Key2 proof — sendFromHash1 |
sig2 ECDSA → ecrecover → Hash2 visible |
key2Proof = keccak256(key2Secret + params) |
Hash2 100% invisible |
| Recipient address | to = Hash2 direct → visible Basescan |
to = stealth address (ephemeral per tx) |
No address linkability |
key2Hash storage |
keccak256(Key2_wallet_address) — L.666 |
keccak256(key2Secret + "GhostProtocol-v2" + contract + chainId) |
Domain salt — no cross-chain reuse |
Key2 proof — getPendingTransfers |
ECDSA sig → ecrecover → Hash2 exposed on read | key2Proof keccak256 — ecrecover impossible |
Full read privacy |
getHash1TokenBalance |
Public — anyone can read balances | require(msg.sender == hash1) |
Financial surveillance blocked |
createAccount() |
No msg.sender restriction |
require(msg.sender == hash1) |
Access control enforced in V2 |
pendingByFingerprint |
Array grows unbounded | Processed entries purged | Gas optimization |
ghost-protocol/
│
├── README.md
├── LICENSE
├── SECURITY.md # Responsible disclosure policy + known V1 limitations
│
└── contracts/
└── GhostProtocol.sol # V1 — deployed & verified on Base mainnet
Interface status — V1 operational, not yet publicly distributed. The V1 Electron interface is functional and used for internal testing. It requires manual configuration (RPC URL, contract address, private key or Ledger USB) and targets technical users only at this stage.
The interface repository is private during this phase — the app handles private key input and Ledger USB integration directly, and publishing the source would enable visually identical phishing clones before a stable public release.
A retail-ready V2 interface is planned alongside the V2 smart contract.
Business Source License 1.1 — see LICENSE
- Non-commercial use (personal, research, educational, open-source) → free, no permission needed
- Commercial use → written permission required — rayane@ia-optimisation.org
- On March 7, 2030 → automatically converts to GPL-3.0 (all forks must remain open source)
Rayane Hila — RayTech R&D — Independent R&D Lab — Belgium
- Creator wallet:
0x78F17Ac54bE3e75D55e8Ef1719991253a4468dFA - Verified contract: Basescan source code
Ghost Protocol V1 — March 2026 — Base Mainnet — Chain ID: 8453