Can a payment be required when an attestation is processed — in SHFT / native or any ERC20? • 2026-06-24 • real source: ShyftNetwork/shyft_shyftcorecontracts @ master
YES — it already exists, is wired, and is tested. It just isn't turned on. The Shyft‑native attestation store (TrustAnchorStorage) has a purpose-built, attachable TrustAnchorStorage_PaymentModule that requires a fee to create an attestation in either the native coin (SHFT/ETH) or any ERC20 you choose. Enabling it is configuration only — no new contract needed. Default mode is NONE (off), which is why "we haven't used it."
Converged 3/3: Claude (line-by-line read) · Codex (independent audit) · Gemini (capability confirm). Scope excludes Antilles trust-channel payments, as instructed.
| Mode | Value | What the payer provides | How it's collected | "SHIFT" mapping |
|---|---|---|---|---|
| NONE | 0 (default) | nothing — no fee | — | free attestations |
| BASE | 1 | native value (msg.value) ≥ price | paymentReceiver.send(msg.value) | native SHFT / ETH (comment: "L1 SHFT, Ether, etc.") |
| ERC20 | 2 | approve() ≥ price to the module | transferFrom(payer, receiver, price) | a SHIFT ERC20, USDC, USDT, any token |
Owner setters: setPaymentMode · setPaymentTokenAddress · setPaymentForAttestation · setPaymentReceiverAddress. One mode + one token + one price + one receiver active at a time.
| Shyft‑native PaymentModule the answer | RMT EAS ShyftGatedResolver in antilles‑v2 | |
|---|---|---|
| Where | Shyft core repo (submodule), TrustAnchorStorage_PaymentModule.sol | antilles-v2/contracts/.../ShyftGatedResolver.sol (EAS) |
| Built on | Shyft‑native attestations (TrustAnchorStorage) | Ethereum Attestation Service (EAS) resolver |
| Tokens | native (BASE) or any ERC20 (ERC20) | single ERC20 only (rmtToken, ETH disabled) |
| Fee goes to | configurable receiver (treasury) | peer‑to‑peer: attester → cited bot |
| Status | implemented + wired + tested; default OFF | implemented; defaults off (fee 0) |
| Multi‑token? | one configured token/mode at a time | one swappable token |
The user's question is about the left column (Shyft‑native). The right is a separate, already-real per-citation fee — useful as a fallback pattern, not the target.
TAS.setPaymentModuleAddress(module)module.setTrustAnchorStorageAddress(TAS)module.setPaymentTokenAddress(SHIFT/USDC)module.setPaymentReceiverAddress(treasury)module.setPaymentForAttestation(price)module.setPaymentMode(2) // ERC20token.approve(module, price)Native instead? Use setPaymentMode(1) and pay via msg.value.
The single-config design means simultaneous multi-token = deploy multiple module instances or a thin extension (a mapping(token⇒price) allowlist). Small, well-scoped — not greenfield. Only do this if the product actually needs payer's-choice-of-token; otherwise configuration covers "require token X."
| Sev | Finding | Where |
|---|---|---|
| MED | ERC20 uses transferFrom(...) == true; non-standard tokens (USDT returns no bool) break payment. Use SafeERC20-style handling or allowlist compliant tokens. | PaymentModule L157-160 |
| MED | BASE accepts msg.value ≥ price but forwards all of it; overpayment is not refunded. | PaymentModule L106,150 |
| MED | ERC20 mode ignores forwarded msg.value → stray native sent in ERC20 mode is trapped (no sweep fn). | PaymentModule L157-160 |
| MED | Centralization: only the module address is multisig-gated on TAS; the module owner can change token/price/receiver after linking. | PM L59-88 vs TAS L309 |
| LOW* | No ReentrancyGuard (Sol 0.7.1). ERC20 transferFrom runs after state writes; only safe because the owner picks a trusted token. Allowlist the token. | PM L144-167 |
| LOW | BASE .send() (2300 gas) returns false → TAS L626 require reverts the attestation if the receiver is a costly contract. Use an EOA/simple receiver. | PM L150 / TAS L626 |
| NOTE | Only BASE mode is exercised in the test; ERC20 path is implemented but not covered by 27_test_attestationFee.js — add an ERC20 test before relying on it. | test/27 |
setDataRetrievalRevenueShare (a revenue split) and no payment module. The module is real in source; the docs just don't cover it (same gap pattern as MachineConsentHelper).payForAttestation/setPaymentAmount/refundPayment (none exist) and mis-stated the enum. Real entrypoints: hasAcceptableFundsForAttestation + processPayment; real enum NONE/BASE/ERC20. No refund function exists.ls (canonical-source-verification rule). Final findings rest only on files actually read.Audit by non-primary coordinator slug, parallel to the primary's antilles-v2 stable-testnet-deploy-prep work (left untouched, read-only). Models: Claude Opus 4.8 + Codex + Gemini 3.1-pro on real source. No hallucinations carried forward — every file:line verified.