# Flare Security

> >

- **Type:** Skill
- **Install:** `agentstack add skill-thanasimos-thanas-flare-builders-toolkit-flare-security`
- **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-security

## Install

```sh
agentstack add skill-thanasimos-thanas-flare-builders-toolkit-flare-security
```

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

## 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.
2. **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`.**

```solidity
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.
3. 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`.**

```solidity
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.

```solidity
// 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.

```solidity
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.

```solidity
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.

```bash
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.
2. **Read-and-call pattern in the contract** — `balanceOf(this)` after the input
   transfer to determine the actual amount available, then use that for any
   downstream transfer.
3. **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.

```solidity
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).

```solidity
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 Multicall3** — `factory.getPool(t0, t1, fee)`
   is cheap and scales O(pairs).
2. **Ankr (paid)** — IP-locked or domain-locked keys, lifts to ~5k blocks.
3. **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:

```ts
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:

```solidity
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:

```solidity
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.

- **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:** no
- **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-security
- 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%.
