Appearance
Trust & security
The honest inventory of what you are trusting, what you are not, and what the contracts actually enforce. Nothing on this page is hedged.
What requires trust
These are real trust assumptions. There is no cryptographic construction that removes them, and no wording that makes them smaller than they are.
Custody of the physical item
A CS2 skin lives in Valve's database. No blockchain can hold one. Somebody has to operate the Steam accounts that hold deposited items, and that party can be compromised, compelled, or dishonest. If the items are lost, your token is a claim on something that is gone.
This is the foundational assumption of the entire product. Everything else on this page is smaller than it.
Mint authority
An allow-listed minter can create tokens. The contract validates the category byte and rejects duplicate ids — it does not, and cannot, validate that a real skin backs a new token. A compromised or dishonest minter can create tokens backed by nothing, diluting the redeemability of tokens that are backed.
What the minter role cannot do: move, freeze, or burn a token that already exists. Those require the token's owner.
Metadata hosting
tokenURI points at an HTTP base URI. Float, pattern, stickers, and images are served from a host the contract owner controls. That host can change what it serves or stop serving. Metadata is not on chain and is not signed.
Facts you can verify without trusting it: who owns a token (ownerOf), its category (categoryOf, a pure bit-shift), and whether it still exists. See Metadata.
The binding between token and item
"Token 42 is backed by that AK" is an operator assertion, recorded off-chain. The chain does not know what a skin is. The uniqueness constraint on the item fingerprint makes it impossible to tokenize the same item twice — it does not prove that any given item exists.
What does not require trust
Enforced by contract code on chain. LootFi cannot override these, and neither can anyone else.
| Guarantee | Enforced by |
|---|---|
| Your token is yours | ERC-721 ownership. There is no admin transfer, no clawback, no freeze function in the contract. |
| You can transfer it | transferFrom / safeTransferFrom. No allow-list of recipients, no operator permission needed. |
| A sale settles atomically | fulfillOrder: payment splits and the NFT moves in one transaction, or the whole thing reverts. |
| The operator never holds your funds | No escrow. Payment goes buyer → royalty/fee/seller directly, in the fill transaction. |
| The operator never holds your token while listed | No escrow. The token stays in your wallet until a fill. |
| You can cancel your own listings | cancel(nonce) and setMinimumNonce, keyed by msg.sender. Nobody can cancel for you; nobody can stop you. |
| Nobody else can cancel your listings | Same — the mapping is keyed by msg.sender; there is no parameter to target another maker. |
| Only you can burn your token | deTokenize requires direct ownership. Approved operators are rejected. |
| A token id is never reissued | _mint reverts on a duplicate id. |
| A category can never be changed | It is bits in the token id. No function writes it. |
Contract-level protections in the deployed code
LootMarket
| Protection | Mechanism |
|---|---|
| Reentrancy | nonReentrant guard, plus strict CEI: the nonce is marked used before any external call, and the NFT transfer is last |
| Order replay | Per-maker nonce marked used at fill (usedOrCancelled[maker][nonce]) |
| Cross-chain replay | chainId in the EIP-712 domain, read from block.chainid at fill time |
| Cross-contract replay | verifyingContract in the domain — a signature for one market cannot be used on another |
| Nonce scoping | Per maker. Your nonce space is yours; no global counter, no interference |
| Bulk invalidation | setMinimumNonce retires every order below a watermark in one transaction |
| Arbitrary collections | Collection allow-list, owner-controlled |
| Arbitrary payment tokens | Currency allow-list; native ETH is address(0) and always accepted |
| Fee inflation | MAX_PLATFORM_FEE_BPS = 1000 — a hard 10% cap as a compile-time constant. setPlatformFee reverts above it |
| Seller shortfall | Royalty + fee exceeding the price reverts with FeeExceedsPrice before any payout |
| Stale ownership | ownerOf re-read at fill; a maker who no longer owns the token cannot have their order filled |
| Missing approval | Approval re-checked at fill |
| Non-standard ERC-20s | SafeERC20 on every token movement |
| Failed payouts | Explicit reverts (PaymentFailed, RefundFailed) — value is never silently retained |
| Emergency stop | whenNotPaused on fills; owner-controlled |
LootSkins
| Protection | Mechanism |
|---|---|
| Unauthorised minting | onlyMinter allow-list, separate from ownership |
| Invalid token ids | Category byte ≥ 8 reverts with InvalidCategory on both mint and categoryOf |
| Duplicate ids | _mint reverts |
| Unauthorised burns | deTokenize requires direct ownership; operator approval deliberately rejected |
| Batch abuse | MAX_BATCH = 50 on both mintBatch and deTokenize; empty batches revert |
| Underpaid withdraw fee | InsufficientFee(required, provided) |
| Burn attribution | Detokenized.owner is msg.sender, which — because only the direct owner can burn — is always the true owner |
| Log recoverability | Detokenized.tokenIds is in the data field, not indexed, so ids stay decodable |
Admin powers and blast radius
Both contracts are UUPS-upgradeable and owner-administered. Concretely, the owner can:
- Upgrade either implementation.
- Add and remove minters (
updateMinter). - Change the metadata base URI (
updateBaseURI). - Change the default royalty receiver and rate (
setDefaultRoyalty). - Change the withdraw fee (
setWithdrawFee) and collect accrued fees. - Change the platform fee, up to the 10% cap, and the fee recipient.
- Change the collection and currency allow-lists.
- Pause and unpause marketplace fills.
Full detail, and the current holder of each role, on Admin & upgrades.
Ownership and the minter set are both transferable (transferOwnership, updateMinter), which is what makes moving to a multisig possible without redeploying.
The operator-approval consequence, stated honestly
To list gaslessly, a seller grants setApprovalForAll to the market contract. That approval persists until revoked.
An upgrade of the market contract could therefore move approved tokens. A new implementation runs at the same address, and every approval already granted to that address applies to it. Nothing about "the code you approved" is pinned.
This is not specific to LootFi — it is the standard tradeoff behind every gasless-listing marketplace, and the reason approvals are worth understanding rather than clicking through. It is also the strongest argument for moving ownership to a multisig, which is the stated intent.
What you can do about it right now:
- Revoke when you are done selling.
setApprovalForAll(market, false)costs one transaction and removes the exposure entirely. Your tokens stay yours; only listing stops working until you re-approve. - Approve per token instead of for all.
approve(market, tokenId)grants the market rights to exactly one token. More transactions, much smaller surface. - Watch the owner address. Ownership transfers and upgrades are on-chain events. You can monitor them without asking anyone's permission.
Things worth knowing that are not vulnerabilities
- Listings do not escrow. A listed token can be transferred or burned by its owner. The listing then simply cannot fill. This is by design and is why listing is free.
- Off-chain order status is advisory. A listing marked invalid off-chain can still fill on chain if the invalidating condition was reversed. The contract decides; LootFi's records only describe.
- Cancellation costs gas. It is an on-chain transaction because it has to be — a cancellation that existed only in LootFi's records would not stop a buyer who already holds your signature.
- Smart-contract wallets cannot list. Signature verification is
ECDSA.recoverwith no ERC-1271 fallback. Contract wallets can buy, hold, transfer, and burn — they cannot be makers. See Marketplace. - Metadata can change after minting. Stat enrichment fills in float, pattern and stickers as they resolve. The item identity never changes. See Metadata.
- A returned skin has its own Steam hold. Valve's rule about a freshly traded item, not a LootFi policy.
Responsible disclosure
If you have found a security issue in the contracts, the backend, or the frontend, please report it privately first. Public disclosure of an unpatched issue in a system holding other people's property puts those people at risk, and there is no upside to it that a coordinated disclosure does not also provide.
Contact the team through the official channels linked from the LootFi application. Please include:
- What you found, and the impact if exploited.
- Reproduction steps, and a transaction hash if the issue is on chain.
- Whether anything has been disclosed elsewhere, and any timeline you intend to hold us to.
Please do not test against other people's tokens, listings, or deposits. If a proof of concept requires touching live state, use your own.
Related
- Risks & limitations — the same ground, without the contract detail.
- Admin & upgrades — every admin function and who holds it.
- Architecture — where the trust boundary sits.
- Custody model — what a token is a claim on.