Skip to content

Expired entities keep validated product and sales privileges #34

@Shawnleeeeee

Description

@Shawnleeeeee

Summary

ImmutableEntity stores a one-year expiration timestamp in EntityStatus when an entity is approved, and entityUpgrade() is documented as the membership extension path. However, the authorization helpers used by product creation, offer creation, activation purchase, and payment forwarding only check whether the raw status is non-zero. They never check whether the embedded expiration has passed.

This lets a previously approved entity keep validated commercial privileges after its approval window has expired. In a focused Truffle PoC, an expired seller still creates a product, creates an activation offer, and sells an activation to a buyer.

The same membership/expiry control also appears in entityUpgrade(): the function attempts to clear the old expiration using ExpirationMask() << ExpirationOffset(), but ExpirationMask is already shifted. A second focused PoC shows normal paid upgrades can inflate the stored expiration beyond the one-year-per-payment schedule.

Affected code

  • contracts/ImmutableEntity.sol:127-137: entityStatusUpdate() writes block.timestamp + 365 days into EntityStatus.
  • contracts/ImmutableEntity.sol:330-370: entityUpgrade() treats the timestamp as membership expiry/extension state and line 366 does not clear the existing expiration bits correctly.
  • contracts/ImmutableEntity.sol:406-424: entityIndexStatus() / entityAddressStatus() return raw non-zero status without checking expiry.
  • contracts/ImmutableProduct.sol:101-113: productCreate() accepts any entityAddressStatus(msg.sender) > 0.
  • contracts/ImmutableProduct.sol:228-271: productOffer() accepts any non-zero status for commercial offers.
  • contracts/ProductActivate.sol:130-147: activatePurchase() accepts any entityIndexStatus(entityIndex) > 0.
  • contracts/ProductActivate.sol:224-253: ETH purchases forward funds to the entity through the same non-expiry-aware status checks.

Why this matters

The documented/product behavior treats entity approval as a time-bounded membership:

  • entityStatusUpdate() writes a one-year timestamp.
  • entityUpgrade() charges UpgradeFee and extends membership.
  • The README describes entity registration/activation and product activation sales as normal ecosystem flows.

But downstream authorization gates only check status > 0. Therefore, after the first legitimate approval, the entity can avoid renewal and continue creating products/offers and receiving activation-sale payments.

This report does not claim direct theft of existing user funds. The impact is an authorization and membership/accounting bypass for validated seller privileges, plus an overextension bug in the paid membership renewal path.

Reproduction

PoC file:

test/ImmutableEntityExpiredStatusBypassPoC.js

Command:

npx truffle test test/ImmutableEntityExpiredStatusBypassPoC.js --migrate-none

Fresh result:

Contract: ImmutableEntity expired status bypass PoC
  1 passing

The PoC:

  1. Registers and approves an entity, which writes a one-year expiration timestamp into EntityStatus.
  2. Advances local chain time beyond that expiration.
  3. Confirms the stored expiration is in the past while the raw status remains non-zero.
  4. Calls productCreate() and productOfferFeature() as the expired entity owner.
  5. Calls activatePurchase() from a buyer and confirms the buyer receives an activation sold by the expired seller.

Additional PoC file:

test/ImmutableEntityUpgradeExpirationOrPoC.js

Command:

npx truffle test test/ImmutableEntityUpgradeExpirationOrPoC.js --migrate-none

Fresh result:

Contract: ImmutableEntity upgrade expiration OR PoC
  1 passing

This second PoC approves an entity, performs six ordinary paid entityUpgrade() calls, and confirms the decoded expiration becomes more than one extra year beyond the intended six annual extensions.

Environment

  • Repository: ImmutableSoft/ImmutableEcosystem
  • Commit tested: 67fc6b1601ef249e7cfa0f6d0b9591b51651d405
  • Compiler used by test/artifacts: 0.8.14+commit.80d49f37
  • Production artifact evidence: client/src/contracts/ImmutableEntity.json includes Polygon 137 address 0xdd88f7448305079728c32ecaAFCA6a87166d52B6; public Polygon RPC shows code at that proxy and its EIP-1967 implementation slot points to 0x4e3113E6DD6A7646aa8520905eC30A341D907B1d.

Suggested fix

Use an expiry-aware status helper for all authorization-sensitive checks. For example, decode the expiration from EntityStatus, return/revert as unvalidated when expiration <= block.timestamp, and update callers such as product creation, offer creation, purchases, entity bank changes, entity moves, and payment forwarding to use that helper. Also fix entityUpgrade() so it clears ExpirationMask() directly before writing the new shifted timestamp, and mask the shifted expiration value before OR-ing it into EntityStatus.

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