Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions contracts/feeds/USDT0PriceFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
pragma solidity 0.5.17;

/**
* @title USDT0 Price Feed Wrapper
* @notice Wraps the Redstone USDT price feed and normalizes from 8 decimals to 18 decimals
* for compatibility with Sovryn's PriceFeeds contract.
*
* Redstone USDT Price Feed on RSK Mainnet: 0x09639692ce6Ff12a06cA3AE9a24B3aAE4cD80dc8
* Contract: RootstockPriceFeedUsdtWithoutRoundsV1
* - latestRoundData() returns (roundId, answer, startedAt, updatedAt, answeredInRound)
* - answer is in 8 decimals
* - decimals() returns 8
*
* This wrapper:
* 1. Validates price data is not stale or invalid
* 2. Scales the price from 8 decimals to 18 decimals for Sovryn compatibility
*/

interface IRedstoneOracle {
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
function decimals() external view returns (uint8);
}

contract USDT0PriceFeed {
IRedstoneOracle public oracle;

/// @dev Maximum acceptable age for price data (24 hours)
/// After this time, price is considered stale (referred to the heartbet of redstone)
uint256 public constant MAX_PRICE_AGE = 24 hours;

/**
* @notice Constructor
* @param _oracle The address of the Redstone USDT price feed (0x09639692ce6Ff12a06cA3AE9a24B3aAE4cD80dc8)
*/
constructor(address _oracle) public {
require(_oracle != address(0), "Invalid oracle address");
oracle = IRedstoneOracle(_oracle);
}

/**
* @notice Get the latest price from Redstone oracle and scale to 18 decimals
* @dev Performs validation checks on the oracle data:
* - Price must be greater than 0
* - Price must not be stale (updated within MAX_PRICE_AGE)
* - Round must be complete (answeredInRound >= roundId)
*
* Redstone returns 8 decimals (e.g., 99937000 = $0.99937)
* We scale to 18 decimals (e.g., 999370000000000000 = $0.99937)
*
* @return The validated price with 18 decimals
*/
function latestAnswer() external view returns (uint256) {
(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = oracle
.latestRoundData();

// Validate price data
require(answer > 0, "Invalid price: answer <= 0");
require(updatedAt > 0, "Invalid price: updatedAt = 0");
require(answeredInRound >= roundId, "Stale price: round not complete");
require(block.timestamp - updatedAt <= MAX_PRICE_AGE, "Stale price: too old");

uint256 price = uint256(answer);
uint8 oracleDecimals = oracle.decimals();

// Scale from oracle decimals to 18 decimals
// price * 10^(18 - oracleDecimals)
if (oracleDecimals < 18) {
return price * (10 ** (18 - uint256(oracleDecimals)));
} else if (oracleDecimals > 18) {
return price / (10 ** (uint256(oracleDecimals) - 18));
} else {
return price;
}
}
}
48 changes: 48 additions & 0 deletions contracts/mockup/MockRedstoneOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Mock Redstone Oracle for testing USDT0PriceFeed wrapper
* Simulates Redstone's USDT price feed which returns 8 decimals
* Implements latestRoundData() for realistic testing
*/

pragma solidity 0.5.17;

contract MockRedstoneOracle {
int256 private price;
uint80 private currentRound;
uint256 private lastUpdateTime;

constructor() public {
price = 100000000; // $1.00 with 8 decimals
currentRound = 1;
lastUpdateTime = block.timestamp;
}

function setPrice(int256 _price) external {
price = _price;
currentRound++;
lastUpdateTime = block.timestamp;
}

function setUpdatedAt(uint256 _timestamp) external {
lastUpdateTime = _timestamp;
}

function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return (currentRound, price, lastUpdateTime, lastUpdateTime, currentRound);
}

/// @dev Always returns 8 decimals to match real Redstone oracle
function decimals() external pure returns (uint8) {
return 8;
}
}
77 changes: 77 additions & 0 deletions deployment/deploy/2170-deploy-USDT0PriceFeed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const hre = require("hardhat");
const { setDeploymentMetaData } = require("../helpers/helpers");

/**
* Deploy USDT0PriceFeed wrapper contract
*
* This wrapper:
* 1. Normalizes Redstone USDT price feed from 8 decimals to 18 decimals
* 2. Validates price data to prevent stale/invalid prices (security-critical)
*
* Security features:
* - Uses latestRoundData() for full validation
* - Checks answer > 0 (prevents zero price exploits)
* - Validates freshness (24-hour staleness check)
* - Verifies round completion (prevents incomplete data usage)
*
* Redstone USDT Price Feed on RSK Mainnet: 0x09639692ce6Ff12a06cA3AE9a24B3aAE4cD80dc8
*/
module.exports = async (hre) => {
const { deployments, getNamedAccounts } = hre;
const { deploy, get } = deployments;
const { deployer } = await getNamedAccounts();

// Get Redstone USDT oracle from deployment
const redstoneOracle = await get("RedStoneUSDT0Oracle");

console.log("\n--- Deploying USDT0PriceFeed Wrapper ---");
console.log(`Deployer: ${deployer}`);
console.log(`Redstone Oracle: ${redstoneOracle.address}`);

const usdt0PriceFeed = await deploy("USDT0PriceFeeds", {
contract: "USDT0PriceFeed",
from: deployer,
args: [redstoneOracle.address],
log: true,
skipIfAlreadyDeployed: true,
});

if (usdt0PriceFeed.newlyDeployed) {
console.log(`✅ USDT0PriceFeed deployed at: ${usdt0PriceFeed.address}`);

// Verify the wrapper is working correctly
const USDT0PriceFeed = await hre.ethers.getContractAt(
"USDT0PriceFeed",
usdt0PriceFeed.address
);

try {
const price = await USDT0PriceFeed.latestAnswer();
console.log(` Price (18 decimals): ${hre.ethers.utils.formatEther(price)}`);
console.log(` Raw value: ${price.toString()}`);

// Expected: ~999370000000000000 (0.99937 with 18 decimals)
const expectedMin = hre.ethers.utils.parseEther("0.9"); // $0.90
const expectedMax = hre.ethers.utils.parseEther("1.1"); // $1.10

if (price.gte(expectedMin) && price.lte(expectedMax)) {
console.log(" ✅ Price feed is working correctly (within expected range)");
} else {
console.log(" ⚠️ Warning: Price is outside expected range ($0.90 - $1.10)");
}
} catch (error) {
console.log(` ⚠️ Warning: Could not verify price feed: ${error.message}`);
}

await setDeploymentMetaData("USDT0PriceFeeds", {
contractAddress: usdt0PriceFeed.address,
description: "USDT0 Price Feed Wrapper (Redstone -> 18 decimals)",
redstoneOracle: redstoneOracle.address,
});
} else {
console.log(`USDT0PriceFeed already deployed at: ${usdt0PriceFeed.address}`);
}
};

module.exports.tags = ["USDT0PriceFeed", "PriceFeeds"];
module.exports.dependencies = ["RedStoneUSDT0Oracle"];
Loading
Loading