Burning a Ricardian parent release can block resale of existing Ricardian-required activations
Summary
For manually created Ricardian-required activations, ActivateToken stores a Ricardian parent hash and later calls CreatorToken.creatorHasChildOf(to, parentHash) during third-party transfers/resales.
If the product creator burns the parent CreatorToken after issuing activations, CreatorToken._burn clears the global HashToRelease[parentHash] lookup. After that, creatorHasChildOf reverts with EntityID zero, so the official ProductActivate.activateTransfer resale path fails even when the resale buyer owns a valid child agreement token.
This is a conservative resale-rights / official resale DoS. I am not claiming direct fund theft or a total ERC721 lock, because the activation owner can still perform owner-initiated direct transfers. The affected path is the project-managed resale/payment flow and Ricardian-gated third-party transfer.
Affected code
CreatorToken._burn clears HashToRelease[Releases[tokenId].hash] and release metadata.
CreatorToken.creatorHasChildOf depends on creatorReleaseHashDetails(parentHash) resolving a nonzero parent entity.
ActivateToken._beforeTokenTransfer calls creatorHasChildOf(to, TokenIdToRicardianParent[tokenId]) for non-owner transfers.
ProductActivate.activateTransfer uses ActivateToken.safeTransferFrom, so the resale flow reaches that hook.
Impact
A validated seller/creator can issue a Ricardian-required activation, transfer it to a buyer, then burn the parent release. Existing buyers who list the activation for resale can no longer complete the official resale to a qualified buyer, even if that buyer owns a valid child agreement token.
The PoC shows:
- both the original buyer and the resale buyer own valid child agreements before the burn;
- the activation is created with
RicardianReqFlag and the parent hash;
- the buyer lists and approves the activation for resale;
- after the seller burns the parent release,
creatorHasChildOf(qualifiedBuyer, parentHash) reverts;
activateTransfer also reverts, leaving the activation listed but unsold.
Reproduction
PoC file:
test/CreatorTokenBurnedRicardianParentResaleDoSPoC.js
Command:
npx truffle test test\CreatorTokenBurnedRicardianParentResaleDoSPoC.js --migrate-none
Result:
Contract: CreatorToken burned Ricardian parent resale DoS PoC
PASS lets the creator burn the Ricardian parent and block resale to a qualified buyer (2527ms)
1 passing (3s)
Why this is not just seller self-harm
The seller controls the parent release, but the denial happens after activation rights are already held by a buyer. The buyer's official resale/payment flow is broken by later global lookup deletion, even though the resale recipient satisfies the child-agreement ownership condition before the burn.
This differs from normal creator-controlled metadata changes because the activation token keeps referencing the parent hash as an authorization dependency, while CreatorToken._burn removes the lookup needed to evaluate that dependency for already-issued activations.
Suggested fix
Do not delete Ricardian relationship metadata that active activations may depend on. Possible fixes include:
- preserve enough tombstone metadata after burn for
creatorHasChildOf / creatorParentOf to keep validating existing child relationships;
- prohibit burning a parent release that has child releases;
- store immutable parent relationship data for Ricardian-required activations so later release burns cannot invalidate existing activation transfer checks.
Burning a Ricardian parent release can block resale of existing Ricardian-required activations
Summary
For manually created Ricardian-required activations,
ActivateTokenstores a Ricardian parent hash and later callsCreatorToken.creatorHasChildOf(to, parentHash)during third-party transfers/resales.If the product creator burns the parent CreatorToken after issuing activations,
CreatorToken._burnclears the globalHashToRelease[parentHash]lookup. After that,creatorHasChildOfreverts withEntityID zero, so the officialProductActivate.activateTransferresale path fails even when the resale buyer owns a valid child agreement token.This is a conservative resale-rights / official resale DoS. I am not claiming direct fund theft or a total ERC721 lock, because the activation owner can still perform owner-initiated direct transfers. The affected path is the project-managed resale/payment flow and Ricardian-gated third-party transfer.
Affected code
CreatorToken._burnclearsHashToRelease[Releases[tokenId].hash]and release metadata.CreatorToken.creatorHasChildOfdepends oncreatorReleaseHashDetails(parentHash)resolving a nonzero parent entity.ActivateToken._beforeTokenTransfercallscreatorHasChildOf(to, TokenIdToRicardianParent[tokenId])for non-owner transfers.ProductActivate.activateTransferusesActivateToken.safeTransferFrom, so the resale flow reaches that hook.Impact
A validated seller/creator can issue a Ricardian-required activation, transfer it to a buyer, then burn the parent release. Existing buyers who list the activation for resale can no longer complete the official resale to a qualified buyer, even if that buyer owns a valid child agreement token.
The PoC shows:
RicardianReqFlagand the parent hash;creatorHasChildOf(qualifiedBuyer, parentHash)reverts;activateTransferalso reverts, leaving the activation listed but unsold.Reproduction
PoC file:
Command:
Result:
Why this is not just seller self-harm
The seller controls the parent release, but the denial happens after activation rights are already held by a buyer. The buyer's official resale/payment flow is broken by later global lookup deletion, even though the resale recipient satisfies the child-agreement ownership condition before the burn.
This differs from normal creator-controlled metadata changes because the activation token keeps referencing the parent hash as an authorization dependency, while
CreatorToken._burnremoves the lookup needed to evaluate that dependency for already-issued activations.Suggested fix
Do not delete Ricardian relationship metadata that active activations may depend on. Possible fixes include:
creatorHasChildOf/creatorParentOfto keep validating existing child relationships;