
Agent
frozen-wUSDC
About this agent
time-locked wrapper for USDC transfers
frozen-wusdc
frozen-wusdc is a Solana/Anchor program for issuing a delayed-redemption form of USDC.
The intended flow is:
- Alice deposits raw USDC into the program for Bob.
- The program mints
wUSDCto Bob's Token-2022 account. - That
wUSDCis non-transferable and effectively soul-bound to Bob's address. - After a configured slot delay, Bob can redeem it back into raw USDC.
- Redemption is only allowed to Bob's canonical SPL USDC account.
This is designed for exchanges, custodians, and program-controlled settlement systems that want to slow post-hack fund movement without holding an entirely off-chain ledger.
The Problem It Solves
After a compromise, attackers usually try to move funds quickly:
- across fresh wallets
- into exchange deposit rails
- into bridge flows
- onto other networks before anyone can respond
Plain USDC does not help with that. Once it hits the attacker's wallet, it is immediately transferable.
This program changes the shape of that workflow:
- the recipient gets
wUSDC, not freely transferable USDC wUSDCcannot be directly forwarded to another wallet- the holder must wait through an on-chain delay before redemption
- redemption is pinned to the intended recipient's own USDC account
That gives operators a deterministic response window after issuance.
Core Model
The program uses a Token-2022 mint with the NonTransferable extension.
That gives wUSDC two important properties:
- it cannot be transferred wallet-to-wallet with ordinary token instructions
- it still behaves like a token balance for accounting and issuance purposes
Each issuance also creates a FreezeReceipt that records:
- who funded the issuance
- who the intended recipient is
- which
wUSDCATA received the balance - how much was issued
- the slot at which redemption becomes valid
The receipt is the on-chain authorization for later redemption.
Why This Helps Contain Hacks
If an exchange credits a user with plain USDC, the user can move it immediately.
If the exchange instead credits the user with wUSDC through this program:
- the balance is stuck to the user's address
- the user cannot forward it to a new wallet
- the user cannot hand it directly to a bridge as a transferable SPL asset
- the user cannot unwrap it early
- once the delay expires, the USDC can only be released to that same user's canonical USDC ATA
That does not make stolen funds disappear. It does make "get funds and immediately hop them somewhere else" materially harder.
Program Flow
1. initialize
Creates:
- the global config PDA
- the Token-2022
wUSDCmint - the SPL USDC vault
The mint is configured as non-transferable and uses the config PDA as mint authority.
2. issue_wusdc
Alice provides:
- her USDC source account
- Bob's wallet address
- a unique nonce
The program:
- derives Bob's canonical
wUSDCToken-2022 ATA - creates that ATA automatically if it does not already exist
- transfers Alice's USDC into the vault
- mints
wUSDC1:1 into Bob'swUSDCATA - creates a
FreezeReceipt - sets the receipt unlock slot
At this point Bob can see the wUSDC balance, but he cannot redeem it yet.
3. direct TransferChecked
Fails.
Because the mint uses Token-2022 NonTransferable, Bob cannot just forward wUSDC to Charlie.
4. unwrap
After the receipt unlock slot has passed, Bob can redeem.
The program verifies:
- the receipt is unlocked
- the receipt has not already been used
- the caller is the recorded recipient
- the caller is burning from the exact
wUSDCATA recorded in the receipt - the USDC destination is the caller's canonical SPL USDC ATA
Then it:
- burns the recorded amount of
wUSDC - releases the same amount of raw USDC from the vault
- marks the receipt claimed
5. close_receipt
After redemption, the claimed receipt can be closed and its rent returned to the treasury.
Security Properties
The current on-chain design enforces:
wUSDCis non-transferable.wUSDCissuance is tied to a per-issuance receipt.wUSDCredemption is delayed by slot count.- redemption is only available to the recorded recipient
- redemption is only paid to the recipient's canonical SPL USDC ATA
- a receipt can only be redeemed once
- the admin can pause new issuance
- authority transfer is two-step
What This Does Not Try To Solve
This repo is about delayed, recipient-bound redemption. It is not a full compliance system.
It does not currently provide:
- blacklist or sanctions logic
- KYC or identity attestation
- per-recipient risk scoring
- operator approval queues for each redemption
- compatibility with arbitrary DeFi protocols that expect transferable tokens
It also does not stop Bob from using his own USDC after the unlock period. That is intentional.
Threat Model Fit
This is a good fit when:
- an exchange wants to issue delayed-settlement balances on-chain
- a custodian wants a quarantine period before recipients receive liquid USDC
- a protocol wants "credit now, redeem later" behavior tied to the recipient address
This is not the right fit when:
- the asset must remain freely composable in DeFi while still being delayed
- recipients must be able to forward balances before redemption
- you need discretionary case-by-case approval instead of deterministic time locks
Admin Controls
The admin surface is intentionally small:
update_freeze_durationset_treasuryset_pausedpropose_authorityaccept_authority
Pause behavior today:
- paused: blocks new
issue_wusdc - not blocked by pause: already-issued balances can still be redeemed once unlocked
So pause is an intake brake, not a confiscation switch.
Integration Notes
- USDC is standard SPL Token.
wUSDCis Token-2022.wUSDCuses 6 decimals to match USDC.- the program auto-creates the recipient's canonical Token-2022 ATA for
wUSDCif needed - redemption pays only to the canonical SPL ATA for the recipient's USDC
- the delay is measured in Solana slots, not seconds
- the canonical program id across clusters is
5E8UrTuJjNk7u9fgVmsLFBD61FCzzwkPCaqzRogmhEjD
Repository Layout
- lib.rs: on-chain program
- tests/localnet_smoke.rs: validator-backed core flow and negative-path tests
- tests/localnet_admin.rs: validator-backed admin and governance tests
- tests/common/mod.rs: shared Rust fixture and instruction builders
- TESTING.md: test tiers, production test policy, CI gates, and known gaps
- scripts/deploy: cluster-aware deploy, initialize, and inspection scripts
- config/networks: per-network deploy config templates
- tests/frozen-wusdc.ts: non-blocking Anchor-style integration sketch
- Anchor.toml: local Anchor config
Local Development
The repo now treats Rust as the canonical test path.
Target runtime for automation:
- Agave / Solana CLI
3.1.13
The source of truth for that pin is scripts/ci/versions.sh. It should track the newest Agave release explicitly marked stable and suitable for Mainnet Beta.
This pin is a runtime and CI target, not a claim that the full crate graph has already been migrated to the newest Anchor or Solana SDK line. The program still carries older Anchor-era Rust dependencies and those must be requalified separately.
Build:
BASHcargo build-sbf
Start local validator:
BASHmkdir -p .localnet [[ -f .localnet/id.json ]] || solana-keygen new --no-bip39-passphrase --silent -o .localnet/id.json solana-test-validator \ --ledger .localnet/ledger \ --reset \ --mint "$(solana-keygen pubkey .localnet/id.json)" \ --rpc-port 8899 \ --faucet-port 0 \ --bind-address 127.0.0.1
Deploy:
BASHsolana program deploy \ --url http://127.0.0.1:8899 \ --keypair .localnet/id.json \ --program-id target/deploy/frozen_wusdc-keypair.json \ target/deploy/frozen_wusdc.so
Run fast Rust tests:
BASHcargo test-fast
Run the validator-backed suites:
BASHbash scripts/ci/run-localnet-suite.sh
Project docs and operator entry points:
TESTING.mdis the test and CI policyscripts/deploycontains the deploy, initialize, inspection, and operator workflowsconfig/networkscontains the per-network configuration templates
Network Deployment
This repo now supports the same program ID on:
localnetdevnettestnetmainnet
That is an intentional constraint of the current codebase. The program ID is fixed in lib.rs, so the safe deployment model is one canonical program address reused across clusters, not a different address per cluster.
Per-network inputs live outside source control:
- copy
config/networks/devnet.env.exampletoconfig/networks/devnet.env - copy
config/networks/testnet.env.exampletoconfig/networks/testnet.env - copy
config/networks/mainnet.env.exampletoconfig/networks/mainnet.env - fill in
DEPLOY_WALLET,TREASURY,USDC_MINT, andFREEZE_SLOTS
Deploy only:
BASHbash scripts/deploy/deploy-program.sh devnet
Initialize after deploy:
BASHbash scripts/deploy/initialize-program.sh devnet
One-shot deploy and initialize:
BASHbash scripts/deploy/full-deploy.sh devnet
Inspect the derived addresses or live config:
BASHcargo run --example admin -- addresses --rpc-url https://api.devnet.solana.com bash scripts/deploy/show-config.sh devnet
Live operator CLI:
BASHbash scripts/deploy/operator.sh devnet issue --recipient <PUBKEY> --amount 1.25 bash scripts/deploy/operator.sh devnet check-receipt --sender <PUBKEY> --nonce <U64> bash scripts/deploy/operator.sh devnet unwrap --sender <PUBKEY> --nonce <U64> --wallet "$HOME/.config/solana/id.json" bash scripts/deploy/operator.sh devnet close-receipt --sender <PUBKEY> --nonce <U64>
Devnet Deployment
- program id:
5E8UrTuJjNk7u9fgVmsLFBD61FCzzwkPCaqzRogmhEjD - config PDA:
5JFFUEbbMSakCdU49jJHAqNMUT6kgdxtj1QASpSCtsZW - authority:
9DHxnTzPvYwkgz2rxgappTh8ZpergAMd1kT8GG5aKxuW - treasury:
9DHxnTzPvYwkgz2rxgappTh8ZpergAMd1kT8GG5aKxuW - USDC mint:
4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU - wUSDC mint:
FEVpweZ9Scv5NRmx2FYtzSZgYqV1jri6tt6fYXCa7cZi - vault:
DQWmUNhALjsaNzbV5upBaNva5XyW8H3X15wTLsfRoPzT - freeze duration:
100slots - paused:
false
Deployment transactions:
- program deploy signature:
aJ5t5weAUXDXqesGmbViorassziXffHTazzsUNLyPRM9gQe1PZkVLaDmZD3EqL9bwsuZKR3tpU1XdbBzAFuFtJB - initialize signature:
3z7gCUNasHqjCkvfJVsahuSE8wvK8oDbbEq6mCAVxMDcesuMuFjYXgqgwvLibqFE5v6AkB3GAY8tbLBFJT6k1gHY
The current validator-backed suites verify:
- initialize
- issue Alice USDC to Bob as
wUSDC - direct
wUSDCtransfer rejection - early unwrap rejection
- delayed unwrap to Bob's USDC ATA
- receipt close
- pause blocking new issuance but not delayed redemption
- authority handoff and stale-authority rejection
- freeze-duration and treasury updates
- rejection of non-canonical recipient and redemption destinations
Current Status
The program has been exercised successfully on a fresh local validator with the exact flow above:
- Alice funds the issuance with USDC
- Bob receives non-transferable
wUSDC - Bob cannot transfer it away
- Bob cannot redeem early
- after the slot delay, Bob can redeem only into Bob's USDC account
That is the current security claim this repo is built to support.
Donations
Donations are gladly accepted at 9DHxnTzPvYwkgz2rxgappTh8ZpergAMd1kT8GG5aKxuW.
Source: https://github.com/trustless-engineering/frozen-wUSDC
Requirements
| Package | Installation |
|---|---|
| requests | pip3 install requests |
Agent Code
The main implementation code for this agent
Comments & Discussion
Scroll to load comments...
Tags
Share
This item is not available for tokenization.
Loading recommendations...