AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Flare Security

skill-thanasimos-thanas-flare-builders-toolkit-flare-security · by Thanasimos

>

No reviews yet
0 installs
14 views
0.0% view→install

Install

$ agentstack add skill-thanasimos-thanas-flare-builders-toolkit-flare-security

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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 No
  • 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-thanasimos-thanas-flare-builders-toolkit-flare-security)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Flare Security? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Flare-Specific Security Standards

This skill has two halves you should read together:

  1. Foundational secure-coding patterns that apply to every EVM contract, but

are non-negotiable here. These are the defaults. Deviating from any of them without a documented reason should fail review.

  1. Flare-specific overlays — things the generic audit / audit-contract

skills don't catch because they're not chain-aware.

Philosophy: assume hostile. Every external surface — function inputs, token calls, oracle reads, signatures, calldata, RPCs — is adversarial. Defense in depth always. The audit reports we've seen on Flare contracts are clear: most real exploits come from token edge cases, signature replay, missing access control on admin functions, and FTSO/oracle manipulation. None of those are exotic — all are preventable with the patterns below.


Part 1 — Foundational secure-coding patterns

Ownership

ALWAYS use Ownable2Step from OpenZeppelin, never Ownable.

import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";

contract MyContract is Ownable2Step {
    constructor(address initialOwner) Ownable(initialOwner) { }
}

Why: a single-step ownership transfer is a critical vulnerability if the target address is wrong (typo'd, hardware-wallet path mismatch, contract that can't accept the role). Ownable2Step requires the new owner to call acceptOwnership(), eliminating the silent-handoff failure mode. The cost is two transactions instead of one — non-issue.

For Flare deployments, the deploy → handoff dance is:

  1. Deploy from a hot EOA (deployer key).
  2. Call transferOwnership() — this stages the

pendingOwner.

  1. Have the Ledger / multisig call acceptOwnership() to finalize.

After step 3, the deployer EOA has zero privileged access. Verify by reading owner() and pendingOwner() (must be 0x0 post-handoff).

For multi-role systems, prefer AccessControl from OpenZeppelin with explicit role-grants. Never invent your own role mapping.

Reentrancy guards

Use ReentrancyGuardTransient (post-EIP-1153, ~2k gas per call) over the storage-slot ReentrancyGuard whenever your pragma >=0.8.24.

import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";

contract MyContract is ReentrancyGuardTransient {
    function externalEntry() external nonReentrant {
        // ...
    }
}

Critical: nonReentrant MUST come BEFORE all other modifiers in a function's modifier list. This ensures the guard is set before any other modifier could make an external call. Modifier order is not commutative — it executes left-to-right.

// good — guard sets first
function withdraw(uint256 amount) external nonReentrant onlyOwner whenNotPaused {

// BAD — onlyOwner could (in some patterns) hit external code first
function withdraw(uint256 amount) external onlyOwner nonReentrant whenNotPaused {

Cross-function reentrancy is the harder case. If two functions touch the SAME state variable, both must be nonReentrant. Read-only reentrancy (a view function returning stale state during a callback) bites view-based price oracles — mitigate by validating the read against an independent source.

Token transfers

Always use SafeERC20, never raw transfer / transferFrom. Many tokens on Flare (USDT0, eUSDT, similar Tether forks) don't return bool — a raw call either reverts your transaction at compile-time-bool-decode-fail or silently proceeds with a stale-state assumption. SafeERC20 handles both.

import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

using SafeERC20 for IERC20;

IERC20(token).safeTransfer(to, amount);
IERC20(token).safeTransferFrom(from, to, amount);
IERC20(token).forceApprove(spender, amount);  // NEVER plain approve()

Use forceApprove, not approve. Some tokens (USDT-family, KNC) reject a non-zero → non-zero approval transition; forceApprove resets to 0 first when needed.

For fee-on-transfer tokens (FLX, FLRFROG on Flare), safeTransferFrom does NOT validate the recipient's balance change matches the requested amount. The recipient receives strictly less. Patterns for handling FoT:

  • Read balance before/after if the contract's logic depends on the actual

transferred amount.

  • Use a probe to detect FoT rate up front (see flare-network for the

pattern when integrating with V2 routers via Permit2).

  • Document explicitly in your contract that FoT tokens are not supported,

and add a require check that compares balance delta to amount.

Checks-Effects-Interactions (CEI)

Always: validate inputs → update state → make external calls. Never reverse this order.

function withdraw(uint256 amount) external nonReentrant {
    // CHECKS
    if (amount == 0) revert MyContract__ZeroAmount();
    if (balances[msg.sender]  uint256[]) pending;
function withdrawAll() external {
    uint256[] storage list = pending[msg.sender];
    for (uint256 i; i  deadline`.
3. **Commit-reveal** — for high-value actions where front-running would extract
   meaningful value.
4. **Permit2 / EIP-712 signed permits** — bind the spender into the hash so
   replays across contracts are impossible.

### Pause / emergency-stop

Every contract that holds value or controls user funds should have an
owner-callable pause:

```solidity
bool public paused;

modifier whenNotPaused() {
    if (paused) revert MyContract__Paused();
    _;
}

function setPaused(bool paused_) external onlyOwner {
    paused = paused_;
    emit PausedSet(paused_);
}

Pause should NEVER affect user-fund-recovery paths. If the contract holds user tokens and is paused, users must still be able to withdraw what they put in.


Part 2 — Flare-specific overlays

These are the things that bit real Flare deployments and are NOT covered by generic audit checklists. Apply on top of Part 1 for any contract you ship to Flare/Songbird/Coston2.

Permit2 chain availability

The canonical Uniswap Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3) is deployed only on Flare (chain 14). NOT on Songbird, NOT on Coston2.

cast code 0x000000000022D473030F116dDEE9F6B43aC78BA3 --rpc-url 
# Returns full bytecode on Flare; returns 0x on Songbird/Coston2

If your contract takes a Permit2 address as an immutable, document this; if it hardcodes the canonical address, gate deployment on a chain-id check.

For Songbird/Coston2 canary-testing of Permit2-dependent contracts, your only options are: deploy your own Permit2 (~3000 LOC of Uniswap source — substantial), mock-only test, or skip canary and validate via Flare fork.

Fee-on-transfer (FoT) token surface

Multiple tokens on Flare apply a transfer fee — the recipient receives strictly less than the sender debited. Examples: FLX (0x22757fb83836e3F9F0F353126cACD3B1Dc82a387, ~3% FoT), FLRFROG, others.

Threats this creates:

  • Your contract calls transferFrom(user, this, X) and receives X * (1-r).
  • Your contract then calls safeTransferFrom(this, downstream, X)reverts

with TransferHelper::transferFrom because you don't have X anymore.

  • Pair-side V2 swap math may underflow if the actual liquidity received differs

from what was quoted.

Mitigations:

  1. Probe the FoT rate before encoding the swap calldata — measure

balanceOf(this) change over a single transferFrom, then encode the downstream amount as that delta.

  1. Read-and-call pattern in the contractbalanceOf(this) after the input

transfer to determine the actual amount available, then use that for any downstream transfer.

  1. Document non-support — explicit revert if received != amount on input,

so the user gets a clean failure instead of weird behavior deeper in the call.

Blacklistable stablecoin surface

USDT0, USDC.e, eUSDT, exUSDT, and similar admin-controlled stablecoins can freeze any address (including your contract) at the token issuer's discretion.

Implications for your design:

  • A blacklisted contract cannot transfer the token outbound — pending

state is stuck.

  • Treasury fee splits to a blacklisted treasury address freeze that share — the

rest of the operation should NOT revert (use try/catch to fall through to a redirect).

  • Don't add an admin rescue function that pulls user-deposited tokens.

That's a bigger threat than the blacklist itself. Instead document the risk and let users avoid the contract for blacklist-prone tokens.

FTSO redistributor proxy upgradeability

Enosys's FtsoRewardRedistributorForNft (the contract you call to claim FTSO delegation rewards on V3 NFTs) is an upgradeable proxy. The implementation can change without warning. Implications:

  • The interface (function selectors, return shapes) is stable in practice but

not guaranteed in the contract code itself.

  • A future upgrade could route claimed rewards somewhere new. Audit-time

verification is point-in-time only.

  • Always wrap claim calls in try/catch so a redistributor regression doesn't

brick your contract's main flow.

try IFtsoRewardRedistributorForNft(redistributor).claim(ids, recipient) {
    // ok
} catch {
    // redistributor reverted or mis-routed; continue without rewards
}

For per-DEX rotation patterns and recovery via historical addresses, see enosys-dex-v3.

Native-currency basefee floor

block.basefee on Songbird and Coston2 is often 1 wei or 2 wei. Any incentive-math gate that uses basefee * gasEstimate * coverageMultiple to require a minimum fee collapses to near-zero on those chains.

If your contract has a fee-incentive-coverage check, add a minBasefeeWei floor (typical: 25 gwei to match Flare's mainnet basefee, owner-tunable up to a hard cap of 1000 gwei).

uint256 effectiveBasefee = block.basefee > minBasefeeWei ? block.basefee : minBasefeeWei;
uint256 required = effectiveBasefee * gasEstimate * minCoverageMultiple;

Public RPC eth_getLogs 30-block cap

The public RPC endpoints on Flare/Songbird/Coston2 cap historical log queries to 30 blocks per request. Don't build features that require scanning longer history off the public RPC.

Workarounds:

  1. Pairwise factory queries via Multicall3factory.getPool(t0, t1, fee)

is cheap and scales O(pairs).

  1. Ankr (paid) — IP-locked or domain-locked keys, lifts to ~5k blocks.
  2. Self-hosted node — full history, no caps.

Multicall3 not auto-registered in viem

Multicall3 lives at 0xcA11bde05977b3631167028862bE2a173976CA11 on every Flare-family chain, but viem's defineChain does NOT auto-add it. If you call publicClient.multicall(...) against a custom-defined chain, it throws silently with a confusing error. Always register it explicitly:

defineChain({
  id: 14,
  name: 'Flare',
  // ...
  contracts: {
    multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11', blockCreated: 3002461 },
  },
})

EIP-3855 (PUSH0) status on Coston2

Coston2 emits an EIP-3855 warning when running Solidity 0.8.20+. It's cosmetic — deployments and execution work fine. Don't downgrade solc to 0.8.19 over this warning.

WFLR transfer hooks

WFLR's transfer / transferFrom invoke an FTSO delegation-state hook (updateAtTokenTransfer). The hook is internal state-only on the WFLR contract itself — it does NOT call into the recipient. A recipient that reverts in receive()/fallback() cannot block delivery. Empirically verified 2026-05-08 with a Flare-fork test (WFLR.transfer(evil, 1 ether) to a contract whose receive() reverts succeeds).

The hook DOES revert with SafeMath: subtraction overflow when a SENDER has phantom balance (deal(WFLR, addr, X) writes the balance slot but skips the delegation state init). That's a test-setup pitfall, not a production DoS surface.

Test setup correction: use vm.deal(user, X) + vm.prank(user); WFLR.deposit{value: X}() instead of deal(WFLR, user, X). The latter writes the balance slot but skips the delegation state init.

Implication for audit findings: claims that protocol-fee safeTransfer of WFLR can be DoS'd by a malicious recipient are categorically false positives. Add a fork test to lock the answer down before re-litigation:

function test_WflrSafeTransfer_ToRevertingReceiver_Succeeds() external {
    vm.createSelectFork("flare");
    vm.deal(address(this), 10 ether);
    IWNat(WFLR).deposit{value: 10 ether}();
    address evil = address(new RevertingReceiver()); // receive() reverts
    IERC20(WFLR).safeTransfer(evil, 1 ether);        // succeeds
    assertEq(IERC20(WFLR).balanceOf(evil), 1 ether);
}

Token decimals

Sanity-check decimals at integration time. Common confusions:

| Token | Decimals | |---|---| | WFLR / WSGB / WC2FLR | 18 | | USDT0 / USDC.e / FXRP / eUSDT | 6 | | YAM-V2-style oddities | 24 |

Math involving cross-decimal tokens MUST normalize. The classic precision-loss bug is (a / b) * c — multiply BEFORE dividing.

Owner-key threat model on Flare

Standard Flare-family ownership pattern: hardware wallet (Ledger) owner via Ownable2Step, optionally graduating to a multisig (Safe/Gnosis on Flare).

If owner() on a deployed contract is a deployer EOA, that's a one-key compromise away from total loss. Audit findings should flag this and recommend ownership handoff before mainnet.

The owner controls only what the contract's admin functions allow — review those individually:

  • Pause? Ok if it doesn't lock user funds.
  • Set fee parameters? Ok if bounded.
  • Withdraw user balances? Critical finding — never allow.
  • Set new oracle addresses? Risky — needs timelock or multisig at minimum.

Algebra dynamic-fee snapshot drift

Algebra V1.9+ pools (SparkDEX V4) have plugin-driven dynamic fees that adjust per-block. If your contract snapshots the fee at order-create time and uses it for incentive math at fill time, the actual fee may drift (typically modestly, but during volatility regime changes it can shift meaningfully).

Acceptable as approximation in spam-filter contexts. NOT acceptable when the fee determines a user's payout — read fresh in that case.

Algebra MintParams.deployer = address(0) only safe for default-deployer pools

Algebra's NPM derives the destination pool via CREATE2(deployer, salt(t0, t1)). Passing deployer = address(0) uses the factory's default deployer. If a future DEX registers pools with a custom deployer, mints with deployer = address(0) silently route to the wrong pool.

Mitigation: pre-mint check that factory.computePoolAddress(t0, t1) matches the user's intended pool address. Revert if not.

Algebra communityFee cuts LP fees BEFORE distribution

globalState().communityFee is the protocol's cut taken off LP fees before distributing to LPs. SparkDEX V4 WFLR/USDT0 has communityFee = 250 (25%). Any contract that estimates "LP-fee share to executor" must apply this factor:

uint256 lpShareBps = 10_000 - globalState.communityFee * 10;  // communityFee is 1/1000

Frontend-supplied calldata trust

Contracts that take router calldata as a passthrough (sweeper-style designs) must enforce TWO layers:

  1. Router-address allowlist (isRouter[router])
  2. Function-selector allowlist (allowedSelector[router][selector])

Both layers AND-gated. The selector check is critical — V3's SwapRouter has a multicall(bytes[]) that lets calldata chain arbitrary internal calls. Do NOT seed multicall for any router unless you've audited the implications.

Sandwich-floor enforcement on FTSO-anchored swaps

Any time a contract swaps an asset whose price has an FTSO feed (FAsset → WFLR, WFLR → vault collateral, etc.), the slippage gate should be FTSO-anchored, not pool-quote-anchor

Source & license

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

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.