Shyft Attestation Payment — Ground-Truth Audit

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

Answer

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.

The three payment modes (enum, real source)

ModeValueWhat the payer providesHow it's collected"SHIFT" mapping
NONE0 (default)nothing — no feefree attestations
BASE1native value (msg.value) ≥ pricepaymentReceiver.send(msg.value)native SHFT / ETH (comment: "L1 SHFT, Ether, etc.")
ERC202approve() ≥ price to the moduletransferFrom(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.

End-to-end flow (verified, with file:line)

TrustAnchorStorage.setAttestation(...) // payable ├─ L463 require verified Trust Anchor ├─ L465 GATE: paymentModuleAddress==0 OR hasAcceptableFundsForAttestation(TA) │ └─ module: BASE→ msg.value≥price | ERC20→ allowance(TA,module)≥price ├─ writes attestation to storage (createAttestation, L586+) ├─ L624 if a payment module is linked: │ L626 require( processPayment(msg.sender) == true , "could not process payment") │ └─ module: BASE→ receiver.send(msg.value) | ERC20→ transferFrom(TA,receiver,price) └─ if payment fails → whole tx REVERTS (attestation rolls back — atomic) Proof: test/27_test_attestationFee.js → link module, setPaymentMode(1)=BASE, set price+receiver, attestation FAILS w/o payment, SUCCEEDS with it.

Two payment paths in the codebase — don't confuse them

 Shyft‑native PaymentModule the answerRMT EAS ShyftGatedResolver in antilles‑v2
WhereShyft core repo (submodule), TrustAnchorStorage_PaymentModule.solantilles-v2/contracts/.../ShyftGatedResolver.sol (EAS)
Built onShyft‑native attestations (TrustAnchorStorage)Ethereum Attestation Service (EAS) resolver
Tokensnative (BASE) or any ERC20 (ERC20)single ERC20 only (rmtToken, ETH disabled)
Fee goes toconfigurable receiver (treasury)peer‑to‑peer: attester → cited bot
Statusimplemented + wired + tested; default OFFimplemented; defaults off (fee 0)
Multi‑token?one configured token/mode at a timeone 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.

To "execute on multi-token" (e.g. require a SHIFT/USDC fee)

Config only — minutes
  1. (multisig) TAS.setPaymentModuleAddress(module)
  2. module.setTrustAnchorStorageAddress(TAS)
  3. module.setPaymentTokenAddress(SHIFT/USDC)
  4. module.setPaymentReceiverAddress(treasury)
  5. module.setPaymentForAttestation(price)
  6. module.setPaymentMode(2) // ERC20
  7. payer (TA) token.approve(module, price)

Native instead? Use setPaymentMode(1) and pay via msg.value.

If you truly need many tokens at once

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."

Security findings on the real module (deploy-blockers to clear first)

SevFindingWhere
MEDERC20 uses transferFrom(...) == true; non-standard tokens (USDT returns no bool) break payment. Use SafeERC20-style handling or allowlist compliant tokens.PaymentModule L157-160
MEDBASE accepts msg.value ≥ price but forwards all of it; overpayment is not refunded.PaymentModule L106,150
MEDERC20 mode ignores forwarded msg.value → stray native sent in ERC20 mode is trapped (no sweep fn).PaymentModule L157-160
MEDCentralization: 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
LOWBASE .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
NOTEOnly 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

Ground-truth provenance & corrections

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.