Agent documentation index: llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Skip to main content

Weather Insurance Extension

Build and deploy a Trusted Execution Environment (TEE) extension that settles parametric rainfall insurance using weather data from OpenWeatherMap inside a secure enclave. This guide walks through every step — from writing the smart contract and extension handler to deploying on Coston2 and running an end-to-end policy buy and settlement test. The code for this example is available on GitHub.

New to Flare TEE?

A TEE extension is an off-chain program that runs inside a Trusted Execution Environment. It receives instructions from on-chain transactions, processes them in a secure enclave, and writes the signed results back to the blockchain. The TEE framework handles attestation, key management, and message routing — you only write the business logic.

Overview

The Weather Insurance extension demonstrates a full Flare Confidential Compute (FCC) application workflow:

  1. A policyholder buys rainfall cover for a specific date and location, paying a premium in an ERC-20 token.
  2. After midnight on the day following the coverage date, a keeper sends a SETTLE instruction to the extension.
  3. The extension fetches that day's precipitation from the OpenWeatherMap API inside the enclave.
  4. The extension signs the settlement result; anyone calls settle() on-chain to verify the TEE signature and pay out if the rainfall threshold was met.

The extension also supports:

  • FETCH — ad-hoc current weather for a city (useful for testing).
  • BUY — private policy purchase where coverage terms are ECIES-encrypted and only decrypted inside the TEE.

We will build this in three parts:

  1. on-chain contract (WeatherInsurance) that manages policies and routes instructions;
  2. off-chain handler (Go) that calls the OpenWeatherMap API and signs results;
  3. deployment tooling that ties everything together.

Architecture

The extension stack consists of three components running as Docker services:

  • extension-tee: Your extension code (Go). Receives decoded instructions from the proxy, calls the OpenWeatherMap API, and returns signed results.
  • ext-proxy: The TEE extension proxy. Watches the chain for new instructions targeting your extension, forwards them to your handler, and submits results back on-chain.
  • redis: In-memory store used by the proxy for internal state.

Onchain Contract

The WeatherInsurance smart contract is the onchain entry point.

It interacts with two Flare system contracts:

  • TeeExtensionRegistry: Registers extensions and routes instructions to TEE machines.
  • TeeMachineRegistry: Tracks registered TEE machines and provides random selection.

The contract also manages policy state, ERC-20 premiums and payouts, and on-chain verification of signatures.

Contract Code
contracts/WeatherInsurance.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

// TODO: Replace local interfaces with imports from flare-smart-contracts-v2 once published as a package.
import { ITeeExtensionRegistry } from "./interfaces/ITeeExtensionRegistry.sol";
import { ITeeMachineRegistry } from "./interfaces/ITeeMachineRegistry.sol";
import { SettlementTime } from "./SettlementTime.sol";

/// @notice Minimal ERC-20 surface used for premiums and payouts.
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}

/// @title WeatherInsurance
/// @author Flare Foundation
/// @notice Parametric rainfall insurance settled by a Flare Confidential Compute
/// (FCC) TEE extension. A policyholder buys cover for a specific date; from 00:00 UTC
/// on the day after that date a keeper asks the TEE to fetch the day's rainfall from
/// OpenWeatherMap's One Call `day_summary` endpoint. The TEE returns a signed
/// settlement and `settle()` verifies the TEE signature on-chain (ecrecover
/// against the registered TEE address) before paying out if the measured
/// precipitation met the policy threshold.
///
/// Premiums and payouts are denominated in an ERC-20 `payToken` (set by the
/// owner). Native value is only used for the TEE instruction fee forwarded to
/// the registry in requestSettlement/getWeather.
///
/// This contract also doubles as the FCC InstructionSender: it owns the
/// extension registration and routes WEATHER/{FETCH,SETTLE,BUY} instructions
/// through the Flare TEE Manager diamond.
///
/// DO NOT MODIFY: the registry wiring in the constructor, setExtensionId(),
/// and _getExtensionId().
contract WeatherInsurance {
// --- FCC operation identifiers (must match internal/config/config.go) ---

/// @notice Operation type for weather-data actions.
// forge-lint: disable-next-line(unsafe-typecast)
bytes32 public constant OP_TYPE_WEATHER = bytes32("WEATHER");

/// @notice Command for the ad-hoc current-weather FETCH action.
// forge-lint: disable-next-line(unsafe-typecast)
bytes32 public constant OP_COMMAND_FETCH = bytes32("FETCH");

/// @notice Command for the policy SETTLE action (fetch daily rainfall).
// forge-lint: disable-next-line(unsafe-typecast)
bytes32 public constant OP_COMMAND_SETTLE = bytes32("SETTLE");

/// @notice Command for a private policy BUY action (ECIES-encrypted params).
// forge-lint: disable-next-line(unsafe-typecast)
bytes32 public constant OP_COMMAND_BUY = bytes32("BUY");

// --- Registries ---

/// @notice Reference to the TEE extension registry contract.
ITeeExtensionRegistry public immutable TEE_EXTENSION_REGISTRY;
/// @notice Reference to the TEE machine registry contract.
ITeeMachineRegistry public immutable TEE_MACHINE_REGISTRY;

uint256 private _extensionId;

// --- Policy state ---

/// @notice A parametric rainfall policy. Amounts are in `payToken` units.
struct Policy {
address policyholder; // who gets paid on trigger
string date; // coverage date, "YYYY-MM-DD" (OpenWeatherMap day_summary)
uint256 rainThresholdMmE2; // payout iff measured precipitation (mm × 100) >= this
uint256 payout; // payToken paid to the holder on trigger
uint256 premium; // payToken paid at purchase
uint256 measuredMmE2; // precipitation reported by the TEE (mm × 100), set on settle
bool settled;
bool paidOut;
bool isPrivate; // if true, rain threshold is not stored on-chain (TEE-held)
bytes32 termsCommitment; // keccak256(abi.encode PrivateBuyParams); links TEE memory to policy
string lat; // decimal latitude for One Call day_summary (set at buy)
string lon; // decimal longitude for One Call day_summary (set at buy)
uint64 settlementUnlockAt; // unix sec: 00:00 UTC on the day after coverage date
}

/// @notice ABI payload of a SETTLE instruction (decoded by the TEE).
struct SettleMessage {
uint256 policyId;
address contractAddr; // this contract — echoed back so settle() can bind the result
string date; // coverage date (on-chain for all policies)
string lat;
string lon;
bytes32 termsCommitment; // nonzero => TEE loads coverage terms from private BUY memory
}

/// @notice ABI payload of an ad-hoc FETCH instruction (decoded by the TEE).
struct GetWeatherMessage {
string city;
}

/// @notice Decrypted by the TEE; not sent on-chain in plaintext during buyPolicyPrivate.
struct PrivateBuyParams {
address holder;
address contractAddr;
string date;
uint256 rainThresholdMmE2;
uint256 payout;
uint256 premium;
string lat;
string lon;
}

/// @notice Contract owner (deployer). Funds the pool and registers the TEE address.
address public owner;

/// @notice Registered TEE signing address; settlements must be signed by it.
address public teeAddress;

/// @notice ERC-20 used for premiums and payouts. Set by the owner before use.
IERC20 public payToken;

/// @notice payToken reserved to cover payouts of unsettled policies.
uint256 public reserved;

Policy[] public policies;

event PayTokenSet(address indexed token);
event PolicyBought(uint256 indexed policyId, address indexed holder, string date, uint256 rainThresholdMmE2, uint256 payout, uint256 premium);
/// @notice Emitted for private buys instead of PolicyBought (no coverage terms in logs).
event PrivatePolicyRelayed(uint256 indexed policyId, address indexed holder, bytes32 indexed termsCommitment);
event PrivateBuyRequested(bytes32 indexed instructionId, address indexed holder);
event SettlementRequested(uint256 indexed policyId, bytes32 instructionId);
event Settled(uint256 indexed policyId, uint256 measuredMmE2, bool paidOut);
event TeeAddressSet(address indexed teeAddress);
event PoolFunded(address indexed from, uint256 amount);

modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}

/// @notice Initializes the contract with registry addresses.
/// @param _teeExtensionRegistry Address of the TEE extension registry.
/// @param _teeMachineRegistry Address of the TEE machine registry.
constructor(
ITeeExtensionRegistry _teeExtensionRegistry,
ITeeMachineRegistry _teeMachineRegistry
) {
require(address(_teeExtensionRegistry) != address(0), "TeeExtensionRegistry cannot be zero address");
require(address(_teeMachineRegistry) != address(0), "TeeMachineRegistry cannot be zero address");
require(address(_teeExtensionRegistry).code.length > 0, "TeeExtensionRegistry has no code");
require(address(_teeMachineRegistry).code.length > 0, "TeeMachineRegistry has no code");
TEE_EXTENSION_REGISTRY = _teeExtensionRegistry;
TEE_MACHINE_REGISTRY = _teeMachineRegistry;
owner = msg.sender;
}

/// @notice Finds and sets this contract's extension id. Can only be set once.
/// DO NOT MODIFY this function.
function setExtensionId() external {
require(_extensionId == 0, "Extension ID already set.");

uint256 c = TEE_EXTENSION_REGISTRY.extensionsCounter();
for (uint256 i = 0; i < c; ++i) {
if (TEE_EXTENSION_REGISTRY.getTeeExtensionInstructionsSender(i) == address(this)) {
_extensionId = i;
return;
}
}
revert("Extension ID not found.");
}

// --- Pool funding (payToken) ---

/// @notice Set the ERC-20 used for premiums and payouts. Owner only.
function setPayToken(address _token) external onlyOwner {
require(_token != address(0), "zero token");
payToken = IERC20(_token);
emit PayTokenSet(_token);
}

/// @notice Fund the payout pool with `_amount` payToken. Caller must approve first.
function fundPool(uint256 _amount) external {
require(address(payToken) != address(0), "payToken not set");
require(payToken.transferFrom(msg.sender, address(this), _amount), "fund transfer failed");
emit PoolFunded(msg.sender, _amount);
}

/// @notice Owner withdraws unreserved payToken liquidity (premiums + unclaimed pool).
function withdraw(uint256 _amount) external onlyOwner {
require(_amount <= availableLiquidity(), "exceeds unreserved liquidity");
require(payToken.transfer(owner, _amount), "withdraw failed");
}

/// @notice payToken liquidity not reserved against outstanding policy payouts.
function availableLiquidity() public view returns (uint256) {
if (address(payToken) == address(0)) {
return 0;
}
return payToken.balanceOf(address(this)) - reserved;
}

// --- Policy lifecycle ---

/// @notice Buy a rainfall policy for `_date`. The premium is pulled in payToken,
/// so the caller must `approve` this contract for `_premium` first.
/// @param _date Coverage date as "YYYY-MM-DD".
/// @param _rainThresholdMmE2 Trigger threshold in mm × 100 (e.g. 100 = 1.00 mm).
/// @param _payout payToken paid to the holder if the threshold is met. Must be
/// covered by currently unreserved liquidity (after the premium is added).
/// @param _premium payToken pulled from the caller at purchase.
/// @return policyId The new policy's id.
function buyPolicy(
string calldata _date,
uint256 _rainThresholdMmE2,
uint256 _payout,
uint256 _premium,
string calldata _lat,
string calldata _lon
) external returns (uint256 policyId) {
policyId = _createPolicy(msg.sender, _date, _rainThresholdMmE2, _payout, _premium, _lat, _lon, false);
}

/// @notice Request a private policy buy. Policy parameters are ECIES-encrypted under
/// the TEE public key and passed as opaque bytes; only the ciphertext is visible
/// in this transaction. After the TEE processes the instruction, call
/// `relayPrivateBuy` with the signed ActionResult.
/// @param _encryptedPolicy ECIES ciphertext of ABI-encoded PrivateBuyParams.
function buyPolicyPrivate(bytes calldata _encryptedPolicy) external payable {
require(_encryptedPolicy.length > 0, "empty ciphertext");

address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);
address[] memory cosigners = new address[](0);

ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({
opType: OP_TYPE_WEATHER,
opCommand: OP_COMMAND_BUY,
message: _encryptedPolicy,
cosigners: cosigners,
cosignersThreshold: 0,
claimBackAddress: msg.sender
});

bytes32 instructionId = TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(teeIds, params);
emit PrivateBuyRequested(instructionId, msg.sender);
}

/// @notice Finalize a private buy with a TEE-signed result from the BUY instruction.
/// @dev The TEE node signs `ActionResult.Hash()` with its registered key using the
/// EIP-191 personal-sign prefix. We reconstruct that hash from the result
/// fields the proxy returned and recover the signer, requiring it to equal
/// `teeAddress`. `_resultData` is the exact bytes the TEE returned in
/// ActionResult.Data:
/// abi.encode(address holder, address contractAddr, string date,
/// uint256 rainThresholdMmE2, uint256 payout, uint256 premium, string lat,
/// string lon).
/// - holder: policy buyer; must equal msg.sender.
/// - contractAddr: target WeatherInsurance; must equal address(this).
/// - date: coverage date as "YYYY-MM-DD".
/// - rainThresholdMmE2: rainfall trigger threshold in mm × 100 (e.g. 100 = 1.00 mm).
/// - payout: payToken paid to holder if triggered; must be covered by pool liquidity.
/// - premium: payToken pulled from holder at relay (caller must approve first).
/// - lat: latitude for settlement weather fetch.
/// - lon: longitude for settlement weather fetch.
/// A `termsCommitment` over those fields is stored on the policy; threshold and
/// coordinates stay off-chain until settlement reveals them.
/// @param _resultData Raw `ActionResult.Data` returned by the TEE extension after it
/// decrypts the ECIES ciphertext from `buyPolicyPrivate`, validates
/// the terms, and ABI-encodes them. Must be the exact byte string
/// the proxy signed (do not re-encode). Decoded as:
/// `(holder, contractAddr, date, rainThresholdMmE2, payout, premium,
/// lat, lon)` — see @dev above. `holder` must be `msg.sender`;
/// `contractAddr` must be `address(this)`.
/// @param _actionId `ActionResult.ID`: the instruction id emitted by
/// `buyPolicyPrivate` / `sendInstructions`. Binds this relay to one
/// FCC instruction so the signed result cannot be replayed against
/// another action. Included in `ActionResult.Hash()` as a plain
/// `bytes32` (not hashed again).
/// @param _submissionTag `ActionResult.SubmissionTag` from the original action payload
/// (typically `"submit"`). Hashed as `keccak256(bytes(tag))` inside
/// `ActionResult.Hash()`. Must match the tag the TEE node signed.
/// @param _status `ActionResult.Status` reported by the extension: `0` = error,
/// `1` = success, `2` = still processing. Only `1` is accepted;
/// any other value reverts with `"TEE reported failure"`. Part of
/// the signed hash so a failed TEE result cannot be relayed.
/// @param _signature ECDSA signature from the registered TEE node over
/// `ActionResult.Hash()` = `keccak256(abi.encodePacked(
/// keccak256(_resultData), _actionId,
/// keccak256(bytes(_submissionTag)), _status))`, wrapped with the
/// EIP-191 `"\x19Ethereum Signed Message:\n32"` prefix. Recovered
/// signer must equal `teeAddress`.
/// @return policyId The new policy's id.
function relayPrivateBuy(
bytes calldata _resultData,
bytes32 _actionId,
string calldata _submissionTag,
uint8 _status,
bytes calldata _signature
) external returns (uint256 policyId) {
require(teeAddress != address(0), "TEE address not set");
require(_status == 1, "TEE reported failure");

// Reconstruct ActionResult.Hash() = keccak256(keccak256(data) || id || keccak256(tag) || status).
bytes32 resultHash = keccak256(
abi.encodePacked(
keccak256(_resultData),
_actionId,
keccak256(bytes(_submissionTag)),
_status
)
);

// Recover the signer from the signature and verify it matches the TEE address.
address signer = _recover(_ethSigned(resultHash), _signature);
// Verify the signer matches the registered TEE address.
require(signer == teeAddress, "bad TEE signature");

// ActionResult.Data from the TEE BUY instruction (see @dev above for field meanings).
(
address holder,
address contractAddr,
string memory date,
uint256 rainThresholdMmE2,
uint256 payout,
uint256 premium,
string memory lat,
string memory lon
) = abi.decode(_resultData, (address, address, string, uint256, uint256, uint256, string, string));

require(contractAddr == address(this), "buy not for this contract");
require(msg.sender == holder, "not holder");

// Hash attested terms for settlement verification; rain threshold stays off-chain (isPrivate).
bytes32 commitment = _privateTermsCommitment(
holder, contractAddr, date, rainThresholdMmE2, payout, premium, lat, lon
);
policyId = _createPolicy(holder, date, rainThresholdMmE2, payout, premium, lat, lon, true);
policies[policyId].termsCommitment = commitment;

emit PrivatePolicyRelayed(policyId, holder, commitment);
}

/// @notice Ask the TEE to settle a policy by fetching its date's rainfall.
/// @dev Allowed only from settlementUnlockAt onward (midnight UTC after coverage date).
function requestSettlement(uint256 _policyId) external payable {
Policy storage p = policies[_policyId];
require(p.policyholder != address(0), "no such policy");
require(!p.settled, "already settled");
require(block.timestamp >= p.settlementUnlockAt, "settlement not open yet");

// Pick one registered TEE for this extension; no cosigners on settlement.
address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);
address[] memory cosigners = new address[](0);

// TEE fetches rainfall for date/lat/lon; termsCommitment links private policies to TEE-held terms.
bytes memory message = abi.encode(SettleMessage({
policyId: _policyId,
contractAddr: address(this),
date: p.date,
lat: p.lat,
lon: p.lon,
termsCommitment: p.termsCommitment
}));

ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({
opType: OP_TYPE_WEATHER,
opCommand: OP_COMMAND_SETTLE,
message: message,
cosigners: cosigners,
cosignersThreshold: 0,
claimBackAddress: msg.sender
});

// After processing, call settle() with the signed ActionResult from the proxy.
bytes32 instructionId = TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(teeIds, params);
emit SettlementRequested(_policyId, instructionId);
}

/// @notice Finalize a policy with a TEE-signed settlement and pay out if triggered.
/// @dev The TEE node signs `ActionResult.Hash()` with its registered key using the
/// EIP-191 personal-sign prefix. We reconstruct that hash from the result
/// fields the proxy returned and recover the signer, requiring it to equal
/// `teeAddress`. `_resultData` is the exact bytes the TEE returned in
/// ActionResult.Data: abi.encode(address contractAddr, uint256 policyId,
/// uint256 precipitationMmE2, string date, uint256 rainThresholdMmE2, bool triggered).
/// For private policies, rainThresholdMmE2 is verified against
/// termsCommitment and written on-chain; isPrivate is cleared before payout.
/// @param _resultData ActionResult.Data bytes (the settlement payload).
/// @param _actionId ActionResult.ID.
/// @param _submissionTag ActionResult.SubmissionTag (e.g. "submit").
/// @param _status ActionResult.Status (1 = success).
/// @param _signature TEE node signature over ActionResult.Hash().
function settle(
bytes calldata _resultData,
bytes32 _actionId,
string calldata _submissionTag,
uint8 _status,
bytes calldata _signature
) external {
require(teeAddress != address(0), "TEE address not set");
require(_status == 1, "TEE reported failure");

// Reconstruct ActionResult.Hash() = keccak256(keccak256(data) || id || keccak256(tag) || status).
bytes32 resultHash = keccak256(
abi.encodePacked(
keccak256(_resultData),
_actionId,
keccak256(bytes(_submissionTag)),
_status
)
);
address signer = _recover(_ethSigned(resultHash), _signature);
require(signer == teeAddress, "bad TEE signature");

// ActionResult.Data from the TEE SETTLE instruction (see @dev above for field meanings).
(
address contractAddr,
uint256 policyId,
uint256 precipitationMmE2,
string memory date,
uint256 revealedThresholdMmE2,
bool triggered
) = abi.decode(_resultData, (address, uint256, uint256, string, uint256, bool));
require(contractAddr == address(this), "settlement not for this contract");

Policy storage p = policies[policyId];
require(p.policyholder != address(0), "no such policy");
require(!p.settled, "already settled");
require(block.timestamp >= p.settlementUnlockAt, "settlement not open yet");

// Private policy: verify revealed threshold against BUY commitment, then store on-chain.
if (p.termsCommitment != bytes32(0)) {
require(p.isPrivate, "not private");
require(
_privateTermsCommitment(
p.policyholder,
address(this),
date,
revealedThresholdMmE2,
p.payout,
p.premium,
p.lat,
p.lon
) == p.termsCommitment,
"terms mismatch"
);
require(triggered == (precipitationMmE2 >= revealedThresholdMmE2), "triggered mismatch");
p.date = date;
p.rainThresholdMmE2 = revealedThresholdMmE2;
p.isPrivate = false;
}

p.settled = true;
p.measuredMmE2 = precipitationMmE2;
reserved -= p.payout; // release payout liquidity whether or not we pay out

bool pay = precipitationMmE2 >= p.rainThresholdMmE2;
if (pay) {
p.paidOut = true;
require(payToken.transfer(p.policyholder, p.payout), "payout transfer failed");
}

emit Settled(policyId, precipitationMmE2, p.paidOut);
}

/// @notice Whether settlement may be requested now (block.timestamp >= settlementUnlockAt).
function canRequestSettlement(uint256 _policyId) external view returns (bool) {
Policy storage p = policies[_policyId];
if (p.policyholder == address(0) || p.settled) {
return false;
}
return block.timestamp >= p.settlementUnlockAt;
}

/// @notice Register the active TEE signing address (read off TeeMachineRegistry).
function setTeeAddress(address _teeAddress) external onlyOwner {
require(_teeAddress != address(0), "zero TEE address");
teeAddress = _teeAddress;
emit TeeAddressSet(_teeAddress);
}

// --- Ad-hoc current weather (unchanged FETCH path; handy for testing) ---

/// @notice Request current weather for a city from the TEE (no policy involved).
/// @param _city UTF-8 city name (e.g. "Berlin,DE").
function getWeather(string calldata _city) external payable {
address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);
address[] memory cosigners = new address[](0);

ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({
opType: OP_TYPE_WEATHER,
opCommand: OP_COMMAND_FETCH,
message: abi.encode(GetWeatherMessage({city: _city})),
cosigners: cosigners,
cosignersThreshold: 0,
claimBackAddress: msg.sender
});

TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(teeIds, params);
}

// --- Views ---

function policyCount() external view returns (uint256) {
return policies.length;
}

// --- Internal ---

/// @notice Returns the cached extension ID, reverting if not set.
function _getExtensionId() internal view returns (uint256) {
require(_extensionId != 0, "Extension ID is not set.");
return _extensionId;
}

/// @notice Commitment over private buy terms (matches TEE extension storage key).
function _privateTermsCommitment(
address holder,
address contractAddr,
string memory date,
uint256 rainThresholdMmE2,
uint256 payout,
uint256 premium,
string memory lat,
string memory lon
) internal pure returns (bytes32) {
return keccak256(abi.encode(holder, contractAddr, date, rainThresholdMmE2, payout, premium, lat, lon));
}

/// @notice Pull premium, reserve payout liquidity, and append a policy record.
/// @param _isPrivate If true, rain threshold is kept in TEE memory only (date is stored on-chain).
function _createPolicy(
address _holder,
string memory _date,
uint256 _rainThresholdMmE2,
uint256 _payout,
uint256 _premium,
string memory _lat,
string memory _lon,
bool _isPrivate
) internal returns (uint256 policyId) {
require(address(payToken) != address(0), "payToken not set");
require(_premium > 0, "premium required");
require(_payout > 0, "payout required");
require(_holder != address(0), "zero holder");
require(bytes(_lat).length > 0, "lat required");
require(bytes(_lon).length > 0, "lon required");
require(bytes(_date).length > 0, "date required");
uint64 unlockAt = uint64(SettlementTime.unlockAt(_date));

require(payToken.transferFrom(_holder, address(this), _premium), "premium transfer failed");
require(_payout <= availableLiquidity(), "insufficient pool liquidity for payout");

policyId = policies.length;
policies.push(Policy({
policyholder: _holder,
date: _date,
rainThresholdMmE2: _isPrivate ? 0 : _rainThresholdMmE2,
payout: _payout,
premium: _premium,
measuredMmE2: 0,
settled: false,
paidOut: false,
isPrivate: _isPrivate,
termsCommitment: bytes32(0),
lat: _lat,
lon: _lon,
settlementUnlockAt: unlockAt
}));
reserved += _payout;

if (!_isPrivate) {
emit PolicyBought(policyId, _holder, _date, _rainThresholdMmE2, _payout, _premium);
}
}

/// @notice EIP-191 personal-sign hash of a 32-byte digest.
function _ethSigned(bytes32 _hash) private pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash));
}

/// @notice Recover the signer of a 65-byte [r||s||v] secp256k1 signature.
function _recover(bytes32 _digest, bytes calldata _sig) private pure returns (address) {
require(_sig.length == 65, "bad signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := calldataload(_sig.offset)
s := calldataload(add(_sig.offset, 32))
v := byte(0, calldataload(add(_sig.offset, 64)))
}
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "bad signature v");
address signer = ecrecover(_digest, v, r, s);
require(signer != address(0), "invalid signature");
return signer;
}
}
note

The constructor takes the addresses of the two Flare system contracts. These are already deployed on Coston2 — the deployment tooling reads their addresses from config/coston2/deployed-addresses.json. This is a temporary solution, because Flare Confidential Compute is still in development. On release, both addresses will be available through the FlareContractRegistry contract.

Operation Identifiers

Constants must match internal/config/config.go:

contracts/WeatherInsurance.sol
bytes32 public constant OP_TYPE_WEATHER = bytes32("WEATHER");
bytes32 public constant OP_COMMAND_FETCH = bytes32("FETCH");
bytes32 public constant OP_COMMAND_SETTLE = bytes32("SETTLE");
bytes32 public constant OP_COMMAND_BUY = bytes32("BUY");
CommandPurpose
FETCHReturn current weather JSON for a city (testing and dApp display).
SETTLEFetch daily rainfall for a policy and return a signed settlement payload.
BUYDecrypt ECIES-encrypted private policy terms and return attested parameters.

Policy Lifecycle

A Policy struct stores coverage metadata on-chain:

contracts/WeatherInsurance.sol
struct Policy {
address policyholder;
string date; // "YYYY-MM-DD"
uint256 rainThresholdMmE2; // trigger threshold in mm x 100
uint256 payout;
uint256 premium;
uint256 measuredMmE2; // set on settle
bool settled;
bool paidOut;
bool isPrivate; // threshold held in TEE until settlement
bytes32 termsCommitment;
string lat;
string lon;
uint64 settlementUnlockAt; // 00:00 UTC day after coverage date
}

Settlement unlock time is computed by the SettlementTime library — the earliest requestSettlement / settle call is midnight on the day after the coverage date.

Buying a Policy

Public buy — all terms are visible on-chain:

contracts/WeatherInsurance.sol
function buyPolicy(
string calldata _date,
uint256 _rainThresholdMmE2,
uint256 _payout,
uint256 _premium,
string calldata _lat,
string calldata _lon
) external returns (uint256 policyId)

The caller must approve the contract for _premium in payToken before calling. The contract pulls the premium, reserves _payout from pool liquidity, and records the policy.

Private buy — coverage terms stay encrypted on-chain:

  1. The buyer ECIES-encrypts ABI-encoded PrivateBuyParams with the Flare Confidential Compute extension public key.
  2. The buyPolicyPrivate function sends the ciphertext as a WEATHER / BUY instruction.
  3. After the extension processes the instruction, the buyer calls relayPrivateBuy with the signed result.
  4. The contract verifies the TEE signature via ecrecover against teeAddress and creates the policy.

Settlement Flow

From settlementUnlockAt onward, a keeper triggers settlement:

contracts/WeatherInsurance.sol
function requestSettlement(uint256 _policyId) external payable {
// ...
bytes memory message = abi.encode(SettleMessage({
policyId: _policyId,
contractAddr: address(this),
date: p.date,
lat: p.lat,
lon: p.lon,
termsCommitment: p.termsCommitment
}));
// sendInstructions with opCommand = SETTLE
}

Anyone can then call settle() with the TEE-signed ActionResult that is returned by the extension. The contract reconstructs ActionResult.Hash(), recovers the signer, and requires that the signer equal the registered teeAddress. If measured precipitation meets the threshold, payToken is transferred to the policyholder.

TEE Signature Verification

Both relayPrivateBuy and settle verify TEE results using the same pattern:

bytes32 resultHash = keccak256(
abi.encodePacked(
keccak256(_resultData),
_actionId,
keccak256(bytes(_submissionTag)),
_status
)
);
address signer = _recover(_ethSigned(resultHash), _signature);
require(signer == teeAddress, "bad TEE signature");

The TEE node signs ActionResult.Hash() with the EIP-191 personal-sign prefix. Only successful results are accepted.

Extension ID Discovery

After the extension is registered on-chain, call setExtensionId() once to discover and cache the extension ID — same pattern as the Private Key Extension.

Off-chain Handler

The off-chain handler lives in internal/extension/. It exposes an HTTP server with two endpoints:

  • GET /state — reports whether the OpenWeatherMap API key is configured.
  • POST /action — receives TEE actions and routes them by opType / opCommand.

Constants match the Solidity contract:

internal/config/config.go
const (
OPTypeWeather = "WEATHER"
OPCommandFetch = "FETCH"
OPCommandSettle = "SETTLE"
OPCommandBuy = "BUY"
)

FETCH Handler

The processWeatherFetch function ABI-decodes a city name, calls the OpenWeatherMap API, and returns a JSON WeatherReport in ActionResult.Data:

internal/extension/extension.go
func (e *Extension) processWeatherFetch(action teetypes.Action, df *instruction.DataFixed) teetypes.ActionResult {
req, err := structs.Decode[types.GetWeatherRequest](types.GetWeatherMessageArg, df.OriginalMessage)
// ...
report, err := fetchWeather(req.City)
payload, err := json.Marshal(report)
return buildResult(action, df, payload, 1, nil)
}

SETTLE Handler

The processWeatherSettle function fetches daily precipitation from the OpenWeatherMap API and ABI-encodes the settlement:

internal/extension/extension.go
encoded, err := types.SettlementResultArgs.Pack(
req.ContractAddr,
req.PolicyId,
precipMmE2,
coverageDate,
revealedThreshold,
triggered,
)
return buildResult(action, df, encoded, 1, nil)

For private policies, the handler loads coverage terms from in-enclave memory using termsCommitment as the key.

BUY Handler

The processWeatherBuy function decrypts ECIES ciphertext via the TEE node's /decrypt endpoint, validates PrivateBuyParams, stores terms in memory, and returns ABI-encoded parameters for relayPrivateBuy:

internal/extension/extension.go
plaintext, err := decryptViaNode(e.signPort, df.OriginalMessage)
params, err := structs.Decode[types.PrivateBuyParams](types.PrivateBuyParamsArg, plaintext)
// ...
e.privateTerms[commitment] = params
encoded, err := types.PrivateBuyResultArgs.Pack(params.Holder, params.ContractAddr, ...)
return buildResult(action, df, encoded, 1, nil)

Deploying and Testing on Coston2

This walkthrough deploys a local simulated TEE against the real Coston2 chain using Docker and an ngrok tunnel. For production deployment on a Google Cloud Platform Confidential Space VM, see DEPLOYMENT_STEPS.md in the repository.

Step 0: Activate local simulated mode

./scripts/use-chain.sh local coston2

This copies .env.local.coston2 to .env, setting SIMULATED_TEE=true and LOCAL_MODE=false.

Step 1: Configure deployer keys and API key

Edit .env.local.coston2 and set your funded Coston2 credentials and OpenWeatherMap key:

.env.local.coston2
DEPLOYMENT_PRIVATE_KEY="<your-funded-coston2-private-key-hex-no-0x>"
INITIAL_OWNER="0x<your-address>"
OPENWEATHERMAP_API_KEY="<your-openweathermap-api-key>"
PAY_TOKEN="0x53192e788991AD96bC180249B15AefB94E597dD1"

The PAY_TOKEN variable is the address of a mocked ERC-20 token on Coston2 — used for premiums and payouts during testing. You can use this token for testing purposes or deploy your own ERC-20 token.

Re-activate after editing:

./scripts/use-chain.sh local coston2

Step 2: Deploy contract and register extension

./scripts/pre-build.sh
./scripts/extension-setup.sh

The pre-build.sh script compiles Solidity, deploys WeatherInsurance, and registers the extension on-chain. It writes EXTENSION_ID and INSTRUCTION_SENDER to config/extension.env.

The extension-setup.sh script calls setPayToken on the deployed contract.

warning

Once config/extension.env exists, the pre-build step refuses to run again. Use ./scripts/pre-build.sh --force only when you intentionally want a new extension.

Step 3: Start the ngrok tunnel

In a separate terminal:

ngrok http 6674

Copy the HTTPS URL and set it in .env.local.coston2:

.env.local.coston2
EXT_PROXY_URL="https://<your-ngrok-domain>"

Re-activate:

./scripts/use-chain.sh local coston2

Step 4: Configure the indexer database

cp config/proxy/extension_proxy.coston2.docker.toml.example \
config/proxy/extension_proxy.coston2.docker.toml

Edit the [db] block with Coston2 indexer credentials — same process as the Private Key Extension.

Flare Indexer Access

To get the indexer credentials, please get in touch with us via support or X and share what you are building.

Step 5: Start the extension stack

./scripts/start-services.sh

Wait for the proxy to become healthy:

until curl -sf http://localhost:6674/info >/dev/null 2>&1; do sleep 2; done
echo "Extension proxy is ready"

Step 6: Verify the proxy

curl -s "$EXT_PROXY_URL/info" | jq '.machineData'

For a simulated TEE, expect the same codeHash and extensionId pattern as the sign extension.

Step 7: Register the TEE machine

./scripts/post-build.sh
./scripts/extension-post-setup.sh

The post-build.sh script allows the TEE version and registers the machine.

The extension-post-setup.sh script reads the TEE signing address from the proxy and calls setTeeAddress on the WeatherInsurance contract.

Step 8: Run the end-to-end test

./scripts/test.sh

The test does the following:

  1. Funds the payout pool with WPT.
  2. Buys a private policy (ECIES-encrypted terms).
  3. Relays the TEE-signed buy result via the relayPrivateBuy function on the contract.
  4. Requests settlement and waits for the TEE to fetch OpenWeatherMap data.
  5. Calls the settle function on the contract with the signed result and verifies the payout.

If the test passes, your extension is fully operational.

Web Frontend (Optional)

The repository includes a Next.js dApp in the frontend/ directory for wallet connect, policy purchase (public or private), settlement, and live weather fetch.

After the extension stack is running and config/extension.env exists:

cp frontend/.env.local.example frontend/.env.local
# Set NEXT_PUBLIC_WEATHER_INSURANCE from INSTRUCTION_SENDER in config/extension.env
# Set EXT_PROXY_URL to http://127.0.0.1:6674 for local UI

cd frontend && npm install && npm run dev

Open the application in your browser, connect MetaMask on Coston2 (chain ID 114), and fund C2FLR and the mocked ERC-20 token for premiums and payouts.

The UI proxies TEE /info and /action/result through Next.js API routes to avoid browser CORS issues.

Troubleshooting

OPENWEATHERMAP_API_KEY is not set in TEE logs

Set the key in .env and re-run start-services.sh.

Proxy won't start or DB sync error

Check the proxy logs and verify DB credentials in config/proxy/extension_proxy.coston2.docker.toml:

docker compose logs ext-proxy

MachineManager.TooMany() during test

The extension ID in config/extension.env does not match the on-chain TEE record. Run a full reset and start again, or keep the existing config/extension.env and re-run post-build.sh and test.sh.

bad TEE signature on settle or relay

Confirm extension-post-setup.sh ran successfully and teeAddress on the contract matches the registered TEE machine.

Verification.ChallengeExpired

Re-run post-build.sh.

ngrok URL changed

  1. Update EXT_PROXY_URL in .env.local.coston2 and re-run use-chain.sh.
  2. Restart the Docker stack: ./scripts/stop-services.sh && ./scripts/start-services.sh.
  3. Re-run post-build.sh, extension-post-setup.sh, and test.sh.

Cleanup

Stop the Docker stack

./scripts/stop-services.sh

Full reset

./scripts/stop-services.sh
docker compose down --rmi local
rm -f .env config/extension.env config/proxy/extension_proxy.coston2.docker.toml

After a full reset, start again from Step 0.

What's next?

Read the Flare Confidential Compute (FCC) overview for more information on how to build and deploy TEE extensions on Flare. Compare with the Private Key Extension for a simpler introduction to the instruction routing pattern.