Skip to content

feat(token-mint-authority): add testnet faucet mint-authority program - #331

Open
0x-r4bbit wants to merge 1 commit into
mainfrom
feat/token-minter
Open

feat(token-mint-authority): add testnet faucet mint-authority program#331
0x-r4bbit wants to merge 1 commit into
mainfrom
feat/token-minter

Conversation

@0x-r4bbit

Copy link
Copy Markdown
Collaborator

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.

@0x-r4bbit
0x-r4bbit force-pushed the feat/token-minter branch 2 times, most recently from 9bd9e2e to 66983e1 Compare August 27, 2026 07:04
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 implementing FaucetMint + 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.

Comment on lines +63 to +72
// 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 }
Comment on lines +32 to +37
{
"name": "mint_authority",
"writable": true,
"signer": false,
"init": false
},
Comment on lines +37 to +41
#[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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@3esmit 3esmit Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exploit call path:

  1. Attacker deploys program Tok and initializes account Fox owned by Tok. Fox contains a valid TokenDefinition::Fungible whose authority is faucet PDA Pog.
  2. Attacker calls FaucetMint with a valid signed recipient, fresh allowance PDA, user_holding = Dom where Dom is a real faucet token definition, token_definition = Fox, mint_authority = Pog, and canonical clock.
  3. faucet_mint accepts Fox: it is initialized, decodes as fungible, and names Pog as authority. Line 70 selects Tok from Fox.program_owner.
  4. The emitted call invokes Tok with [Fox, Dom, Pog], marks Pog authorized, and supplies the faucet PDA seed.
  5. Runtime validates that PDA authorization for Tok, then unions Pog into the authorization set inherited by subsequent calls. Tok can therefore emit a call to the real Token Program with [Dom, Pog] and SetAuthorityWithAuthority { new_authority: Some(attacker) }.
  6. Token's authority checks pass: Dom is Token-owned, Dom.authority == Pog, and Pog.is_authorized == true. The attacker now owns mint authority and can mint arbitrary amounts without the faucet cooldown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants