Skip to content

Commit 279b86d

Browse files
committed
docs(ftso): FTSO adapters
1 parent 612cd70 commit 279b86d

File tree

11 files changed

+1571
-0
lines changed

11 files changed

+1571
-0
lines changed

docs/ftso/guides/adapters.mdx

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
---
2+
slug: adapters
3+
title: Oracle Adapters
4+
description: Easily migrate existing dApps to Flare's FTSO using adapters that replicate popular oracle interfaces like Pyth, Chainlink, and API3.
5+
keywords:
6+
[
7+
solidity,
8+
reference,
9+
ftso,
10+
flare-time-series-oracle,
11+
flare-network,
12+
smart-contracts,
13+
]
14+
---
15+
16+
import Tabs from "@theme/Tabs";
17+
import TabItem from "@theme/TabItem";
18+
import CodeBlock from "@theme/CodeBlock";
19+
import ChainlinkExample from "!!raw-loader!/examples/developer-hub-solidity/ChainlinkExample.sol";
20+
import chainlinkExample from "!!raw-loader!/examples/developer-hub-javascript/chainlinkExample.ts";
21+
import PythExample from "!!raw-loader!/examples/developer-hub-solidity/PythExample.sol";
22+
import pythExample from "!!raw-loader!/examples/developer-hub-javascript/pythExample.ts";
23+
import Api3Example from "!!raw-loader!/examples/developer-hub-solidity/Api3Example.sol";
24+
import api3Example from "!!raw-loader!/examples/developer-hub-javascript/api3Example.ts";
25+
import BandExample from "!!raw-loader!/examples/developer-hub-solidity/BandExample.sol";
26+
import bandExample from "!!raw-loader!/examples/developer-hub-javascript/bandExample.ts";
27+
import ChronicleExample from "!!raw-loader!/examples/developer-hub-solidity/ChronicleExample.sol";
28+
import chronicleExample from "!!raw-loader!/examples/developer-hub-javascript/chronicleExample.ts";
29+
30+
FTSO Adapters, provided by the `@flarenetwork/ftso-adapters` library, allow decentralized applications (dApps) built for other popular oracle interfaces to integrate with Flare's FTSO with minimal code changes. The library provides adapters for Pyth, Chainlink, API3, Band Protocol, and Chronicle. These adapters act as a compatibility layer, translating the FTSO's data structure into the format expected by each respective oracle's interface.
31+
32+
This enables a seamless migration path for projects looking to leverage the speed, decentralization, and cost-effectiveness of Flare's native oracle. This guide focuses on the specific code modifications required to migrate your existing dApp.
33+
34+
All code examples can be found in our [hardhat-starter-kit](https://github.com/flare-network/hardhat-starter-kit/tree/main/contracts/adapters).
35+
36+
## Migrating Your dApp: Key Code Changes
37+
38+
Migrating from an external oracle to Flare's FTSO adapter involves a paradigm shift: instead of your contract calling an external oracle for data, your contract becomes its own oracle by integrating an adapter library. This process involves two key areas of change: on-chain contract modifications and a new off-chain keeper process.
39+
40+
### 1. On-Chain: Integrate the Adapter Library
41+
42+
The core of the migration happens within your smart contract. You will modify it to store, update, and serve the FTSO price data itself.
43+
44+
- **State Variables**: Instead of storing an address to an external oracle proxy, you add state variables to your contract to manage the FTSO feed and cache the price data.
45+
- **Before**: `AggregatorV3Interface internal dataFeed;`
46+
- **After**: `bytes21 public immutable ftsoFeedId; FtsoChainlinkAdapterLibrary.Round private _latestPriceData;`
47+
48+
- **Constructor**: Your constructor no longer needs a proxy address. Instead, it takes FTSO-specific configuration, such as the `ftsoFeedId` and any adapter-specific parameters (like `chainlinkDecimals`).
49+
- **Before**: `constructor(address _dataFeedAddress) { dataFeed = AggregatorV3Interface(_dataFeedAddress); }`
50+
- **After**: `constructor(bytes21 _ftsoFeedId, uint8 _chainlinkDecimals) { ftsoFeedId = _ftsoFeedId; chainlinkDecimals = _chainlinkDecimals; }`
51+
52+
- **Implement `refresh()`**: You must add a public `refresh()` function. This function's only job is to call the adapter library's `refresh` logic, passing in your contract's state variables to be updated with the latest FTSO price.
53+
54+
- **Implement the Oracle Interface**: You then implement the standard `view` function for the oracle you are migrating from (e.g., `latestRoundData()` for Chainlink, `getPriceNoOlderThan()` for Pyth). This function calls the corresponding logic from the adapter library, reading directly from your contract's cached state.
55+
56+
- **No Change to Core Logic**: Crucially, your dApp's internal business logic, which consumes the price data, remains unchanged. It continues to call the same standard oracle functions as before (e.g., `latestRoundData()`), but now it's calling a function implemented directly on your own contract.
57+
58+
### 2. Off-Chain: Set Up a Keeper Bot
59+
60+
Since your contract now controls its own price updates, you need an external process to trigger them.
61+
62+
- **Create a Keeper Script**: This is a simple script that connects to the network and periodically calls the public `refresh()` function on your deployed contract.
63+
- **Run the Keeper**: This script ensures the cached price in your contract remains fresh and doesn't become stale. It replaces the need to rely on the oracle provider's keeper network, giving you direct control over the frequency—and therefore the cost—of your price updates.
64+
65+
<Tabs block>
66+
<TabItem value="chainlink" label="Chainlink">
67+
68+
### FtsoChainlinkAdapter
69+
70+
The `FtsoChainlinkAdapter` implements Chainlink's `AggregatorV3Interface`. The example is an `AssetVault` contract that uses the FTSO price to value collateral for borrowing and lending.
71+
72+
#### Smart Contract: `ChainlinkExample.sol`
73+
74+
<CodeBlock language="solidity" title="/contracts/adapters/ChainlinkExample.sol">
75+
{ChainlinkExample}
76+
</CodeBlock>
77+
78+
#### Off-Chain Script: `chainlinkExample.ts`
79+
80+
<CodeBlock language="typescript" title="/scripts/adapters/chainlinkExample.ts">
81+
{chainlinkExample}
82+
</CodeBlock>
83+
84+
</TabItem>
85+
<TabItem value="pyth" label="Pyth">
86+
87+
### FtsoPythAdapter
88+
89+
The `FtsoPythAdapter` implements Pyth's `IPyth` interface. The example is a `PythNftMinter` contract that dynamically calculates a $1 minting fee based on the live FTSO price.
90+
91+
#### Smart Contract: `PythExample.sol`
92+
93+
<CodeBlock language="solidity" title="/contracts/adapters/PythExample.sol">
94+
{PythExample}
95+
</CodeBlock>
96+
97+
#### Off-Chain Script: `pythExample.ts`
98+
99+
<CodeBlock language="typescript" title="/scripts/adapters/pythExample.ts">
100+
{pythExample}
101+
</CodeBlock>
102+
103+
</TabItem>
104+
<TabItem value="api3" label="API3">
105+
106+
### FtsoApi3Adapter
107+
108+
The `FtsoApi3Adapter` implements the `IApi3ReaderProxy` interface. The example is a `PriceGuesser` prediction market that uses the FTSO price to settle bets.
109+
110+
#### Smart Contract: `Api3Example.sol`
111+
112+
<CodeBlock language="solidity" title="/contracts/adapters/Api3Example.sol">
113+
{Api3Example}
114+
</CodeBlock>
115+
116+
#### Off-Chain Script: `api3Example.ts`
117+
118+
<CodeBlock language="typescript" title="/scripts/adapters/api3Example.ts">
119+
{api3Example}
120+
</CodeBlock>
121+
122+
</TabItem>
123+
<TabItem value="band" label="Band">
124+
125+
### FtsoBandAdapter
126+
127+
The `FtsoBandAdapter` implements Band Protocol's `IStdReference` interface. The example is a `PriceTriggeredSafe` that locks withdrawals during high market volatility, detected by checking a basket of FTSO prices.
128+
129+
#### Smart Contract: `BandExample.sol`
130+
131+
<CodeBlock language="solidity" title="/contracts/adapters/BandExample.sol">
132+
{BandExample}
133+
</CodeBlock>
134+
135+
#### Off-Chain Script: `bandExample.ts`
136+
137+
<CodeBlock language="typescript" title="/scripts/adapters/bandExample.ts">
138+
{bandExample}
139+
</CodeBlock>
140+
141+
</TabItem>
142+
<TabItem value="chronicle" label="Chronicle">
143+
144+
### FtsoChronicleAdapter
145+
146+
The `FtsoChronicleAdapter` implements the `IChronicle` interface. The example is a `DynamicNftMinter` that mints NFTs of different tiers based on the live FTSO asset price.
147+
148+
#### Smart Contract: `ChronicleExample.sol`
149+
150+
<CodeBlock language="solidity" title="/contracts/adapters/ChronicleExample.sol">
151+
{ChronicleExample}
152+
</CodeBlock>
153+
154+
#### Off-Chain Script: `chronicleExample.ts`
155+
156+
<CodeBlock language="typescript" title="/scripts/adapters/chronicleExample.ts">
157+
{chronicleExample}
158+
</CodeBlock>
159+
160+
</TabItem>
161+
</Tabs>
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { artifacts, run, web3 } from "hardhat";
2+
import { PriceGuesserInstance } from "../../typechain-types";
3+
4+
// --- Configuration ---
5+
const PriceGuesser: PriceGuesserInstance = artifacts.require("PriceGuesser");
6+
const FTSO_FEED_ID = "0x01464c522f55534400000000000000000000000000";
7+
const DESCRIPTION = "FTSOv2 FLR/USD adapted for API3";
8+
const MAX_AGE_SECONDS = 3600;
9+
const STRIKE_PRICE_USD = 0.025;
10+
const ROUND_DURATION_SECONDS = 300;
11+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
12+
13+
async function deployContracts(): Promise<{ guesser: PriceGuesserInstance }> {
14+
const strikePriceWei = BigInt(STRIKE_PRICE_USD * 1e18);
15+
const guesserArgs: (string | number)[] = [
16+
FTSO_FEED_ID,
17+
DESCRIPTION,
18+
MAX_AGE_SECONDS,
19+
strikePriceWei.toString(),
20+
ROUND_DURATION_SECONDS,
21+
];
22+
console.log("\nDeploying integrated PriceGuesser contract with arguments:");
23+
console.log(` - FTSO Feed ID: ${guesserArgs[0]}`);
24+
console.log(` - Description: ${guesserArgs[1]}`);
25+
console.log(` - Max Age (seconds): ${guesserArgs[2]}`);
26+
console.log(` - Strike Price: ${STRIKE_PRICE_USD} (${guesserArgs[3]} wei)`);
27+
console.log(` - Round Duration: ${guesserArgs[4]} seconds`);
28+
const guesser = await PriceGuesser.new(
29+
...(guesserArgs as [string, string, number, string, number]),
30+
);
31+
console.log("\n✅ PriceGuesser deployed to:", guesser.address);
32+
33+
try {
34+
console.log("\nVerifying PriceGuesser on block explorer...");
35+
await run("verify:verify", {
36+
address: guesser.address,
37+
constructorArguments: guesserArgs,
38+
});
39+
console.log("PriceGuesser verification successful.");
40+
} catch (e: unknown) {
41+
if (e instanceof Error) {
42+
console.error("PriceGuesser verification failed:", e.message);
43+
} else {
44+
console.error("An unknown error occurred during verification:", e);
45+
}
46+
}
47+
48+
return { guesser };
49+
}
50+
51+
async function interactWithMarket(guesser: PriceGuesserInstance) {
52+
const accounts = await web3.eth.getAccounts();
53+
const deployer = accounts[0];
54+
const bettorAbove = accounts.length > 1 ? accounts[1] : deployer;
55+
const bettorBelow = accounts.length > 2 ? accounts[2] : deployer;
56+
const betAmountAbove = 10n * 10n ** 18n;
57+
const betAmountBelow = 20n * 10n ** 18n;
58+
59+
console.log(`\n--- Simulating Prediction Market ---`);
60+
console.log(` - Deployer/Settler: ${deployer}`);
61+
console.log(` - Bettor "Above": ${bettorAbove}`);
62+
console.log(` - Bettor "Below": ${bettorBelow}`);
63+
64+
console.log("\nStep 1: Bettors are placing their bets...");
65+
await guesser.betAbove({
66+
from: bettorAbove,
67+
value: betAmountAbove.toString(),
68+
});
69+
console.log(
70+
` - Bettor "Above" placed ${web3.utils.fromWei(betAmountAbove.toString())} tokens.`,
71+
);
72+
await guesser.betBelow({
73+
from: bettorBelow,
74+
value: betAmountBelow.toString(),
75+
});
76+
console.log(
77+
` - Bettor "Below" placed ${web3.utils.fromWei(betAmountBelow.toString())} tokens.`,
78+
);
79+
80+
console.log(
81+
`\nStep 2: Betting round is live. Waiting ${ROUND_DURATION_SECONDS} seconds for it to expire...`,
82+
);
83+
await wait(ROUND_DURATION_SECONDS * 1000);
84+
console.log(" - The betting round has now expired.");
85+
86+
console.log(
87+
"\nStep 3: Refreshing the FTSO price on the contract post-expiry...",
88+
);
89+
await guesser.refresh({ from: deployer });
90+
console.log(" - Price has been updated on the PriceGuesser contract.");
91+
92+
console.log("\nStep 4: Settling the prediction market...");
93+
const settleTx = await guesser.settle({ from: deployer });
94+
const settledEvent = settleTx.logs.find((e) => e.event === "MarketSettled");
95+
const finalPrice = BigInt(settledEvent.args.finalPrice.toString());
96+
const outcome = Number(settledEvent.args.outcome);
97+
const finalPriceFormatted = Number(finalPrice / 10n ** 14n) / 10000;
98+
const outcomeString = outcome === 1 ? "ABOVE" : "BELOW";
99+
console.log(
100+
`✅ Market settled! Final Price: ${finalPriceFormatted.toFixed(4)}`,
101+
);
102+
console.log(`✅ Outcome: The price was ${outcomeString} the strike price.`);
103+
104+
console.log("\nStep 5: Distributing winnings...");
105+
const [winner, loser] =
106+
outcome === 1 ? [bettorAbove, bettorBelow] : [bettorBelow, bettorAbove];
107+
const prizePool = outcome === 1 ? betAmountBelow : betAmountAbove;
108+
const winnerBet = outcome === 1 ? betAmountAbove : betAmountBelow;
109+
110+
if (prizePool > 0n || winnerBet > 0n) {
111+
console.log(` - Attempting to claim for WINNER ("${outcomeString}")`);
112+
await guesser.claimWinnings({ from: winner });
113+
const totalWinnings = winnerBet + prizePool;
114+
console.log(
115+
` - WINNER claimed their prize of ${web3.utils.fromWei(totalWinnings.toString())} tokens.`,
116+
);
117+
} else {
118+
console.log(" - WINNER's pool won, but no bets were placed to claim.");
119+
}
120+
121+
if (winner !== loser) {
122+
try {
123+
await guesser.claimWinnings({ from: loser });
124+
} catch (error: unknown) {
125+
if (error instanceof Error && error.message.includes("NothingToClaim")) {
126+
console.log(
127+
" - LOSER correctly failed to claim winnings as expected.",
128+
);
129+
} else if (error instanceof Error) {
130+
console.error(
131+
" - An unexpected error occurred for the loser:",
132+
error.message,
133+
);
134+
} else {
135+
console.error(" - An unknown error occurred for the loser:", error);
136+
}
137+
}
138+
} else {
139+
console.log(
140+
" - Skipping loser claim attempt as winner and loser are the same account.",
141+
);
142+
}
143+
}
144+
145+
async function main() {
146+
console.log("🚀 Starting Prediction Market Management Script 🚀");
147+
const { guesser } = await deployContracts();
148+
await interactWithMarket(guesser);
149+
console.log("\n🎉 Script finished successfully! 🎉");
150+
}
151+
152+
void main()
153+
.then(() => process.exit(0))
154+
.catch((error) => {
155+
console.error(error);
156+
process.exit(1);
157+
});

0 commit comments

Comments
 (0)