Fix small bugs in TokenForwarding for bricked forwarder and wrong getSupportedVaultTypes#185
Merged
joshuahannan merged 9 commits intomasterfrom Mar 11, 2026
Merged
Conversation
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>
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>
- 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>
turbolent
approved these changes
Mar 11, 2026
| // 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) |
Member
There was a problem hiding this comment.
Maybe make this more readable by choping down the long argument list so each argument is on separate line
jribbink
reviewed
Mar 11, 2026
| // 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) |
There was a problem hiding this comment.
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?
Member
Author
There was a problem hiding this comment.
thats a good idea. I'll do that
jribbink
approved these changes
Mar 11, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
CLAUDE.mdandAGENTS.mdfor this repochangeRecipientpermanently bricks forwarder on stale old capability: The oldself.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 sochangeRecipientalways succeeds regardless of the old cap's state.getSupportedVaultTypesreturns 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 usingisSupportedVaultTypeon a chained forwarder would incorrectly getfalse. Now delegates tovaultRef.getSupportedVaultTypes()to correctly propagate the supported types.Test plan
changeRecipientsucceeds when the old recipient capability is stale (recipient deleted vault / revoked cap)ForwarderRecipientUpdatedevent is emitted witholdRecipient: nilwhen old cap is stalegetSupportedVaultTypes()on aForwarderreturns the underlying vault type (e.g.ExampleToken.Vault), notTokenForwarding.ForwardergetSupportedVaultTypes()correctly propagates through chained forwarders🤖 Generated with Claude Code