ProductActivate lets buyers without the required Ricardian child agreement purchase and receive resales
Summary
ProductActivate.activatePurchase() validates that the supplied ricardianClients[i] hash is a descendant of the offer's configured Ricardian parent, but it does not verify that the purchaser owns that child agreement token. Any buyer can reuse an existing child hash and receive a Ricardian-gated activation without holding the required agreement.
For activations purchased through productOfferFeature() / productOfferLimitation(), the Ricardian requirement also is not preserved on the minted activation token, so a later resale can reach another buyer without the required child agreement.
Affected Code
contracts/ProductActivate.sol:167-174: checks creatorParentOf(ricardianClients[i], theOffer.ricardianParent) but not ownership by msg.sender.
contracts/ProductActivate.sol:212-215: only stores the parent on the activation when RicardianReqFlag is set in theOffer.value.
contracts/ImmutableProduct.sol:168-176 and contracts/ImmutableProduct.sol:206-214: offer builders accept requireRicardian but do not set RicardianReqFlag.
contracts/ActivateToken.sol:441-447: transfer enforcement depends on a stored TokenIdToRicardianParent[tokenId].
Impact
A Ricardian-required product offer is intended to gate activation purchase/transfer on ownership of a matching client agreement. A non-owner can bypass that gate by supplying another valid child hash during purchase. The resulting activation can also be resold to another non-owner because the Ricardian parent requirement is not retained for offer-purchased activations.
This is an authorization/terms-gating bypass for ProductActivate flows. I am not claiming direct fund theft.
Reproduction
Tested at commit:
67fc6b1601ef249e7cfa0f6d0b9591b51651d405
I added a focused PoC test:
test/ProductActivateRicardianOwnershipBypassPoC.js
Run:
npx truffle test test/ProductActivateRicardianOwnershipBypassPoC.js --migrate-none
Observed result:
Contract: ProductActivate Ricardian ownership bypass PoC
[PASS] lets buyers without the required Ricardian child purchase and receive resales (2160ms)
1 passing (2s)
The test asserts:
- the seller owns the child Ricardian agreement;
- the attacker and second buyer both have
creatorHasChildOf(..., parentHash) == 0;
- the seller creates an offer with
requireRicardian = parentHash;
- the attacker purchases by passing the seller-owned
childHash;
- the activation owner becomes the attacker;
- the attacker resells it and the second buyer receives it without owning a child agreement.
Expected Behavior
When theOffer.ricardianParent > 0, activatePurchase() should require that the purchaser owns a valid child of that parent, for example through creatorHasChildOf(msg.sender, theOffer.ricardianParent).
Offer-created activations should also retain the Ricardian parent requirement so transfer/resale enforcement has the same constraint.
Suggested Fix Direction
- In
activatePurchase(), check ownership for the caller, not only parent/child relation for a supplied hash.
- When
requireRicardian > 0, set or otherwise persist the Ricardian requirement on the minted activation so ActivateToken._beforeTokenTransfer() can enforce it for future transfers.
- Add tests for buyers who provide a valid child hash they do not own.
ProductActivate lets buyers without the required Ricardian child agreement purchase and receive resales
Summary
ProductActivate.activatePurchase()validates that the suppliedricardianClients[i]hash is a descendant of the offer's configured Ricardian parent, but it does not verify that the purchaser owns that child agreement token. Any buyer can reuse an existing child hash and receive a Ricardian-gated activation without holding the required agreement.For activations purchased through
productOfferFeature()/productOfferLimitation(), the Ricardian requirement also is not preserved on the minted activation token, so a later resale can reach another buyer without the required child agreement.Affected Code
contracts/ProductActivate.sol:167-174: checkscreatorParentOf(ricardianClients[i], theOffer.ricardianParent)but not ownership bymsg.sender.contracts/ProductActivate.sol:212-215: only stores the parent on the activation whenRicardianReqFlagis set intheOffer.value.contracts/ImmutableProduct.sol:168-176andcontracts/ImmutableProduct.sol:206-214: offer builders acceptrequireRicardianbut do not setRicardianReqFlag.contracts/ActivateToken.sol:441-447: transfer enforcement depends on a storedTokenIdToRicardianParent[tokenId].Impact
A Ricardian-required product offer is intended to gate activation purchase/transfer on ownership of a matching client agreement. A non-owner can bypass that gate by supplying another valid child hash during purchase. The resulting activation can also be resold to another non-owner because the Ricardian parent requirement is not retained for offer-purchased activations.
This is an authorization/terms-gating bypass for ProductActivate flows. I am not claiming direct fund theft.
Reproduction
Tested at commit:
I added a focused PoC test:
Run:
Observed result:
The test asserts:
creatorHasChildOf(..., parentHash) == 0;requireRicardian = parentHash;childHash;Expected Behavior
When
theOffer.ricardianParent > 0,activatePurchase()should require that the purchaser owns a valid child of that parent, for example throughcreatorHasChildOf(msg.sender, theOffer.ricardianParent).Offer-created activations should also retain the Ricardian parent requirement so transfer/resale enforcement has the same constraint.
Suggested Fix Direction
activatePurchase(), check ownership for the caller, not only parent/child relation for a supplied hash.requireRicardian > 0, set or otherwise persist the Ricardian requirement on the minted activation soActivateToken._beforeTokenTransfer()can enforce it for future transfers.