@@ -3,12 +3,15 @@ title: URL Parsing Security
33slug : url-parsing-security
44authors : [amadiaflare]
55description : Secure URL validation patterns for FDC Web2Json attestations to prevent MitM attacks.
6- tags : [advanced, security, fdc, web2json ]
6+ tags : [advanced, fdc, solidity ]
77keywords :
88 [flare-data-connector, security, url-parsing, mitm-prevention, web2json]
99sidebar_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
1417When 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