Install
$ agentstack add skill-thanasimos-thanas-flare-builders-toolkit-flare-dapp-pitfalls ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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.
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:
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_calls.
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.
// 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:
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.
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:
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
- Check if it's a proxy (EIP-1967 implementation slot
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc).
- Call
getUnclaimed([]). The active redistributor returns
non-zero for active NFTs; legacy or wrong ones return 0 even for active NFTs.
- NFT id
1is 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.
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:
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 feenotuint24 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
- Source: Thanasimos/Thanas-flare-builders-toolkit
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.