Skip to content

fix(contract): close open-proxy hole in burn (#1021) - #1059

Merged
Ejirowebfi merged 1 commit into
Favourorg:mainfrom
JoesWalker:fix/burn-open-proxy-1021
Jul 28, 2026
Merged

fix(contract): close open-proxy hole in burn (#1021)#1059
Ejirowebfi merged 1 commit into
Favourorg:mainfrom
JoesWalker:fix/burn-open-proxy-1021

Conversation

@JoesWalker

@JoesWalker JoesWalker commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

burn(token_address, from, amount) acted as an open proxy for arbitrary token contracts and skipped the burn_enabled gate for unknown tokens.

The old code looked the token up only after calling balance on it, and gated on burn_enabled only inside an if let Some(index) — so for any address the factory never deployed:

  • the burn_enabled invariant did not apply (unknown tokens bypassed it); and
  • the factory would invoke an arbitrary external contract (running attacker code with the factory as the caller) and emit its own official burn event referencing a token it never created — polluting the indexed history the Transaction History view renders.

Fix

Make the TokenIndex lookup mandatory and move it before any external call:

  • an unregistered token_address is rejected with TokenNotFound without the factory ever invoking it;
  • the burn_enabled gate is now unconditional (no code path reaches the burn call without verifying the flag).

Holders of external (non-factory) tokens can still burn directly on those tokens' own contracts — there is no legitimate factory-burn path for them.

Trust-boundary audit

mint_tokens, set_metadata, and set_burn_enabled already resolve the per-token owner/TokenIndex and reject unknown tokens (TokenNotFound) before doing anything. Added explicit tests locking that in so a future refactor can't silently reopen the boundary.

Tests (7 new, all green — 165 total)

  • test_burn_unregistered_token_fails — burn on a never-registered address → TokenNotFound, no factory event, external balance untouched.
  • test_burn_registered_index_missing_info_fails — dangling index → TokenNotFound.
  • test_burn_event_only_emitted_for_factory_token — a factory burn event is emitted only for a factory token; a rejected unregistered attempt emits none.
  • test_burn_disabled_no_bypassburn_enabled=false blocks burn with no bypass; balance untouched, no event.
  • test_mint_tokens_unregistered_token_fails, test_set_metadata_unregistered_token_fails, test_set_burn_enabled_unregistered_token_fails — trust-boundary lock-in.

cargo test (165 passed), cargo clippy -- -D warnings (clean), cargo fmt --check (clean).

Acceptance criteria

  • ✅ The factory only ever invokes token contracts it deployed — proven by the unregistered-address tests rejecting before any cross-contract call.
  • ✅ No code path can burn a factory token whose burn_enabled is false, and no factory burn event can reference a non-factory token.

Docs

docs/contract-abi.md — documented the trust boundary and TokenNotFound on burn.

Closes #1021

burn(token_address, from, amount) previously only checked the
burn_enabled flag when the token happened to be registered
(`if let Some(index)`), and looked the token up only *after* calling
`balance` on it. For any address the factory never deployed this meant:

- the burn_enabled invariant did not apply (unknown tokens bypassed it);
- the factory would invoke an arbitrary external contract (running
  attacker code with the factory as caller) and emit its own official
  `burn` event referencing a token it never created, polluting the
  indexed history the Transaction History view renders.

Make the TokenIndex lookup mandatory and move it *before* any external
call: an unregistered address is now rejected with TokenNotFound without
the factory ever touching it, and the burn_enabled gate is unconditional
as a side effect. Holders of external tokens can still burn directly on
those tokens' own contracts.

mint_tokens, set_metadata and set_burn_enabled already gate on the
per-token owner/index and reject unknown tokens; add explicit tests
locking that boundary in so a future refactor cannot silently reopen it.

Tests: burn on a never-registered address fails; a registered index with
a missing TokenInfo fails; a factory burn event is only ever emitted for
a factory token (and none for a rejected unregistered/disabled token);
burn_enabled=false blocks burn with no bypass.

Docs: document the trust boundary and TokenNotFound on burn in
contract-abi.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Ejirowebfi
Ejirowebfi merged commit 895a621 into Favourorg:main Jul 28, 2026
18 checks passed
github-actions Bot pushed a commit that referenced this pull request Jul 28, 2026
## [1.3.2](v1.3.1...v1.3.2) (2026-07-28)

### Bug Fixes

* **contract:** close open-proxy hole in burn ([#1021](#1021)) ([#1059](#1059)) ([895a621](895a621))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.3.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🟡 burn acts as an open proxy for arbitrary token contracts, and skips burn_enabled for unknown tokens

2 participants