Skip to content

Fix small bugs in TokenForwarding for bricked forwarder and wrong getSupportedVaultTypes#185

Merged
joshuahannan merged 9 commits intomasterfrom
fix/token-forwarding-bugs
Mar 11, 2026
Merged

Fix small bugs in TokenForwarding for bricked forwarder and wrong getSupportedVaultTypes#185
joshuahannan merged 9 commits intomasterfrom
fix/token-forwarding-bugs

Conversation

@joshuahannan
Copy link
Member

@joshuahannan joshuahannan commented Mar 10, 2026

Summary

  • Adds CLAUDE.md and AGENTS.md for this repo

  • changeRecipient permanently bricks forwarder on stale old capability: The old self.recipient.borrow<&{FungibleToken.Receiver}>()! force-unwrap panicked if the old recipient had deleted their vault or revoked the capability. This meant the forwarder owner could never redirect to a working address — the forwarder was permanently stuck. Changed to an optional borrow so changeRecipient always succeeds regardless of the old cap's state.

  • getSupportedVaultTypes returns wrong type for chained forwarders: The old code returned {vaultRef.getType(): true}, which gives the receiver's concrete runtime type (e.g. TokenForwarding.Forwarder) instead of the underlying depositable vault type when forwarders are chained. Any caller using isSupportedVaultType on a chained forwarder would incorrectly get false. Now delegates to vaultRef.getSupportedVaultTypes() to correctly propagate the supported types.

Test plan

  • Verify that changeRecipient succeeds when the old recipient capability is stale (recipient deleted vault / revoked cap)
  • Verify that ForwarderRecipientUpdated event is emitted with oldRecipient: nil when old cap is stale
  • Verify that getSupportedVaultTypes() on a Forwarder returns the underlying vault type (e.g. ExampleToken.Vault), not TokenForwarding.Forwarder
  • Verify that getSupportedVaultTypes() correctly propagates through chained forwarders
  • Verify existing forwarder tests still pass

🤖 Generated with Claude Code

joshuahannan and others added 2 commits March 10, 2026 12:34
Two bugs fixed in TokenForwarding.Forwarder:

1. changeRecipient: force-unwrapping the old recipient capability panicked
   if it had gone stale (e.g. recipient deleted their vault), permanently
   preventing the forwarder owner from redirecting to a working address.
   Changed to an optional borrow so the update always succeeds regardless
   of the old capability's validity.

2. getSupportedVaultTypes: returned {vaultRef.getType(): true}, which yields
   the receiver's concrete runtime type (e.g. TokenForwarding.Forwarder)
   rather than the underlying vault type when forwarders are chained.
   Now delegates to vaultRef.getSupportedVaultTypes() so callers correctly
   discover the actual depositable vault type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CLAUDE.md and AGENTS.md with repo guidance covering commands,
  architecture, contract hierarchy, and key Cadence patterns
- Regenerate lib/go/contracts/internal/assets/assets.go to embed the
  updated TokenForwarding.cdc changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@joshuahannan joshuahannan changed the title fix: TokenForwarding bricked forwarder and wrong getSupportedVaultTypes Fix small bus in TokenForwarding for bricked forwarder and wrong getSupportedVaultTypes Mar 10, 2026
@joshuahannan joshuahannan changed the title Fix small bus in TokenForwarding for bricked forwarder and wrong getSupportedVaultTypes Fix small bugs in TokenForwarding for bricked forwarder and wrong getSupportedVaultTypes Mar 10, 2026
joshuahannan and others added 2 commits March 10, 2026 12:49
Tests for the two fixes made in contracts/utility/TokenForwarding.cdc:

1. changeRecipient with stale old capability (bug fix #1):
   - testChangeRecipientSucceedsWhenOldCapabilityIsStale: verifies the
     transaction succeeds and the ForwarderRecipientUpdated event emits
     nil for oldRecipient when the old cap is no longer valid
   - testTokensForwardedToNewRecipientAfterChange: end-to-end check that
     tokens sent to the Forwarder after the update arrive at the new
     recipient, not the stale old one

2. getSupportedVaultTypes for chained forwarders (bug fix #2):
   - testGetSupportedVaultTypesForSingleForwarder: baseline check that a
     single Forwarder → Vault returns {ExampleToken.Vault: true}
   - testGetSupportedVaultTypesForChainedForwarders: the key regression
     test — Forwarder B → Forwarder A → Vault must return
     {ExampleToken.Vault: true}, not {TokenForwarding.Forwarder: true}

Also adds tests/transactions/unload_example_token_vault.cdc, a test-only
helper transaction that removes and destroys an account's ExampleToken
vault to simulate a stale receiver capability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove references to specific fixes and implementation details from
doc comments. Each test now only describes what it is verifying.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
joshuahannan and others added 2 commits March 10, 2026 15:48
- Fix AGENTS.md heading to say AGENTS.md instead of CLAUDE.md
- Add workflow orchestration, task management, and core principles
  sections to both AGENTS.md and CLAUDE.md
- Remove outdated note in README.md about cadence-0.42 branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
// so we use an optional borrow instead of a force-unwrap to avoid permanently
// bricking the forwarder in that case.
let oldRef = self.recipient.borrow<&{FungibleToken.Receiver}>()
emit ForwarderRecipientUpdated(owner: self.owner?.address, oldRecipient: oldRef?.owner?.address, newRecipient: newRef.owner?.address, newReceiverType: newRef.getType().identifier, newReceiverUUID: newRef.uuid)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe make this more readable by choping down the long argument list so each argument is on separate line

// so we use an optional borrow instead of a force-unwrap to avoid permanently
// bricking the forwarder in that case.
let oldRef = self.recipient.borrow<&{FungibleToken.Receiver}>()
emit ForwarderRecipientUpdated(owner: self.owner?.address, oldRecipient: oldRef?.owner?.address, newRecipient: newRef.owner?.address, newReceiverType: newRef.getType().identifier, newReceiverUUID: newRef.uuid)

Choose a reason for hiding this comment

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

Suggested change
emit ForwarderRecipientUpdated(owner: self.owner?.address, oldRecipient: oldRef?.owner?.address, newRecipient: newRef.owner?.address, newReceiverType: newRef.getType().identifier, newReceiverUUID: newRef.uuid)
emit ForwarderRecipientUpdated(owner: self.owner?.address, oldRecipient: self.recipient.address, newRecipient: newRecipient.address, newReceiverType: newRef.getType().identifier, newReceiverUUID: newRef.uuid)

Maybe it makes sense to use the capability's address field instead, so that the old address is still known, even when the underlying capability is no longer linked. Wdyt?

Copy link
Member Author

Choose a reason for hiding this comment

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

thats a good idea. I'll do that

joshuahannan and others added 3 commits March 11, 2026 14:43
Improves readability for multi-argument emit calls across all contracts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…event

Emit self.recipient.address instead of the optional vault owner address,
so oldRecipient is always non-nil even when the old capability is stale.
Update the test assertion to match the new behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regenerate assets.go to embed updated .cdc files after emit formatting changes.
Update CLAUDE.md and AGENTS.md with test requirements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@joshuahannan joshuahannan merged commit 8dae4c0 into master Mar 11, 2026
2 checks passed
@joshuahannan joshuahannan deleted the fix/token-forwarding-bugs branch March 11, 2026 20:00
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