Skip to content

ProductActivate lets buyers without the required Ricardian child agreement purchase and receive resales #30

@Shawnleeeeee

Description

@Shawnleeeeee

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions