See how Chainlink CCIP, Functions, and Automation combine to power a prediction dApp that uses real-time sports data to trigger automatic payouts.
Retrieve Web2 data and store it onchain using Chainlink Functions.
Built using Chainlink Data Streams, this dApp enables you to use low-latency data in your smart contracts.
Create batch-revealed NFT collections powered by Chainlink Automation & VRF.
See how Chainlink CCIP, Functions, and Automation combine to power a prediction dApp that uses real-time sports data to trigger automatic payouts.
pragma solidity ^0.8.7;
import {ResultsConsumer} from "./ResultsConsumer.sol";
import {NativeTokenSender} from "./ccip/NativeTokenSender.sol";
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";
struct Config {
address oracle;
address ccipRouter;
address link;
address weth9Token;
address exchangeToken;
address uniswapV3Router;
uint64 subscriptionId;
uint64 destinationChainSelector;
uint32 gasLimit;
bytes secrets;
string source;
}
contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationCompatibleInterface {
uint256 private constant MIN_WAGER = 0.00001 ether;
uint256 private constant MAX_WAGER = 0.01 ether;
uint256 private constant GAME_RESOLVE_DELAY = 2 hours;
mapping(uint256 => Game) private games;
mapping(address => mapping(uint256 => Prediction[])) private predictions;
mapping(uint256 => bytes32) private pendingRequests;
uint256[] private activeGames;
uint256[] private resolvedGames;
struct Game {
uint256 sportId;
uint256 externalId;
uint256 timestamp;
uint256 homeWagerAmount;
uint256 awayWagerAmount;
bool resolved;
Result result;
}
struct Prediction {
uint256 gameId;
Result result;
uint256 amount;
bool claimed;
}
enum Result {
None,
Home,
Away
}
event GameRegistered(uint256 indexed gameId);
event GameResolved(uint256 indexed gameId, Result result);
event Predicted(address indexed user, uint256 indexed gameId, Result result, uint256 amount);
event Claimed(address indexed user, uint256 indexed gameId, uint256 amount);
error GameAlreadyRegistered();
error TimestampInPast();
error GameNotRegistered();
error GameIsResolved();
error GameAlreadyStarted();
error InsufficientValue();
error ValueTooHigh();
error InvalidResult();
error GameNotResolved();
error GameNotReadyToResolve();
error ResolveAlreadyRequested();
error NothingToClaim();
constructor(
Config memory config
)
ResultsConsumer(config.oracle, config.subscriptionId, config.source, config.secrets, config.gasLimit)
NativeTokenSender(
config.ccipRouter,
config.link,
config.weth9Token,
config.exchangeToken,
config.uniswapV3Router,
config.destinationChainSelector
)
{}
function predict(uint256 gameId, Result result) public payable {
Game memory game = games[gameId];
uint256 wagerAmount = msg.value;
if (game.externalId == 0) revert GameNotRegistered();
if (game.resolved) revert GameIsResolved();
if (game.timestamp < block.timestamp) revert GameAlreadyStarted();
if (wagerAmount < MIN_WAGER) revert InsufficientValue();
if (wagerAmount > MAX_WAGER) revert ValueTooHigh();
if (result == Result.Home) games[gameId].homeWagerAmount += wagerAmount;
else if (result == Result.Away) games[gameId].awayWagerAmount += wagerAmount;
else revert InvalidResult();
predictions[msg.sender][gameId].push(Prediction(gameId, result, wagerAmount, false));
emit Predicted(msg.sender, gameId, result, wagerAmount);
}
function registerAndPredict(uint256 sportId, uint256 externalId, uint256 timestamp, Result result) external payable {
uint256 gameId = _registerGame(sportId, externalId, timestamp);
predict(gameId, result);
}
function claim(uint256 gameId, bool transfer) external {
Game memory game = games[gameId];
address user = msg.sender;
if (!game.resolved) revert GameNotResolved();
uint256 totalWinnings = 0;
Prediction[] memory userPredictions = predictions[user][gameId];
for (uint256 i = 0; i < userPredictions.length; i++) {
Prediction memory prediction = userPredictions[i];
if (prediction.claimed) continue;
if (game.result == Result.None) {
totalWinnings += prediction.amount;
} else if (prediction.result == game.result) {
uint256 winnings = calculateWinnings(gameId, prediction.amount, prediction.result);
totalWinnings += winnings;
}
predictions[user][gameId][i].claimed = true;
}
if (totalWinnings == 0) revert NothingToClaim();
if (transfer) {
_sendTransferRequest(user, totalWinnings);
} else {
payable(user).transfer(totalWinnings);
}
emit Claimed(user, gameId, totalWinnings);
}
function _registerGame(uint256 sportId, uint256 externalId, uint256 timestamp) internal returns (uint256 gameId) {
gameId = getGameId(sportId, externalId);
if (games[gameId].externalId != 0) revert GameAlreadyRegistered();
if (timestamp < block.timestamp) revert TimestampInPast();
games[gameId] = Game(sportId, externalId, timestamp, 0, 0, false, Result.None);
activeGames.push(gameId);
emit GameRegistered(gameId);
}
function _requestResolve(uint256 gameId) internal {
Game memory game = games[gameId];
if (pendingRequests[gameId] != 0) revert ResolveAlreadyRequested();
if (game.externalId == 0) revert GameNotRegistered();
if (game.resolved) revert GameIsResolved();
if (!readyToResolve(gameId)) revert GameNotReadyToResolve();
pendingRequests[gameId] = _requestResult(game.sportId, game.externalId);
}
function _processResult(uint256 sportId, uint256 externalId, bytes memory response) internal override {
uint256 gameId = getGameId(sportId, externalId);
Result result = Result(uint256(bytes32(response)));
_resolveGame(gameId, result);
}
function _resolveGame(uint256 gameId, Result result) internal {
games[gameId].result = result;
games[gameId].resolved = true;
resolvedGames.push(gameId);
_removeFromActiveGames(gameId);
emit GameResolved(gameId, result);
}
function _removeFromActiveGames(uint256 gameId) internal {
uint256 index;
for (uint256 i = 0; i < activeGames.length; i++) {
if (activeGames[i] == gameId) {
index = i;
break;
}
}
for (uint256 i = index; i < activeGames.length - 1; i++) {
activeGames[i] = activeGames[i + 1];
}
activeGames.pop();
}
function getGameId(uint256 sportId, uint256 externalId) public pure returns (uint256) {
return (sportId << 128) | externalId;
}
function getGame(uint256 gameId) external view returns (Game memory) {
return games[gameId];
}
function getActiveGames() public view returns (Game[] memory) {
Game[] memory activeGamesArray = new Game[](activeGames.length);
for (uint256 i = 0; i < activeGames.length; i++) {
activeGamesArray[i] = games[activeGames[i]];
}
return activeGamesArray;
}
function getActivePredictions(address user) external view returns (Prediction[] memory) {
uint256 totalPredictions = 0;
for (uint256 i = 0; i < activeGames.length; i++) {
totalPredictions += predictions[user][activeGames[i]].length;
}
uint256 index = 0;
Prediction[] memory userPredictions = new Prediction[](totalPredictions);
for (uint256 i = 0; i < activeGames.length; i++) {
Prediction[] memory gamePredictions = predictions[user][activeGames[i]];
for (uint256 j = 0; j < gamePredictions.length; j++) {
userPredictions[index] = gamePredictions[j];
index++;
}
}
return userPredictions;
}
function getPastPredictions(address user) external view returns (Prediction[] memory) {
uint256 totalPredictions = 0;
for (uint256 i = 0; i < resolvedGames.length; i++) {
totalPredictions += predictions[user][resolvedGames[i]].length;
}
uint256 index = 0;
Prediction[] memory userPredictions = new Prediction[](totalPredictions);
for (uint256 i = 0; i < resolvedGames.length; i++) {
Prediction[] memory gamePredictions = predictions[user][resolvedGames[i]];
for (uint256 j = 0; j < gamePredictions.length; j++) {
userPredictions[index] = gamePredictions[j];
index++;
}
}
return userPredictions;
}
function isPredictionCorrect(address user, uint256 gameId, uint32 predictionIdx) external view returns (bool) {
Game memory game = games[gameId];
if (!game.resolved) return false;
Prediction memory prediction = predictions[user][gameId][predictionIdx];
return prediction.result == game.result;
}
function calculateWinnings(uint256 gameId, uint256 wager, Result result) public view returns (uint256) {
Game memory game = games[gameId];
uint256 totalWager = game.homeWagerAmount + game.awayWagerAmount;
uint256 winnings = (wager * totalWager) / (result == Result.Home ? game.homeWagerAmount : game.awayWagerAmount);
return winnings;
}
function readyToResolve(uint256 gameId) public view returns (bool) {
return games[gameId].timestamp + GAME_RESOLVE_DELAY < block.timestamp;
}
function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
Game[] memory activeGamesArray = getActiveGames();
for (uint256 i = 0; i < activeGamesArray.length; i++) {
uint256 gameId = getGameId(activeGamesArray[i].sportId, activeGamesArray[i].externalId);
if (readyToResolve(gameId) && pendingRequests[gameId] == 0) {
return (true, abi.encodePacked(gameId));
}
}
return (false, "");
}
function performUpkeep(bytes calldata data) external override {
uint256 gameId = abi.decode(data, (uint256));
_requestResolve(gameId);
}
function deletePendingRequest(uint256 gameId) external onlyOwner {
delete pendingRequests[gameId];
}
}
Retrieve Web2 data and store it onchain using Chainlink Functions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol";
/**
* @title Chainlink Functions example consuming X (formerly Twitter) API
*/
contract XUserDataConsumer is FunctionsClient, ConfirmedOwner {
using FunctionsRequest for FunctionsRequest.Request;
enum ResponseType {
UserInfo,
UserLastTweet
}
struct APIResponse {
ResponseType responseType;
string response;
}
// Chainlink Functions script soruce code
string private constant SOURCE_USERNAME_INFO =
"const username = args[0];"
"if(!secrets.xBearerToken) {"
"throw Error('No bearer token');"
"}"
"const xUserResponse = await Functions.makeHttpRequest({"
"url: `https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url`,"
"headers: { Authorization: `Bearer ${secrets.xBearerToken}` },"
"});"
"if (xUserResponse.error) {"
"throw Error(`X User Request Error`);"
"}"
"const { name, id } = xUserResponse.data.data;"
"return Functions.encodeString([name, id]);";
string private constant SOURCE_LAST_TWEET_INFO =
"const id = args[0];"
"if (!secrets.xBearerToken) {"
"throw Error('No bearer token');"
"}"
"const xTweetsResponse = await Functions.makeHttpRequest({"
"url: `https://api.twitter.com/2/users/${id}/tweets`,"
"headers: { Authorization: `Bearer ${secrets.xBearerToken}` },"
"});"
"if (xTweetsResponse.error) {"
"throw Error('X User Request Error');"
"}"
"const lastTweet = xTweetsResponse.data.data[0].text;"
"const shortenedTweet = lastTweet.substring(0, 200);"
"return Functions.encodeString(shortenedTweet);";
bytes32 public donId; // DON ID for the Functions DON to which the requests are sent
uint64 private subscriptionId; // Subscription ID for the Chainlink Functions
uint32 private gasLimit; // Gas limit for the Chainlink Functions callbacks
// Mapping of request IDs to API response info
mapping(bytes32 => APIResponse) public requests;
event UserInfoRequested(bytes32 indexed requestId, string username);
event UserInfoReceived(bytes32 indexed requestId, string response);
event UserLastTweetRequested(bytes32 indexed requestId, string username);
event UserLastTweetReceived(bytes32 indexed requestId, string response);
event RequestFailed(bytes error);
constructor(
address router,
bytes32 _donId,
uint64 _subscriptionId,
uint32 _gasLimit
) FunctionsClient(router) ConfirmedOwner(msg.sender) {
donId = _donId;
subscriptionId = _subscriptionId;
gasLimit = _gasLimit;
}
/**
* @notice Request X profile information for provided handle
* @param username username of said user e.g. chainlink
* @param slotId the location of the DON-hosted secrets
* @param version the version of the secret to be used
*/
function requestUserInfo(string calldata username, uint8 slotId, uint64 version) external {
string[] memory args = new string[](1);
args[0] = username;
bytes32 requestId = _sendRequest(SOURCE_USERNAME_INFO, args, slotId, version);
requests[requestId].responseType = ResponseType.UserInfo;
emit UserInfoRequested(requestId, username);
}
/**
* @notice Request last post for given username
* @param userId username of said user e.g. chainlink
* @param slotId the location of the DON-hosted secrets
* @param version the version of the secret to be used
*/
function requestLastTweet(string calldata userId, uint8 slotId, uint64 version) external {
string[] memory args = new string[](1);
args[0] = userId;
bytes32 requestId = _sendRequest(SOURCE_LAST_TWEET_INFO, args, slotId, version);
requests[requestId].responseType = ResponseType.UserLastTweet;
emit UserLastTweetRequested(requestId, userId);
}
/**
* @notice Process the response from the executed Chainlink Functions script
* @param requestId The request ID
* @param response The response from the Chainlink Functions script
*/
function _processResponse(bytes32 requestId, bytes memory response) private {
requests[requestId].response = string(response);
if (requests[requestId].responseType == ResponseType.UserInfo) {
emit UserInfoReceived(requestId, string(response));
} else {
emit UserLastTweetReceived(requestId, string(response));
}
}
// CHAINLINK FUNCTIONS
/**
* @notice Triggers an on-demand Functions request
* @param args String arguments passed into the source code and accessible via the global variable `args`
* @param slotId the location of the DON-hosted secrets
* @param version the version of the secret to be used
*/
function _sendRequest(
string memory source,
string[] memory args,
uint8 slotId,
uint64 version
) internal returns (bytes32 requestId) {
FunctionsRequest.Request memory req;
req.initializeRequest(FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, source);
req.addDONHostedSecrets(slotId, version);
if (args.length > 0) {
req.setArgs(args);
}
requestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, donId);
}
/**
* @notice Fulfillment callback function
* @param requestId The request ID, returned by sendRequest()
* @param response Aggregated response from the user code
* @param err Aggregated error from the user code or from the execution pipeline
* Either response or error parameter will be set, but never both
*/
function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override {
if (err.length > 0) {
emit RequestFailed(err);
return;
}
_processResponse(requestId, response);
}
// OWNER
/**
* @notice Set the DON ID
* @param newDonId New DON ID
*/
function setDonId(bytes32 newDonId) external onlyOwner {
donId = newDonId;
}
/**
* @notice Set the gas limit
* @param newGasLimit new gas limit
*/
function setCallbackGasLimit(uint32 newGasLimit) external onlyOwner {
gasLimit = newGasLimit;
}
}
Built using Chainlink Data Streams, this dApp enables you to use low-latency data in your smart contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import {IERC20} from "@uniswap/v2-core/contracts/interfaces/IERC20.sol";
import {ILogAutomation, Log} from "@chainlink/contracts/src/v0.8/automation/interfaces/ILogAutomation.sol";
import {StreamsLookupCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/interfaces/StreamsLookupCompatibleInterface.sol";
import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
import {IVerifierProxy} from "./interfaces/IVerifierProxy.sol";
/**
* @title DataStreamsConsumer
* @dev This contract is a Chainlink Data Streams consumer.
* This contract provides low-latency delivery of low-latency delivery of market data.
* These reports can be verified onchain to verify their integrity.
*/
contract DataStreamsConsumer is
ILogAutomation,
StreamsLookupCompatibleInterface
{
// ================================================================
// | CONSTANTS |
// ================================================================
string public constant STRING_DATASTREAMS_FEEDLABEL = "feedIDs";
string public constant STRING_DATASTREAMS_QUERYLABEL = "timestamp";
uint24 public constant FEE = 3000;
// ================================================================
// | STATE |
// ================================================================
string[] public s_feedsHex;
// ================================================================
// | IMMUTABLES |
// ================================================================
address public i_linkToken;
ISwapRouter public i_router;
IVerifierProxy public i_verifier;
// ================================================================
// | STRUCTS |
// ================================================================
struct Report {
bytes32 feedId;
uint32 lowerTimestamp;
uint32 observationsTimestamp;
uint192 nativeFee;
uint192 linkFee;
uint64 upperTimestamp;
int192 benchmark;
}
struct TradeParamsStruct {
address recipient;
address tokenIn;
address tokenOut;
uint256 amountIn;
string feedId;
}
struct Quote {
address quoteAddress;
}
// ================================================================
// | Events |
// ================================================================
event InitiateTrade(
address msgSender,
address tokenIn,
address tokenOut,
uint256 amountIn,
string feedId
);
event TradeExecuted(uint256 tokensAmount);
// ================================================================
// | Errors |
// ================================================================
error InvalidFeedId(string feedId);
/**
* @dev Initializes the contract with necessary parameters.
* @param router The address of the swap router contract.
* @param verifier The address of the verifier contract.
* @param linkToken The address of the LINK token contract.
* @param feedsHex An array of hexadecimal feed IDs.
*/
function initializer(
address router,
address payable verifier,
address linkToken,
string[] memory feedsHex
) public {
i_router = ISwapRouter(router);
i_verifier = IVerifierProxy(verifier);
i_linkToken = linkToken;
s_feedsHex = feedsHex;
}
// ================================================================
// | Chainlink DON |
// ================================================================
/**
* @dev Initiates a trade by emitting a InitiateTrade event.
* When emitted Data Streams will trigger the checkLog function
* indicating that the network should initiate a trade.
* @param tokenIn The input token address.
* @param tokenOut The output token address.
* @param amount The amount to trade.
* @param feedId data feed of the id you are trading
*/
function trade(
address tokenIn,
address tokenOut,
uint256 amount,
string memory feedId
) external {
emit InitiateTrade(msg.sender, tokenIn, tokenOut, amount, feedId);
}
/**
* @dev Checks the log data using Chainlink Data Streams via the StreamsLookup error.
* Once StreamsLookUp error is emitted the data in the error is passed
* to the checkCallback function. Which afterwards executes performUpkeep.
* @param log The log data to be checked.
* @return A boolean indicating if performUpkeep is needed.
*/
function checkLog(
Log calldata log,
bytes memory
) external view returns (bool, bytes memory) {
revert StreamsLookup(
STRING_DATASTREAMS_FEEDLABEL,
s_feedsHex,
STRING_DATASTREAMS_QUERYLABEL,
log.timestamp,
log.data
);
}
/**
* @dev Checks if upkeep is needed and returns the corresponding performData.
* @param values An array of values for the upkeep check.
* @param extraData Additional data for the upkeep.
* @return upkeepNeeded A boolean indicating whether upkeep is needed
* @return performData Bytes that include the signed reports and the extra data
* that is passed in StreamsLookup error.
*/
function checkCallback(
bytes[] memory values,
bytes memory extraData
) external pure returns (bool upkeepNeeded, bytes memory performData) {
return (true, abi.encode(values, extraData));
}
/**
* @dev 1. Decodes the report and extraData.
* 2. Verifies the integrity of the data.
* 3. Performs a swap between tokens using the verified report data.
* This function is executed by Chainlink's Automation Registry.
* @notice This contract needs to have the networks native token to verify the report.
* @param performData The data needed to perform the upkeep.
*/
function performUpkeep(bytes calldata performData) external {
(
Report memory unverifiedReport,
TradeParamsStruct memory tradeParams,
bytes memory bundledReport
) = _decodeData(performData);
// verify tokens
bytes memory verifiedReportData = i_verifier.verify{
value: unverifiedReport.nativeFee
}(bundledReport, abi.encode(i_linkToken));
Report memory verifiedReport = abi.decode(verifiedReportData, (Report));
// swap tokens
uint256 successfullyTradedTokens = _swapTokens(
verifiedReport,
tradeParams
);
emit TradeExecuted(successfullyTradedTokens);
}
// ================================================================
// | REPORT MANIPULATION |
// ================================================================
/**
* @dev Decodes and extracts relevant data from the provided `performData`.
* It decodes the `performData` into signed reports, swap parameters,
* and a bundled report.
* @param performData The data needed for the decoding process.
* @return signedReport The decoded report from the bundled report data.
* @return tradeParams The decoded swap parameters.
* @return bundledReport The bundled report data for verification.
*/
function _decodeData(
bytes memory performData
)
private
view
returns (
Report memory signedReport,
TradeParamsStruct memory tradeParams,
bytes memory bundledReport
)
{
(bytes[] memory signedReports, bytes memory extraData) = abi.decode(
performData,
(bytes[], bytes)
);
(
address sender,
address tokenIn,
address tokenOut,
uint256 amountIn,
string memory feedId
) = abi.decode(extraData, (address, address, address, uint256, string));
tradeParams = TradeParamsStruct({
recipient: sender,
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: amountIn,
feedId: feedId
});
uint256 feedIdIndex = getIdFromFeed(feedId);
bundledReport = _bundleReport(signedReports[feedIdIndex]);
signedReport = _getReportData(bundledReport);
}
/**
* @dev Bundles a report with a quote and returns the bundled report.
* @param report The report to be bundled.
* @return The bundled report.
*/
function _bundleReport(
bytes memory report
) private view returns (bytes memory) {
Quote memory quote;
quote.quoteAddress = i_linkToken;
(
bytes32[3] memory reportContext,
bytes memory reportData,
bytes32[] memory rs,
bytes32[] memory ss,
bytes32 raw
) = abi.decode(
report,
(bytes32[3], bytes, bytes32[], bytes32[], bytes32)
);
bytes memory bundledReport = abi.encode(
reportContext,
reportData,
rs,
ss,
raw,
abi.encode(quote)
);
return bundledReport;
}
/**
* @dev Extracts and decodes the report data from a signed report.
* @param signedReport The signed report.
* @return The decoded report data.
*/
function _getReportData(
bytes memory signedReport
) internal pure returns (Report memory) {
(, bytes memory reportData, , , ) = abi.decode(
signedReport,
(bytes32[3], bytes, bytes32[], bytes32[], bytes32)
);
Report memory report = abi.decode(reportData, (Report));
return report;
}
/**
* @dev Returns the index of a feed ID in the array of feed IDs.
* @param feedId The feed id that you are looking for its index
* @return The index of the feed ID in the array, or reverts with an error if not found.
*/
function getIdFromFeed(string memory feedId) public view returns (uint256) {
uint256 result;
string[] storage feeds = s_feedsHex;
for (uint256 i = 0; i < feeds.length; i++) {
if (
keccak256(abi.encode(feeds[i])) == keccak256(abi.encode(feedId))
) {
result = i;
break;
}
if (i == feeds.length - 1) {
revert InvalidFeedId(feedId);
}
}
return result;
}
// ================================================================
// | SWAP |
// ================================================================
/**
* @dev Scales the price from a report to match the token's decimals.
* @param tokenOut The output token for which the price should be scaled.
* @param priceFromReport The price from the report to be scaled.
* @return The scaled price with the appropriate token decimals.
*/
function _scalePriceToTokenDecimals(
IERC20 tokenOut,
int192 priceFromReport
) private view returns (uint256) {
uint256 pricefeedDecimals = 18;
uint8 tokenOutDecimals = tokenOut.decimals();
if (tokenOutDecimals < pricefeedDecimals) {
uint256 difference = pricefeedDecimals - tokenOutDecimals;
return uint256(uint192(priceFromReport)) / 10 ** difference;
} else {
uint256 difference = tokenOutDecimals - pricefeedDecimals;
return uint256(uint192(priceFromReport)) * 10 ** difference;
}
}
/**
* @dev Swaps tokens using the verified report data and swap parameters.
* It first decodes the verified report data to obtain the benchmark price,
* then transfers tokens from the recipient to this contract, approves the
* token transfer, and executes the swap using the provided parameters.
* @param verifiedReport The verified report data containing price information.
* @param tradeParams The parameters for the token swap.
* @return The amount of tokens received after the swap.
*/
function _swapTokens(
Report memory verifiedReport,
TradeParamsStruct memory tradeParams
) private returns (uint256) {
uint8 inputTokenDecimals = IERC20(tradeParams.tokenIn).decimals();
uint256 priceForOneToken = _scalePriceToTokenDecimals(
IERC20(tradeParams.tokenOut),
verifiedReport.benchmark
);
uint256 outputAmount = (priceForOneToken * tradeParams.amountIn) /
10 ** inputTokenDecimals;
IERC20(tradeParams.tokenIn).transferFrom(
tradeParams.recipient,
address(this),
tradeParams.amountIn
);
IERC20(tradeParams.tokenIn).approve(
address(i_router),
tradeParams.amountIn
);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams(
tradeParams.tokenIn,
tradeParams.tokenOut,
FEE,
tradeParams.recipient,
tradeParams.amountIn,
outputAmount,
0
);
return i_router.exactInputSingle(params);
}
/**
* @dev Extracts and decodes the report data from a signed report.
* This function is needed because the contract needs native tokens
* to verify reports.
**/
receive() external payable {}
}
Create batch-revealed NFT collections powered by Chainlink Automation & VRF.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";
import "./INFTCollection.sol";
contract NFTCollection is
INFTCollection,
Ownable,
ERC721Enumerable,
VRFConsumerBaseV2,
KeeperCompatibleInterface
{
// STRUCTS
struct Metadata {
uint256 startIndex;
uint256 endIndex;
uint256 entropy;
}
// IMMUTABLE STORAGE
uint256 private immutable MAX_SUPPLY;
uint256 private immutable MINT_COST;
// MUTABLE STORAGE
uint256 private s_revealedCount;
uint256 private s_revealBatchSize;
uint256 private s_revealInterval;
uint256 private s_lastRevealed = block.timestamp;
bool private s_revealInProgress;
Metadata[] private s_metadatas;
// VRF CONSTANTS & IMMUTABLE
uint16 private constant VRF_REQUEST_CONFIRMATIONS = 3;
uint32 private constant VRF_NUM_WORDS = 1;
VRFCoordinatorV2Interface private immutable VRF_COORDINATOR_V2;
uint64 private immutable VRF_SUBSCRIPTION_ID;
bytes32 private immutable VRF_GAS_LANE;
uint32 private immutable VRF_CALLBACK_GAS_LIMIT;
// EVENTS
event BatchRevealRequested(uint256 requestId);
event BatchRevealFinished(uint256 startIndex, uint256 endIndex);
// ERRORS
error InvalidAmount();
error MaxSupplyReached();
error InsufficientFunds();
error RevealCriteriaNotMet();
error RevealInProgress();
error InsufficientLINK();
error WithdrawProceedsFailed();
error NonExistentToken();
constructor(
string memory _name,
string memory _symbol,
uint256 _maxSupply,
uint256 _mintCost,
uint256 _revealBatchSize,
uint256 _revealInterval,
address _vrfCoordinatorV2,
uint64 _vrfSubscriptionId,
bytes32 _vrfGasLane,
uint32 _vrfCallbackGasLimit
) ERC721(_name, _symbol) VRFConsumerBaseV2(_vrfCoordinatorV2) {
MAX_SUPPLY = _maxSupply;
MINT_COST = _mintCost;
VRF_COORDINATOR_V2 = VRFCoordinatorV2Interface(_vrfCoordinatorV2);
VRF_SUBSCRIPTION_ID = _vrfSubscriptionId;
VRF_GAS_LANE = _vrfGasLane;
VRF_CALLBACK_GAS_LIMIT = _vrfCallbackGasLimit;
s_revealBatchSize = _revealBatchSize;
s_revealInterval = _revealInterval;
}
// ACTIONS
function mint(uint256 _amount) external payable override {
uint256 totalSupply = totalSupply();
if (_amount == 0) {
revert InvalidAmount();
}
if (totalSupply + _amount > MAX_SUPPLY) {
revert MaxSupplyReached();
}
if (msg.value < MINT_COST * _amount) {
revert InsufficientFunds();
}
for (uint256 i = 1; i <= _amount; i++) {
_safeMint(msg.sender, totalSupply + i);
}
}
function withdrawProceeds() external override onlyOwner {
(bool sent, ) = payable(owner()).call{value: address(this).balance}("");
if (!sent) {
revert WithdrawProceedsFailed();
}
}
// GETTERS
function tokenURI(uint256 tokenId)
public
view
override
returns (string memory)
{
if (!_exists(tokenId)) {
revert NonExistentToken();
}
(uint256 randomness, bool metadataCleared) = _getTokenRandomness(tokenId);
string memory svg = _generateSVG(randomness, metadataCleared);
string memory svgEncoded = _svgToImageURI(svg);
return _formatTokenURI(svgEncoded);
}
function revealedCount() external view override returns (uint256) {
return s_revealedCount;
}
function lastRevealed() external view override returns (uint256) {
return s_lastRevealed;
}
function batchSize() external view override returns (uint256) {
return s_revealBatchSize;
}
function revealInterval() external view override returns (uint256) {
return s_revealInterval;
}
function batchCount() external view returns (uint256) {
return s_metadatas.length;
}
function batchDetails(uint256 index)
external
view
returns (
uint256,
uint256,
uint256
)
{
Metadata memory batch = s_metadatas[index];
return (batch.startIndex, batch.endIndex, batch.entropy);
}
function mintCost() public view override returns (uint256) {
return MINT_COST;
}
function maxSupply() external view override returns (uint256) {
return MAX_SUPPLY;
}
// HELPERS
function _getTokenRandomness(uint256 tokenId)
internal
view
returns (uint256 randomness, bool metadataCleared)
{
for (uint256 i = 0; i < s_metadatas.length; i++) {
if (
tokenId >= s_metadatas[i].startIndex &&
tokenId < s_metadatas[i].endIndex
) {
randomness = uint256(
keccak256(abi.encode(s_metadatas[i].entropy, tokenId))
);
metadataCleared = true;
}
}
}
function _formatTokenURI(string memory imageURI)
internal
pure
returns (string memory)
{
return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
bytes(
abi.encodePacked(
'{',
'"name":"NFT", ',
'"description":"Batch-revealed NFT!", ',
'"attributes":"", ',
'"image":"', imageURI, '"',
'}'
)
)
)
)
);
}
function _generateSVG(uint256 _randomness, bool _metadataCleared)
internal
pure
returns (string memory)
{
string[4] memory parts;
parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">';
if (_metadataCleared) {
parts[1] = '<style>.base { fill: white; font-family: serif; font-size: 59px; }</style><rect width="100%" height="100%" fill="black" /><text class="base">';
string[6] memory svgRows;
string memory randomnessString = Strings.toHexString(_randomness, 32);
for (uint8 i = 0; i < 6; i++) {
string memory partialString = _substring(randomnessString, i * 11, (i + 1) * 11);
svgRows[i] = string(
abi.encodePacked(
'<tspan x="16" dy="56">',
partialString,
"</tspan>"
)
);
}
parts[2] = string(
abi.encodePacked(
svgRows[0],
svgRows[1],
svgRows[2],
svgRows[3],
svgRows[4],
svgRows[5]
)
);
} else {
parts[1] = '<style>.base { fill: white; font-family: serif; font-size: 350px; }</style><rect width="100%" height="100%" fill="black" /><text x="90" y="295" class="base">';
parts[2] = "?";
}
parts[3] = "</text></svg>";
return string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3]));
}
function _svgToImageURI(string memory svg)
internal
pure
returns (string memory)
{
string memory baseURL = "data:image/svg+xml;base64,";
string memory svgBase64Encoded = Base64.encode(bytes(string(abi.encodePacked(svg))));
return string(abi.encodePacked(baseURL, svgBase64Encoded));
}
function _substring(
string memory str,
uint256 startIndex,
uint256 endIndex
) internal pure returns (string memory) {
bytes memory strBytes = bytes(str);
bytes memory result = new bytes(endIndex - startIndex);
for (uint256 i = startIndex; i < endIndex; i++) {
result[i - startIndex] = strBytes[i];
}
return string(result);
}
// REVEAL
function shouldReveal() public view override returns (bool) {
uint256 unrevealedCount = totalSupply() - s_revealedCount;
if (unrevealedCount == 0) {
return false;
}
bool batchSizeCriteria = false;
if (s_revealBatchSize > 0 && unrevealedCount >= s_revealBatchSize) {
batchSizeCriteria = true;
}
bool intervalCriteria = false;
if (
s_revealInterval > 0 &&
block.timestamp - s_lastRevealed > s_revealInterval
) {
intervalCriteria = true;
}
return (batchSizeCriteria || intervalCriteria);
}
function revealPendingMetadata()
public
override
returns (uint256 requestId){
if (s_revealInProgress) {
revert RevealInProgress();
}
if (!shouldReveal()) {
revert RevealCriteriaNotMet();
}
requestId = VRF_COORDINATOR_V2.requestRandomWords(
VRF_GAS_LANE,
VRF_SUBSCRIPTION_ID,
VRF_REQUEST_CONFIRMATIONS,
VRF_CALLBACK_GAS_LIMIT,
VRF_NUM_WORDS
);
s_revealInProgress = true;
emit BatchRevealRequested(requestId);
}
function _fulfillRandomnessForMetadata(uint256 randomness) internal {
uint256 totalSupply = totalSupply();
uint256 startIndex = s_revealedCount + 1;
uint256 endIndex = totalSupply + 1;
s_metadatas.push(
Metadata({
startIndex: startIndex,
endIndex: endIndex,
entropy: randomness
})
);
s_revealedCount = totalSupply;
s_lastRevealed = block.timestamp;
s_revealInProgress = false;
emit BatchRevealFinished(startIndex, endIndex);
}
// VRF
function fulfillRandomWords(uint256, uint256[] memory randomWords)
internal
override
{
_fulfillRandomnessForMetadata(randomWords[0]);
}
// KEEPERS
function checkUpkeep(bytes calldata)
external
view
override
returns (bool upkeepNeeded, bytes memory)
{
upkeepNeeded = !s_revealInProgress && shouldReveal();
}
function performUpkeep(bytes calldata) external override {
revealPendingMetadata();
}
// SETTERS
function setRevealBatchSize(uint256 _revealBatchSize)
external
override
onlyOwner
{
s_revealBatchSize = _revealBatchSize;
}
function setRevealInterval(uint256 _revealInterval)
external
override
onlyOwner
{
s_revealInterval = _revealInterval;
}
function supportsInterface(bytes4 interfaceId)
public
view
override
returns (bool)
{
return
super.supportsInterface(interfaceId) ||
interfaceId == type(INFTCollection).interfaceId;
}
}
Explore the creations of other builders from the Chainlink community.
Note: by clicking on certain links on this page, you will leave the DevHub site. Such websites are independent from and unaffiliated with the Chainlink Foundation. The Chainlink Foundation is not responsible for any action taken or content on the third-party website.