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:
- Registers and approves an entity, which writes a one-year expiration timestamp into
EntityStatus.
- Advances local chain time beyond that expiration.
- Confirms the stored expiration is in the past while the raw status remains non-zero.
- Calls
productCreate() and productOfferFeature() as the expired entity owner.
- 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.
Summary
ImmutableEntitystores a one-year expiration timestamp inEntityStatuswhen an entity is approved, andentityUpgrade()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 usingExpirationMask() << ExpirationOffset(), butExpirationMaskis 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()writesblock.timestamp + 365 daysintoEntityStatus.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 anyentityAddressStatus(msg.sender) > 0.contracts/ImmutableProduct.sol:228-271:productOffer()accepts any non-zero status for commercial offers.contracts/ProductActivate.sol:130-147:activatePurchase()accepts anyentityIndexStatus(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()chargesUpgradeFeeand extends membership.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:
Command:
Fresh result:
The PoC:
EntityStatus.productCreate()andproductOfferFeature()as the expired entity owner.activatePurchase()from a buyer and confirms the buyer receives an activation sold by the expired seller.Additional PoC file:
Command:
Fresh result:
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
ImmutableSoft/ImmutableEcosystem67fc6b1601ef249e7cfa0f6d0b9591b51651d4050.8.14+commit.80d49f37client/src/contracts/ImmutableEntity.jsonincludes Polygon 137 address0xdd88f7448305079728c32ecaAFCA6a87166d52B6; public Polygon RPC shows code at that proxy and its EIP-1967 implementation slot points to0x4e3113E6DD6A7646aa8520905eC30A341D907B1d.Suggested fix
Use an expiry-aware status helper for all authorization-sensitive checks. For example, decode the expiration from
EntityStatus, return/revert as unvalidated whenexpiration <= 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 fixentityUpgrade()so it clearsExpirationMask()directly before writing the new shifted timestamp, and mask the shifted expiration value before OR-ing it intoEntityStatus.