# Flare Dapp Pitfalls

> >

- **Type:** Skill
- **Install:** `agentstack add skill-thanasimos-thanas-flare-builders-toolkit-flare-dapp-pitfalls`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Thanasimos](https://agentstack.voostack.com/s/thanasimos)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Thanasimos](https://github.com/Thanasimos)
- **Source:** https://github.com/Thanasimos/Thanas-flare-builders-toolkit/tree/main/flare-dapp-pitfalls

## Install

```sh
agentstack add skill-thanasimos-thanas-flare-builders-toolkit-flare-dapp-pitfalls
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Flare dApp Pitfalls — Cross-Project Lessons

This skill captures **gotchas that are NOT in `enosys-contracts`, `enosys-integrations`,
`enosys-dex-v3`, or `solidity`** but cost real time on Flare-family work. Most are
counterintuitive, RPC-/wallet-/gas-shaped, or specific to Flare's FTSO reward
infrastructure.

If something contradicts the existing Enosys skills, the override is documented inline
with the reason — Enosys skills describe what their own private frontends use; this
skill describes what works for **third-party builders** with no privileged RPC access.

---

## 1. Wallet & RainbowKit setup

### Do NOT use RainbowKit's `metaMaskWallet` SDK connector on desktop

This is a **direct correction** to the `enosys-integrations` skill, which lists
`metaMaskWallet` from `@rainbow-me/rainbowkit/wallets` in its recommended set.

`metaMaskWallet` uses the MetaMask SDK, which races a mobile deep-link flow against
extension detection on desktop. It frequently hangs on "Opening MetaMask…" — a
~15-30s blocking dead state where the user can't tell if anything's happening.

**Use `injectedWallet` instead** — RainbowKit renders it as **"Browser Wallet"** in the
picker. It works seamlessly with the MetaMask extension, Rabby, and Brave Wallet
(any EIP-1193 injected provider). For mobile MetaMask users, `walletConnectWallet`
covers them via QR pairing — no SDK connector needed.

```typescript
import {
  injectedWallet,        // ← use this, not metaMaskWallet
  rainbowWallet,
  coinbaseWallet,
  walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";

const connectors = connectorsForWallets(
  [
    {
      groupName: "Recommended",
      wallets: [injectedWallet, rainbowWallet, coinbaseWallet, walletConnectWallet],
    },
  ],
  { appName: "Your dApp", projectId: WALLETCONNECT_PROJECT_ID }
);
```

Enosys's own frontends can use `metaMaskWallet` because they're served from a curated
domain set with their own RPC infrastructure; outside that, the MetaMask SDK is
the reason "MetaMask doesn't open" support tickets pile up.

### Wagmi button "dead zone" — track BOTH `isPending` AND `isLoading`

`useWriteContract().isPending` = waiting for wallet to sign (0.5–2s).
`useWaitForTransactionReceipt().isLoading` = tx is in mempool / mining.

Most tutorials only check `isLoading`, which means there's a 0.5–2s dead zone where
the user has clicked the button but MetaMask hasn't opened yet, and the button still
says "Place order". Three-state pattern:

```tsx
const { writeContract, isPending, data: txHash } = useWriteContract();
const { isLoading: isMining, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });

const label = isPending ? "Confirm in wallet…"
            : isMining  ? "Placing order…"
            :             "Place order";
```

Apply this to every write-action button.

---

## 2. RPC endpoints — browser vs server

### Public Flare RPCs cap `eth_getLogs` at 30 blocks

| Chain | Public RPC | `eth_getLogs` cap |
|---|---|---|
| Flare | `https://flare-api.flare.network/ext/C/rpc` | **30 blocks** |
| Songbird | `https://songbird-api.flare.network/ext/C/rpc` | **30 blocks** |
| Coston2 | `https://coston2-api.flare.network/ext/C/rpc` | **30 blocks** |

If your frontend depends on historical log scans (e.g. "find all my orders"), the
public RPC will throw `block range too wide`. Either:

- Use **Multicall3 + per-id pairwise reads** instead of log scans (e.g.
  `factory.getPool(t0, t1, fee)` over a curated token list).
- Use **Ankr** (paid tier) — lifts to ~5,000 blocks per request.
- Run a **self-hosted node**.

Plan for the public-RPC `eth_getLogs` cap from day one — assume 30 blocks is your
default and design history-dependent features around that.

### Ankr's API key is origin-locked for browser safety

The Ankr key in `NEXT_PUBLIC_ANKR_KEY` is **domain-locked** to the production hostname
(plus `localhost:3000` for dev). This is correct browser hygiene — anyone can read
the key from your build output, so origin lock prevents abuse.

Side effect: server-side calls (curl, `forge script`, indexers, executor bots) will
hit `Origin not allowed (-32079)`. **Generate a separate IP-locked Ankr key** for
server use; don't try to share the browser key.

### Multicall3 canonical address — same on all three Flare chains

```
0xcA11bde05977b3631167028862bE2a173976CA11
```

Live on Flare (14), Songbird (19), and Coston2 (114). Any dApp doing batched reads
should use it instead of N parallel `eth_call`s.

`aggregate3(allowFailure=true)` is the workhorse — lets one call fail without
killing the batch. **Use it for permissionless writes too**, but only when the target
function doesn't gate on `msg.sender`. Functions that credit `msg.sender` (executor
shares, etc.) cannot be multicalled because Multicall3 becomes the apparent caller.

### EIP-3855 (PUSH0) warning on Coston2 is cosmetic

Forge will print:
```
Warning: EIP-3855 is not supported in one or more of the RPCs used.
Unsupported Chain IDs: 114.
```

Deployments still succeed. **Don't downgrade `solc` to 0.8.19** to silence it —
0.8.20+ targets PUSH0 and Coston2 handles it fine in practice; the RPC just
doesn't advertise the EIP.

---

## 3. Gas: EIP-150 63/64 rule + `try/catch`

The single biggest source of "looks-successful but did nothing" bugs on Flare.

**The rule**: when contract A calls contract B, EVM forwards `min(gasleft, callerLimit)`
where `callerLimit = floor(63/64 × gasleft)`. The remaining 1/64 stays with A.

**Why it bites with `try/catch`**: many Flare contracts wrap external calls in
`try/catch` with a **reserved inner gas budget** (e.g. `B.foo{gas: 10_000_000}()`).
The `gas:` clause is a *ceiling*; the actual forwarded amount is
`min(10_000_000, 63/64 × gasleft)`. If the OUTER tx didn't have enough gas to forward
10M, the inner call gets less, OOGs silently, the `try/catch` swallows the revert,
and the outer tx succeeds with no observable effect.

**MetaMask and `eth_estimateGas` cannot trace through `try/catch`** — the estimator
sees the outer revert path of "did anything", not the inner OOG, so wallets routinely
under-estimate. Your transactions confirm but pay zero out.

**Fix**: pin outer gas explicitly when wrapping a call with a reserved inner budget.
Rule of thumb: `outer ≥ ceil(inner × 64/63) + outer_overhead_estimate`. For a 10M
inner cap, pin **at least 12M** outer — use 14M to be safe.

```typescript
// Wagmi pattern: pin gas on the write to escape estimateGas under-budgeting
writeContract({
  address: lom,
  abi,
  functionName: "claimDelegationRewards",
  args: [orderId],
  gas: 12_000_000n,   // ← outer pin; protects the 10M REWARD_CLAIM_GAS_CAP forward
});
```

Anywhere you see a contract pattern like:

```solidity
try IExternal(target).foo{gas: INNER_CAP}() returns (...) { ... }
catch { /* swallow */ }
```

assume estimateGas will under-budget the outer call. Always pin gas on the frontend.

### WNAT's transfer hooks are invisible to `eth_estimateGas`

A related variant that doesn't need any `try/catch` to bite you. WNAT (`WFLR`,
`WSGB`, `WC2FLR`) fires per-transfer vote-power hooks: every `transfer` and
`transferFrom` calls `updateAtTokenTransfer` on each registered delegate /
governance contract to refresh checkpoints. Those hooks consume real gas — often
75k-200k per hook — and their cost depends on the recipient's delegation graph,
which `eth_estimateGas` can't see through.

Symptom: a contract whose execution path ends with `WNAT.transfer(user, amount)`
runs cleanly through all its accounting and external calls, then OOGs inside the
final WNAT transfer. The 63/64 rule kicks in at the tail: by the time the outer
call has spent most of the budget on the upstream work, only a sliver is
forwarded into WNAT's hooks. The first hook clears, the second OOGs, the whole
tx reverts with no useful reason. Wallets under-estimate because the hook gas
isn't reachable from a standard simulation.

This matters anywhere your contract pays the user in wrapped-native at the end
of a multi-step flow — vault redemptions, claim-and-forward patterns, batch
exits that ultimately settle in WNAT. The fix is the same as the `try/catch`
case: **pin outer gas on the frontend**. Default to 10M-12M on Flare/Songbird.

```typescript
writeContract({
  address: vault,
  abi,
  functionName: "withdraw",
  args: [assets, receiver, owner],
  gas: 12_000_000n,   // ← absorbs WNAT vote-power hooks at the tail
});
```

General rule: when pinning gas for any contract write, lean high. The downside
of pinning too high is "wallet shows a slightly scary number"; the downside of
pinning too low is "tx silently OOGs, user pays gas anyway, you ship a hotfix."

### `block.basefee` is essentially zero on cheap Flare-family chains

Coston2 basefee = 1 wei. Songbird basefee = 2 wei. Flare basefee ≈ 25 gwei.

If you're building anti-spam economics that gate on
`required = basefee × gasEstimate × multiplier`, the gate **collapses to zero on the
cheap chains** unless you floor `basefee` at something like 25 gwei. Treat
`max(block.basefee, MIN_BASEFEE)` as the effective basefee, with `MIN_BASEFEE`
configurable but capped (don't let owner set it to 1000 ETH and brick the system).

---

## 4. WNAT, FTSO, and Enosys's redistributor naming churn

WNAT (wrapped FLR / SGB / C2FLR) holders earn **FTSO delegation rewards** and
historically **FlareDrops**. Pools, vaults, and limit-order contracts that hold WNAT
need to harvest these on behalf of users.

### `vm.deal()` corrupts WNAT vote-power state in fork tests

WNAT is not a vanilla ERC-20 — it has FTSO vote-power checkpoints. `vm.deal(addr, X)`
sets the balance directly without updating the checkpoint. The next time *any* address
tries to `safeTransfer` WNAT (even one unrelated to your test), the call reverts with
**`STF`** (SafeTransferFrom) or similar, because the checkpoint is internally
inconsistent.

**Fix**: in fork tests, fund accounts via the deposit path:

```solidity
vm.deal(alice, 10_000 ether);          // give alice native FLR/SGB
vm.prank(alice);
(bool ok,) = WNAT.call{value: 10_000 ether}(abi.encodeWithSignature("deposit()"));
require(ok, "WNAT deposit failed");    // alice now has WNAT, checkpoint correct
```

`deal(token, addr, amount)` (the `forge-std` helper, lowercase) works fine for
**non-WNAT** ERC-20s like USDT0, USDC, FXRP — just not for WNAT itself.

**WNAT recipient-side reverts CANNOT block transfers**. The hook only updates
internal state on the WNAT contract; it does not call into the recipient. A
contract whose `receive()` and `fallback()` both `revert("blocked")` still
receives WNAT successfully. Empirically confirmed against live Flare WFLR
2026-05-08 — useful to know when an audit tries to flag a `safeTransfer(WFLR, ...)`
to an unknown recipient as a DoS vector.

### FTSO reward-claim gas blows past naive estimates

A long-held NFT position with many accrued FTSO epochs can require **7M+ gas** just
to claim its rewards. One real Enosys V3 NFT on Flare was observed consuming
**7.67M gas** on a single `claim` call — and that was an active mid-life NFT, not the
worst case.

When designing an `executeBatch` or `cancelOrder` path that auto-claims rewards
before burn, reserve **at least 10M inner gas** for the claim and pin outer gas
accordingly. Provide a recovery path (e.g. `claimDelegationRewardsFull(orderId)`)
that forwards all caller gas with no inner cap, so users with overflowing NFTs
have an out.

### Enosys redistributor rotation — naming chaos, same ABI

As of 2026-04, three different redistributor contracts have existed on Flare. **They
share the same ABI shape but are not interchangeable.**

| Contract | Address | Status | Handles |
|---|---|---|---|
| `FlaresRewardsRedistributorForNft` | `0x4410B821Fa1041D242f2C199C0EA78E4A5f15F19` | **Legacy** | FTSO + FlareDrops (NFT-aware) |
| `FtsoRewardRedistributorForNft` | `0x5a0BfF8Ff1AF1DF28619Fce57d07E3bBb7BAF3d7` | **Current Flare** | FTSO delegation only |
| `FtsoRewardRedistributorForAddress` | `0x45aAa2e37B89f7514DF69FA084f381C67891C381` | Different ABI! | FTSO for naked WNAT delegations |

On Songbird, `FtsoRewardRedistributorForNft` lives at `0x421294D3eb38c87390fc7f1442623f5b7DBc5b86`.

**Critical**: `FtsoRewardRedistributorForAddress` takes `address[]` not `uint256[]`
in its `claim` function. **Never register it on a contract that expects an
NFT-aware redistributor** — the selector mismatch will revert every claim call.

FlareDrops are no longer routed through any of the LOM-claimable redistributors;
that path moved to a separate Enosys-handled flow in the 2026-04 rotation.

### How to identify the live redistributor for a given DEX

1. Check if it's a proxy (EIP-1967 implementation slot
   `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`).
2. Call `getUnclaimed([])`. The active redistributor returns
   non-zero for active NFTs; legacy or wrong ones return 0 even for active NFTs.
3. NFT id `1` is usually a safe probe — if it returns 0 from one redistributor and
   non-zero from another, the non-zero one is current.

### `RewardManager` ≠ Redistributor

Enosys's `RewardManager` (e.g. Songbird `0xa997E5FD…` for DEX-level pool incentives)
has a **different signature** from the NFT redistributors:
`claim(uint256[] ids, uint256 epochsToClaimCnt, address recipient)` — note the extra
`epochsToClaimCnt` parameter. A contract expecting the redistributor ABI will revert
on selector mismatch if you wire `RewardManager` in by accident.

### Coston2's redistributor returns 0 even for live NFTs

Coston2 has the FTSO contracts deployed but rewards aren't flowing — `getUnclaimed`
always returns 0. Order/fill flow works for development; reward-path testing has
to happen on Songbird or Flare. Don't waste time debugging "no rewards" on Coston2.

---

## 5. Foundry / fork-test patterns

### Fork tests must self-skip when CI runs without `--fork-url`

`forge test` runs every test by default. Tests that bake real mainnet addresses into
`setUp()` will fail with `call to non-contract address 0x…` when CI runs without
fork. Cleanest fix: gate on `block.chainid`.

```solidity
function setUp() public {
    // Fork-only test: skip in default (non-forked) CI runs.
    // Run locally with: forge test --fork-url flare --match-contract MyTest -vvv
    if (block.chainid == 31337) {
        vm.skip(true);
        return;
    }
    // …rest of setUp using real Flare addresses
}
```

`31337` is Anvil's default chain ID (also Foundry's default test chain). On a real
fork (`14`/`19`/`114`) the guard is a no-op. `vm.skip(true)` requires a recent
forge-std (post-1.8); use `return;` alone if that's unavailable but you'll see a
"setUp succeeded but no tests ran" warning.

### Stack-too-deep in deploy scripts → extract to internal helpers

Foundry scripts often accumulate locals in `run()`: env-var reads, factory
references, address parsing. Yul codegen hits "variable too deep in stack." Fix by
extracting groups of locals to internal helpers:

```solidity
function run() external {
    address deployed = _deployCore();
    _applyTreasuryAndFeeSplit(deployed);
    _applyPricingConfig(deployed);
    // ...
}

function _applyTreasuryAndFeeSplit(address mgr) internal { /* scoped locals */ }
function _applyPricingConfig(address mgr) internal { /* scoped locals */ }
```

Each helper gets its own stack window, so depth resets at every call boundary.

### `forge fmt --check` is strictly enforced in CI — pre-format before pushing

If your repo's CI runs `forge fmt --check`, every column-alignment difference
fails the run. The formatter has strong opinions about:

- Single-space between type and identifier (`uint24 fee` not `uint24   fee`)
- Single-space inside `require(cond, "msg")` (no double-space alignment)
- Multi-line return tuples in interface definitions
- Brace placement on multi-modifier function declarations

**Before opening a PR, run `forge fmt`** and commit. The formatter is deterministic;
its output is the only formatting CI accepts.

### Scope contr

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Thanasimos](https://github.com/Thanasimos)
- **Source:** [Thanasimos/Thanas-flare-builders-toolkit](https://github.com/Thanasimos/Thanas-flare-builders-toolkit)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-thanasimos-thanas-flare-builders-toolkit-flare-dapp-pitfalls
- Seller: https://agentstack.voostack.com/s/thanasimos
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
