feat: finish up challenges
Some checks failed
Lint / ci (lts/*, ubuntu-latest) (push) Has been cancelled

This commit is contained in:
han
2026-01-26 18:22:19 +07:00
parent b330aba2b4
commit c18c66cca1
22 changed files with 8397 additions and 58 deletions

View File

@@ -105,7 +105,25 @@ contract StakingOracle {
* @dev Creates a new OracleNode struct and adds the sender to the nodeAddresses array.
* Requires minimum stake amount and prevents duplicate registrations.
*/
function registerNode(uint256 amount) public {}
function registerNode(uint256 amount) public {
if (amount < MINIMUM_STAKE) revert InsufficientStake();
if (nodes[msg.sender].active) revert NodeAlreadyRegistered();
bool success = oracleToken.transferFrom(msg.sender, address(this), amount);
if (!success) revert TransferFailed();
nodes[msg.sender] = OracleNode({
stakedAmount: amount,
lastReportedBucket: 0,
reportCount: 0,
claimedReportCount: 0,
firstBucket: getCurrentBucketNumber(),
active: true
});
nodeAddresses.push(msg.sender);
emit NodeRegistered(msg.sender, amount);
}
/**
* @notice Updates the price reported by an oracle node (only registered nodes)
@@ -114,25 +132,69 @@ contract StakingOracle {
* This creates a chain of finalized buckets, ensuring all past reports are accountable.
* @param price The new price value to report
*/
function reportPrice(uint256 price) public onlyNode {}
function reportPrice(uint256 price) public onlyNode {
if (price == 0) revert InvalidPrice();
if (getEffectiveStake(msg.sender) < MINIMUM_STAKE) revert InsufficientStake();
uint256 currentBucket = getCurrentBucketNumber();
OracleNode storage n = nodes[msg.sender];
if (n.lastReportedBucket == currentBucket) revert AlreadyReportedInCurrentBucket();
BlockBucket storage bucket = blockBuckets[currentBucket];
bucket.reporters.push(msg.sender);
bucket.prices.push(price);
n.lastReportedBucket = currentBucket;
n.reportCount++;
emit PriceReported(msg.sender, price, currentBucket);
}
/**
* @notice Allows active and inactive nodes to claim accumulated ORA token rewards
* @dev Calculates rewards based on time elapsed since last claim.
*/
function claimReward() public {}
function claimReward() public {
OracleNode storage node = nodes[msg.sender];
uint256 delta = node.reportCount - node.claimedReportCount;
if (delta == 0) revert NoRewardsAvailable();
node.claimedReportCount = node.reportCount;
oracleToken.mint(msg.sender, delta * REWARD_PER_REPORT);
emit NodeRewarded(msg.sender, delta * REWARD_PER_REPORT);
}
/**
* @notice Allows a registered node to increase its ORA token stake
*/
function addStake(uint256 amount) public onlyNode {}
function addStake(uint256 amount) public onlyNode {
if (amount == 0) revert InsufficientStake();
bool success = oracleToken.transferFrom(msg.sender, address(this), amount);
if (!success) revert TransferFailed();
nodes[msg.sender].stakedAmount += amount;
emit StakeAdded(msg.sender, amount);
}
/**
* @notice Records the median price for a bucket once sufficient reports are available
* @dev Anyone who uses the oracle's price feed can call this function to record the median price for a bucket.
* @param bucketNumber The bucket number to finalize
*/
function recordBucketMedian(uint256 bucketNumber) public {}
function recordBucketMedian(uint256 bucketNumber) public {
uint256 currentBucket = getCurrentBucketNumber();
if (bucketNumber == currentBucket) revert OnlyPastBucketsAllowed();
BlockBucket storage bucket = blockBuckets[bucketNumber];
if (bucket.medianPrice != 0) revert BucketMedianAlreadyRecorded();
uint256[] memory prices = bucket.prices;
prices.sort();
uint256 medianPrice = prices.getMedian();
bucket.medianPrice = medianPrice;
emit BucketMedianRecorded(bucketNumber, medianPrice);
}
/**
* @notice Slashes a node for giving a price that is deviated too far from the average
@@ -146,7 +208,39 @@ contract StakingOracle {
uint256 bucketNumber,
uint256 reportIndex,
uint256 nodeAddressesIndex
) public {}
) public {
if (bucketNumber == getCurrentBucketNumber()) revert OnlyPastBucketsAllowed();
if (nodeAddressesIndex >= nodeAddresses.length) revert IndexOutOfBounds();
BlockBucket storage bucket = blockBuckets[bucketNumber];
address reporter = bucket.reporters[reportIndex];
if (bucket.slashedOffenses[reporter]) revert NodeAlreadySlashed();
if (bucket.medianPrice == 0) revert MedianNotRecorded();
if (reportIndex >= bucket.prices.length) revert IndexOutOfBounds();
if (reporter != nodeToSlash) revert NodeNotAtGivenIndex();
uint256 reportedPrice = bucket.prices[reportIndex];
if (reportedPrice == 0) revert NodeDidNotReport();
bool isOutlier = _checkPriceDeviated(reportedPrice, bucket.medianPrice);
if (!isOutlier) revert NotDeviated();
bucket.slashedOffenses[reporter] = true;
OracleNode storage node = nodes[nodeToSlash];
uint256 actualPenalty = MISREPORT_PENALTY > node.stakedAmount ? node.stakedAmount : MISREPORT_PENALTY;
node.stakedAmount -= actualPenalty;
if (node.stakedAmount == 0) {
_removeNode(nodeToSlash, nodeAddressesIndex);
emit NodeExited(nodeToSlash, 0);
}
uint256 reward = (actualPenalty * SLASHER_REWARD_PERCENTAGE) / 100;
bool rewardSent = oracleToken.transfer(msg.sender, reward);
if (!rewardSent) revert TransferFailed();
emit NodeSlashed(nodeToSlash, actualPenalty);
}
/**
* @notice Allows a registered node to exit the system and withdraw their stake
@@ -155,7 +249,24 @@ contract StakingOracle {
* node has been slashed if it reported a bad price before allowing it to exit.
* @param index The index of the node to remove in nodeAddresses
*/
function exitNode(uint256 index) public onlyNode {}
function exitNode(uint256 index) public onlyNode {
if (index >= nodeAddresses.length) revert IndexOutOfBounds();
if (nodeAddresses[index] != msg.sender) revert NodeNotAtGivenIndex();
OracleNode storage node = nodes[msg.sender];
if (!node.active) revert NodeNotRegistered();
if (node.lastReportedBucket + WAITING_PERIOD > getCurrentBucketNumber()) revert WaitingPeriodNotOver();
uint256 effectiveStake = getEffectiveStake(msg.sender);
_removeNode(msg.sender, index);
node.stakedAmount = 0;
node.active = false;
bool success = oracleToken.transfer(msg.sender, effectiveStake);
if (!success) revert TransferFailed();
emit NodeExited(msg.sender, effectiveStake);
}
////////////////////////
/// View Functions /////
@@ -174,21 +285,34 @@ contract StakingOracle {
* @notice Returns the list of registered oracle node addresses
* @return Array of registered oracle node addresses
*/
function getNodeAddresses() public view returns (address[] memory) {}
function getNodeAddresses() public view returns (address[] memory) {
return nodeAddresses;
}
/**
* @notice Returns the stored median price from the most recently completed bucket
* @dev Requires that the median for the bucket be recorded via recordBucketMedian
* @return The median price for the last finalized bucket
*/
function getLatestPrice() public view returns (uint256) {}
function getLatestPrice() public view returns (uint256) {
uint256 bucketNumber = getCurrentBucketNumber() - 1;
BlockBucket storage bucket = blockBuckets[bucketNumber];
if (bucket.medianPrice == 0) revert MedianNotRecorded();
return bucket.medianPrice;
}
/**
* @notice Returns the stored median price from a specified bucket
* @param bucketNumber The bucket number to read the median price from
* @return The median price stored for the bucket
*/
function getPastPrice(uint256 bucketNumber) public view returns (uint256) {}
function getPastPrice(uint256 bucketNumber) public view returns (uint256) {
BlockBucket storage bucket = blockBuckets[bucketNumber];
if (bucket.medianPrice == 0) revert MedianNotRecorded();
return bucket.medianPrice;
}
/**
* @notice Returns the price and slashed status of a node at a given bucket
@@ -200,20 +324,70 @@ contract StakingOracle {
function getSlashedStatus(
address nodeAddress,
uint256 bucketNumber
) public view returns (uint256 price, bool slashed) {}
) public view returns (uint256 price, bool slashed) {
BlockBucket storage bucket = blockBuckets[bucketNumber];
for (uint256 i = 0; i < bucket.reporters.length; i++) {
if (bucket.reporters[i] == nodeAddress) {
price = bucket.prices[i];
slashed = bucket.slashedOffenses[nodeAddress];
}
}
}
/**
* @notice Returns the effective stake accounting for inactivity penalties via missed buckets
* @dev Effective stake = stakedAmount - (missedBuckets * INACTIVITY_PENALTY), floored at 0
*/
function getEffectiveStake(address nodeAddress) public view returns (uint256) {}
function getEffectiveStake(address nodeAddress) public view returns (uint256) {
OracleNode memory n = nodes[nodeAddress]; // get node detail
if (!n.active) return 0; // inactive node
uint256 currentBucket = getCurrentBucketNumber();
if (currentBucket == n.firstBucket) return n.stakedAmount; // basically the node just registered itself
uint256 expectedReports = currentBucket - n.firstBucket; // get the amount of "buckets" since registration
uint256 actualReportsCompleted = n.reportCount;
if (n.lastReportedBucket == currentBucket && actualReportsCompleted > 0) {
actualReportsCompleted -= 1; // we remove the report from current bucket
}
if (actualReportsCompleted >= expectedReports) return n.stakedAmount; // no penalty
uint256 missed = expectedReports - actualReportsCompleted;
uint256 penalty = missed * INACTIVITY_PENALTY;
if (penalty > n.stakedAmount) return 0; // the amount of penalty is more than the staked amount
return n.stakedAmount - penalty;
}
/**
* @notice Returns the addresses of nodes in a bucket whose reported price deviates beyond the threshold
* @param bucketNumber The bucket number to get the outliers from
* @return Array of node addresses considered outliers
*/
function getOutlierNodes(uint256 bucketNumber) public view returns (address[] memory) {}
function getOutlierNodes(uint256 bucketNumber) public view returns (address[] memory) {
BlockBucket storage bucket = blockBuckets[bucketNumber];
if (bucket.medianPrice == 0) revert MedianNotRecorded();
address[] memory outliers = new address[](bucket.reporters.length);
uint256 outlierCount = 0;
for (uint256 i = 0; i < bucket.reporters.length; i++) {
address reporter = bucket.reporters[i];
if (bucket.slashedOffenses[reporter]) continue;
uint256 reportedPrice = bucket.prices[i];
if (reportedPrice == 0) continue;
bool isOutlier = _checkPriceDeviated(reportedPrice, bucket.medianPrice);
if (isOutlier) {
outliers[outlierCount] = reporter;
outlierCount++;
}
}
address[] memory fixedOutliers = new address[](outlierCount);
for (uint256 i = 0; i < outlierCount; i++) {
fixedOutliers[i] = outliers[i];
}
return fixedOutliers;
}
//////////////////////////
/// Internal Functions ///
@@ -224,7 +398,18 @@ contract StakingOracle {
* @param nodeAddress The address of the node to remove
* @param index The index of the node to remove
*/
function _removeNode(address nodeAddress, uint256 index) internal {}
function _removeNode(address nodeAddress, uint256 index) internal {
if (index >= nodeAddresses.length) revert IndexOutOfBounds();
address storedNodeAddress = nodeAddresses[index];
if (storedNodeAddress != nodeAddress) revert NodeNotAtGivenIndex();
if (index != nodeAddresses.length - 1) {
nodeAddresses[index] = nodeAddresses[nodeAddresses.length - 1];
}
nodeAddresses.pop();
nodes[nodeAddress].active = false;
}
/**
* @notice Checks if the price deviation is greater than the threshold
@@ -232,5 +417,9 @@ contract StakingOracle {
* @param medianPrice The average price of the bucket
* @return True if the price deviation is greater than the threshold, false otherwise
*/
function _checkPriceDeviated(uint256 reportedPrice, uint256 medianPrice) internal pure returns (bool) {}
function _checkPriceDeviated(uint256 reportedPrice, uint256 medianPrice) internal pure returns (bool) {
uint256 deviation = reportedPrice > medianPrice ? reportedPrice - medianPrice : medianPrice - reportedPrice;
uint256 deviationBps = (deviation * 10_000) / medianPrice;
return deviationBps > MAX_DEVIATION_BPS;
}
}