feat(token-mint-authority): add testnet faucet mint-authority program - #331
feat(token-mint-authority): add testnet faucet mint-authority program#3310x-r4bbit wants to merge 1 commit into
Conversation
9bd9e2e to
66983e1
Compare
A permissionless faucet that holds the mint authority for faucet token definitions and lets any account self-mint a fixed grant, so testers can fund themselves on testnet. The call chain is user -> token-mint-authority -> token: the program holds no key and delegates every mint to the token program under a PDA seed. - `FaucetMint` (no amount) mints a fixed 10_000e18 to the caller, at most once per 24h per (recipient, token definition), via a chained `Token::MintWithAuthority` authorized by the mint-authority PDA seed. - Rate limiting is a per-(recipient, definition) `MintAllowance` PDA (last-mint timestamp), claimed on first use and rewritten thereafter. - Wall-clock time is read from the system CLOCK_01 account, as the stablecoin program does. Setup: the faucet token must be created via the token program's `NewFungibleDefinition` with `mint_authority` set to `compute_mint_authority_pda(<program id>)`; the program refuses to mint a token whose stored authority is not its PDA.
66983e1 to
a813456
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The faucet currently lacks a critical ownership-consistency check before delegating the chained mint and also over-requests write access for the mint-authority PDA, both of which widen the attack surface.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a new token_mint_authority program to the LEZ workspace: a permissionless, rate-limited testnet faucet that delegates a fixed mint to the Token Program using a mint-authority PDA seed, with both unit and zkVM integration test coverage.
Changes:
- Introduces
token_mint_authority_{core,program,methods,guest}crates implementingFaucetMint+ PDA-based cooldown tracking. - Adds unit + integration tests covering PDA derivation, chained mint behavior, and cooldown enforcement.
- Wires the new program into guest build scripts, workspace manifests/locks, IDL artifacts, and README docs.
File summaries
| File | Description |
|---|---|
| scripts/build-guests.sh | Include token_mint_authority in guest binary packaging. |
| scripts/build-guests.Dockerfile | Include token_mint_authority in Docker guest build loop. |
| README.md | Document the new program and update test/build/IDL examples. |
| Cargo.toml | Add new crates to workspace members and shared deps. |
| Cargo.lock | Lockfile updates for new crates/deps. |
| programs/token_mint_authority/src/lib.rs | New host crate entry module. |
| programs/token_mint_authority/src/faucet_mint.rs | Implements faucet mint host logic + clock read + chained mint. |
| programs/token_mint_authority/src/test_support.rs | Test account builders and constants. |
| programs/token_mint_authority/src/tests.rs | Unit tests for cooldown, PDAs, chained call shape, and preconditions. |
| programs/token_mint_authority/core/src/lib.rs | New core types/constants + PDA derivations + verification helpers. |
| programs/token_mint_authority/core/Cargo.toml | New core crate manifest. |
| programs/token_mint_authority/Cargo.toml | New host crate manifest + example deps. |
| programs/token_mint_authority/examples/mint_authority.rs | Helper to compute/print mint-authority PDA for a built binary. |
| programs/token_mint_authority/methods/build.rs | Embed guest methods via risc0_build. |
| programs/token_mint_authority/methods/Cargo.toml | Methods crate manifest. |
| programs/token_mint_authority/methods/src/lib.rs | Host-side embedded ELF constants include. |
| programs/token_mint_authority/methods/guest/Cargo.toml | Guest crate manifest. |
| programs/token_mint_authority/methods/guest/Cargo.lock | Guest lockfile. |
| programs/token_mint_authority/methods/guest/src/bin/token_mint_authority.rs | Guest entry wiring + FaucetMint instruction handler. |
| programs/integration_tests/Cargo.toml | Add token-mint-authority core/methods to integration tests. |
| programs/integration_tests/tests/token_mint_authority.rs | New zkVM E2E tests for faucet behavior. |
| artifacts/token_mint_authority-idl.json | New IDL for the faucet instruction/accounts. |
Review details
- Files reviewed: 20/22 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // The faucet token is owned by the Token Program; that owner is the target | ||
| // of the chained mint. | ||
| assert_ne!( | ||
| token_definition.account, | ||
| Account::default(), | ||
| "Faucet token definition must be initialized" | ||
| ); | ||
| let token_program_id = token_definition.account.program_owner; | ||
| let definition_id = token_definition.account_id; | ||
|
|
| [dev-dependencies] | ||
| risc0-zkvm = { version = "=3.0.5", default-features = false } | ||
| # For the `mint_authority` example: decode a built guest `.bin` and compute its ImageID. | ||
| risc0-binfmt = { version = "=3.0.4", default-features = false } |
| { | ||
| "name": "mint_authority", | ||
| "writable": true, | ||
| "signer": false, | ||
| "init": false | ||
| }, |
| #[account(mut)] | ||
| token_definition: AccountWithMetadata, | ||
| #[account(mut)] | ||
| mint_authority: AccountWithMetadata, | ||
| clock: AccountWithMetadata, |
| Account::default(), | ||
| "Faucet token definition must be initialized" | ||
| ); | ||
| let token_program_id = token_definition.account.program_owner; |
There was a problem hiding this comment.
token_definition.account.program_owner is attacker-controlled whenever the supplied definition is owned by an attacker-deployed program, yet this value becomes the target of a call carrying the faucet's singleton mint-authority PDA authorization. An attacker can create a program-owned account whose data decodes as TokenDefinition::Fungible { authority: Some(mint_authority_pda), .. }, pass a real faucet definition in the unchecked user_holding slot, and reach this call. The malicious callee then receives mint_authority_pda as authorized and can chain Token::SetAuthorityWithAuthority with the real definition, rotating its authority to the attacker. LEZ deliberately carries validated authorization monotonically into nested calls, so the fixed mint amount and allowance update do not contain this path.
Please bind this authority to the intended Token Program: either hard-bind a trusted Token Program ImageID, or include the verified target program ID in the mint-authority PDA seed so an arbitrary callee receives a different PDA. A caller-supplied program ID or only a user_holding owner check is insufficient while the same singleton PDA controls real faucet definitions. Add an adversarial chained-call test that proves an alternate owner cannot reuse the authority in a nested token call.
There was a problem hiding this comment.
Exploit call path:
- Attacker deploys program
Tokand initializes accountFoxowned byTok.Foxcontains a validTokenDefinition::Fungiblewhose authority is faucet PDAPog. - Attacker calls
FaucetMintwith a valid signedrecipient, fresh allowance PDA,user_holding = DomwhereDomis a real faucet token definition,token_definition = Fox,mint_authority = Pog, and canonical clock. faucet_mintacceptsFox: it is initialized, decodes as fungible, and namesPogas authority. Line 70 selectsTokfromFox.program_owner.- The emitted call invokes
Tokwith[Fox, Dom, Pog], marksPogauthorized, and supplies the faucet PDA seed. - Runtime validates that PDA authorization for
Tok, then unionsPoginto the authorization set inherited by subsequent calls.Tokcan therefore emit a call to the real Token Program with[Dom, Pog]andSetAuthorityWithAuthority { new_authority: Some(attacker) }. - Token's authority checks pass:
Domis Token-owned,Dom.authority == Pog, andPog.is_authorized == true. The attacker now owns mint authority and can mint arbitrary amounts without the faucet cooldown.
A permissionless faucet that holds the mint authority for faucet token definitions and lets any account self-mint a fixed grant, so testers can fund themselves on testnet. The call chain is user -> token-mint-authority -> token: the program holds no key and delegates every mint to the token program under a PDA seed.
FaucetMint(no amount) mints a fixed 10_000e18 to the caller, at most once per 24h per (recipient, token definition), via a chainedToken::MintWithAuthorityauthorized by the mint-authority PDA seed.MintAllowancePDA (last-mint timestamp), claimed on first use and rewritten thereafter.Setup: the faucet token must be created via the token program's
NewFungibleDefinitionwithmint_authorityset tocompute_mint_authority_pda(<program id>); the program refuses to mint a token whose stored authority is not its PDA.