Skip to content

Commit 0129897

Browse files
committed
docs(fdc): import PriceVerifierCustomFeed from examples directory
1 parent 389fbec commit 0129897

File tree

1 file changed

+7
-252
lines changed

1 file changed

+7
-252
lines changed

docs/fdc/guides/url-parsing-security.mdx

Lines changed: 7 additions & 252 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ title: URL Parsing Security
33
slug: url-parsing-security
44
authors: [amadiaflare]
55
description: Secure URL validation patterns for FDC Web2Json attestations to prevent MitM attacks.
6-
tags: [advanced, security, fdc, web2json]
6+
tags: [advanced, fdc, solidity]
77
keywords:
88
[flare-data-connector, security, url-parsing, mitm-prevention, web2json]
99
sidebar_position: 10
1010
---
1111

12+
import CodeBlock from "@theme/CodeBlock";
13+
import PriceVerifierCustomFeed from "!!raw-loader!/examples/developer-hub-solidity/PriceVerifierCustomFeed.sol";
14+
1215
# URL Parsing Security for FDC
1316

1417
When using the `Web2Json` attestation type, your smart contract receives data along with a proof that includes the source URL.
@@ -413,257 +416,9 @@ For production-ready examples, see:
413416

414417
- [PriceVerifierCustomFeed](https://github.com/flare-foundation/flare-hardhat-starter/blob/main/contracts/customFeeds/PriceVerifierCustomFeed.sol) - Asset ID extraction from CoinGecko URLs
415418

416-
<details>
417-
<summary>Full PriceVerifierCustomFeed Contract</summary>
418-
419-
```solidity title="contracts/customFeeds/PriceVerifierCustomFeed.sol"
420-
// SPDX-License-Identifier: MIT
421-
pragma solidity ^0.8.25;
422-
423-
import { ContractRegistry } from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
424-
import { IWeb2Json } from "@flarenetwork/flare-periphery-contracts/coston2/IWeb2Json.sol";
425-
import { IICustomFeed } from "@flarenetwork/flare-periphery-contracts/coston2/customFeeds/interfaces/IICustomFeed.sol";
426-
427-
struct PriceData {
428-
uint256 price;
429-
}
430-
431-
/**
432-
* @title PriceVerifierCustomFeed
433-
* @notice An FTSO Custom Feed contract that sources its value from FDC-verified data using Web2Json.
434-
* @dev Implements the IICustomFeed interface and includes verification logic specific to Web2Json API structure.
435-
*/
436-
contract PriceVerifierCustomFeed is IICustomFeed {
437-
// --- State Variables ---
438-
439-
bytes21 public immutable feedIdentifier;
440-
string public expectedSymbol;
441-
int8 public decimals_;
442-
uint256 public latestVerifiedPrice;
443-
444-
address public owner;
445-
mapping(bytes32 => string) public symbolToCoinGeckoId;
446-
447-
// --- Events ---
448-
event PriceVerified(string indexed symbol, uint256 price, string apiUrl);
449-
event UrlParsingCheck(string apiUrl, string coinGeckoId, string dateString);
450-
event CoinGeckoIdMappingSet(bytes32 indexed symbolHash, string coinGeckoId);
451-
452-
// --- Errors ---
453-
error InvalidFeedId();
454-
error InvalidSymbol();
455-
error UrlCoinGeckoIdMismatchExpected();
456-
error CoinGeckoIdParsingFailed();
457-
error UnknownSymbolForCoinGeckoId(); // Kept for direct call if needed, but mapping is primary
458-
error CoinGeckoIdNotMapped(string symbol);
459-
error DateStringParsingFailed();
460-
error InvalidCoinGeckoIdInUrl(string url, string extractedId, string expectedId);
461-
error InvalidProof();
462-
463-
// --- Constructor ---
464-
constructor(bytes21 _feedId, string memory _expectedSymbol, int8 _decimals) {
465-
if (_feedId == bytes21(0)) revert InvalidFeedId();
466-
if (bytes(_expectedSymbol).length == 0) revert InvalidSymbol();
467-
if (_decimals < 0) revert InvalidSymbol();
468-
469-
owner = msg.sender;
470-
feedIdentifier = _feedId;
471-
expectedSymbol = _expectedSymbol;
472-
decimals_ = _decimals;
473-
474-
// Initialize default CoinGecko IDs
475-
_setCoinGeckoIdInternal("BTC", "bitcoin");
476-
_setCoinGeckoIdInternal("ETH", "ethereum");
477-
478-
// Ensure the expected symbol has a mapping at deployment time
479-
require(
480-
bytes(symbolToCoinGeckoId[keccak256(abi.encodePacked(_expectedSymbol))]).length > 0,
481-
"Initial symbol not mapped"
482-
);
483-
}
484-
485-
// --- Owner Functions ---
486-
/**
487-
* @notice Allows the owner to add or update a CoinGecko ID mapping for a symbol.
488-
* @param _symbol The trading symbol (e.g., "LTC").
489-
* @param _coinGeckoId The corresponding CoinGecko ID (e.g., "litecoin").
490-
*/
491-
function setCoinGeckoIdMapping(string calldata _symbol, string calldata _coinGeckoId) external {
492-
_setCoinGeckoIdInternal(_symbol, _coinGeckoId);
493-
}
494-
495-
// --- FDC Verification & Price Logic ---
496-
/**
497-
* @notice Verifies the price data proof and stores the price.
498-
* @dev Uses Web2Json FDC verification. Checks if the symbol in the URL matches expectedSymbol.
499-
* @param _proof The IWeb2Json.Proof data structure.
500-
*/
501-
function verifyPrice(IWeb2Json.Proof calldata _proof) external {
502-
// 1. CoinGecko ID Verification (from URL)
503-
string memory extractedCoinGeckoId = _extractCoinGeckoIdFromUrl(_proof.data.requestBody.url);
504-
505-
string memory expectedCoinGeckoId = symbolToCoinGeckoId[keccak256(abi.encodePacked(expectedSymbol))];
506-
507-
if (bytes(expectedCoinGeckoId).length == 0) {
508-
revert CoinGeckoIdNotMapped(expectedSymbol);
509-
}
510-
511-
if (keccak256(abi.encodePacked(extractedCoinGeckoId)) != keccak256(abi.encodePacked(expectedCoinGeckoId))) {
512-
revert InvalidCoinGeckoIdInUrl(_proof.data.requestBody.url, extractedCoinGeckoId, expectedCoinGeckoId);
513-
}
514-
515-
// 2. FDC Verification (Web2Json)
516-
require(ContractRegistry.getFdcVerification().verifyWeb2Json(_proof), "FDC: Invalid Web2Json proof");
517-
518-
// 3. Decode Price Data
519-
PriceData memory newPriceData = abi.decode(_proof.data.responseBody.abiEncodedData, (PriceData));
520-
521-
// 4. Store verified data
522-
latestVerifiedPrice = newPriceData.price;
523-
524-
// 5. Emit main event
525-
emit PriceVerified(
526-
expectedSymbol,
527-
newPriceData.price,
528-
_proof.data.requestBody.url // URL from the Web2Json proof
529-
);
530-
}
531-
532-
// --- Custom Feed Logic ---
533-
534-
function getCurrentFeed() external payable override returns (uint256 _value, int8 _decimals, uint64 _timestamp) {
535-
_value = latestVerifiedPrice;
536-
_decimals = decimals_;
537-
_timestamp = 0;
538-
}
539-
540-
function feedId() external view override returns (bytes21 _feedId) {
541-
_feedId = feedIdentifier;
542-
}
543-
544-
function getFeedDataView() external view returns (uint256 _value, int8 _decimals) {
545-
_value = latestVerifiedPrice;
546-
_decimals = decimals_;
547-
}
548-
549-
function decimals() external view returns (int8) {
550-
return decimals_;
551-
}
552-
553-
function calculateFee() external pure override returns (uint256 _fee) {
554-
return 0;
555-
}
556-
557-
function read() public view returns (uint256 value) {
558-
value = latestVerifiedPrice;
559-
}
560-
// --- Internal Helper Functions ---
561-
562-
function _setCoinGeckoIdInternal(string memory _symbol, string memory _coinGeckoId) internal {
563-
require(bytes(_symbol).length > 0, "Symbol cannot be empty");
564-
require(bytes(_coinGeckoId).length > 0, "CoinGecko ID cannot be empty");
565-
bytes32 symbolHash = keccak256(abi.encodePacked(_symbol));
566-
symbolToCoinGeckoId[symbolHash] = _coinGeckoId;
567-
emit CoinGeckoIdMappingSet(symbolHash, _coinGeckoId);
568-
}
569-
570-
// --- Internal Helper Functions To Parse URL---
571-
572-
/**
573-
* @notice Helper function to extract a slice of bytes.
574-
* @param data The original bytes array.
575-
* @param start The starting index (inclusive).
576-
* @param end The ending index (exclusive).
577-
* @return The sliced bytes.
578-
*/
579-
function slice(bytes memory data, uint256 start, uint256 end) internal pure returns (bytes memory) {
580-
require(end >= start, "Slice: end before start");
581-
require(data.length >= end, "Slice: end out of bounds");
582-
bytes memory result = new bytes(end - start);
583-
for (uint256 i = start; i < end; i++) {
584-
result[i - start] = data[i];
585-
}
586-
return result;
587-
}
588-
589-
/**
590-
* @notice Extracts the CoinGecko ID from the API URL.
591-
* @dev It assumes the URL format is like ".../coins/{id}/history..." or ".../coins/{id}"
592-
* @param _url The full URL string from the proof.
593-
* @return The extracted CoinGecko ID.
594-
*/
595-
function _extractCoinGeckoIdFromUrl(string memory _url) internal pure returns (string memory) {
596-
bytes memory urlBytes = bytes(_url);
597-
bytes memory prefix = bytes("/coins/");
598-
bytes memory suffix = bytes("/history");
599-
600-
uint256 startIndex = _indexOf(urlBytes, prefix);
601-
if (startIndex == type(uint256).max) {
602-
return ""; // Prefix not found
603-
}
604-
startIndex += prefix.length;
605-
606-
uint256 endIndex = _indexOfFrom(urlBytes, suffix, startIndex);
607-
if (endIndex == type(uint256).max) {
608-
// Suffix not found, assume it's the end of the string
609-
endIndex = urlBytes.length;
610-
}
611-
612-
return string(slice(urlBytes, startIndex, endIndex));
613-
}
614-
615-
/**
616-
* @notice Helper to find the first occurrence of a marker in bytes.
617-
* @param data The bytes data to search in.
618-
* @param marker The bytes marker to find.
619-
* @return The starting index of the marker, or type(uint256).max if not found.
620-
*/
621-
function _indexOf(bytes memory data, bytes memory marker) internal pure returns (uint256) {
622-
uint256 dataLen = data.length;
623-
uint256 markerLen = marker.length;
624-
if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;
625-
626-
for (uint256 i = 0; i <= dataLen - markerLen; i++) {
627-
bool found = true;
628-
for (uint256 j = 0; j < markerLen; j++) {
629-
if (data[i + j] != marker[j]) {
630-
found = false;
631-
break;
632-
}
633-
}
634-
if (found) return i;
635-
}
636-
return type(uint256).max;
637-
}
638-
639-
/**
640-
* @notice Helper to find the first occurrence of a marker in bytes, starting from an index.
641-
* @param data The bytes data to search in.
642-
* @param marker The bytes marker to find.
643-
* @param from The index to start searching from.
644-
* @return The starting index of the marker, or type(uint256).max if not found.
645-
*/
646-
function _indexOfFrom(bytes memory data, bytes memory marker, uint256 from) internal pure returns (uint256) {
647-
uint256 dataLen = data.length;
648-
uint256 markerLen = marker.length;
649-
if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;
650-
651-
for (uint256 i = from; i <= dataLen - markerLen; i++) {
652-
bool found = true;
653-
for (uint256 j = 0; j < markerLen; j++) {
654-
if (data[i + j] != marker[j]) {
655-
found = false;
656-
break;
657-
}
658-
}
659-
if (found) return i;
660-
}
661-
return type(uint256).max;
662-
}
663-
}
664-
```
665-
666-
</details>
419+
<CodeBlock language="solidity" title="PriceVerifierCustomFeed.sol">
420+
{PriceVerifierCustomFeed}
421+
</CodeBlock>
667422

668423
## Summary
669424

0 commit comments

Comments
 (0)