Skip to content

docs: add DApp security notes#99

Open
skyc1e wants to merge 2 commits into
circlefin:mainfrom
skyc1e:docs/safe-svg-tokenuri-rendering
Open

docs: add DApp security notes#99
skyc1e wants to merge 2 commits into
circlefin:mainfrom
skyc1e:docs/safe-svg-tokenuri-rendering

Conversation

@skyc1e
Copy link
Copy Markdown

@skyc1e skyc1e commented May 27, 2026

Adds a small DApp security page and links it from the README.

It covers two things Arc app developers can easily get wrong:

  • rendering on-chain SVG metadata from tokenURI() without injecting untrusted SVG into the page DOM
  • payment escrow contract basics: bounded release delays, a payee recovery path, fee snapshots at funding time, and two-step ownership transfers

Closes #85
Closes #86

Checked with git diff --check.

@skyc1e skyc1e marked this pull request as ready for review May 27, 2026 22:49
@skyc1e skyc1e changed the title docs: add safe SVG tokenURI rendering note docs: add DApp security notes May 27, 2026
@osr21
Copy link
Copy Markdown

osr21 commented Jun 2, 2026

Good additions. One more security finding that's Arc-specific and might be worth including in the DApp security page, especially since ERC-4337 gasless flows are a natural fit for Arc's USDC-native model.


ERC-4337 Paymaster: nonReentrant on validatePaymasterUserOp violates ERC-7562 and gets UserOps silently dropped

If you build a USDC-funded Paymaster for gasless transactions on Arc, adding the common nonReentrant modifier to validatePaymasterUserOp causes all UserOperations to be silently rejected by ERC-7562-compliant bundlers (Pimlico, Alchemy) during simulation — with no on-chain revert, no useful error message.

The reason: nonReentrant writes to a global _locked boolean. ERC-7562 forbids unstaked paymasters from writing to global storage during validation — only sender-keyed storage is allowed. The bundler catches this at simulation and drops the UserOp before it ever reaches the chain.

// ❌ Rejected by bundler — _locked is global storage
function validatePaymasterUserOp(...) external nonReentrant onlyEntryPoint { ... }

// ✅ Correct — onlyEntryPoint is sufficient; EntryPoint never re-enters validation
function validatePaymasterUserOp(...) external onlyEntryPoint { ... }

nonReentrant is safe (and recommended) on user-callable functions like deposit() and withdraw() — just not on validatePaymasterUserOp.

A companion invariant: if your paymaster tracks both a balances[user] and a locked[user] reservation for in-flight UserOps, the deductGas (legacy path) check must test available = balance − locked, not the total balance. Testing the total allows a concurrent pending UserOp and a legacy deduction to both succeed against the same funds.

Reference: ERC-7562 validation scope rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants