From 0b8ff1f8e6879c35d51c7d7bde56f0400790962d Mon Sep 17 00:00:00 2001 From: Nick <148735107+nick-bisonai@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:01:10 +0900 Subject: [PATCH] Update contract (#2314) * feat: update contract * fix: update based on feedback * fix: separated error cases * fix: add function based on feedback * fix: update based on feedback * Review of superseded feature (#2315) * chore: superseeded review * fix: fix test --------- Co-authored-by: nick * fix: remove unnecessary * fix: rename, update test * fix: update visibility * fix: fix typo * fix: typo --------- Co-authored-by: Martin Kersner --- contracts/v0.2/src/SubmissionProxy.sol | 116 ++++++++++++++++++++-- contracts/v0.2/test/SubmissionProxy.t.sol | 100 +++++++++++++++++++ contracts/v0.2/test/SubmitStrict.t.sol | 87 ++++++++-------- 3 files changed, 250 insertions(+), 53 deletions(-) diff --git a/contracts/v0.2/src/SubmissionProxy.sol b/contracts/v0.2/src/SubmissionProxy.sol index e140153a1..d69d084bd 100644 --- a/contracts/v0.2/src/SubmissionProxy.sol +++ b/contracts/v0.2/src/SubmissionProxy.sol @@ -37,8 +37,8 @@ contract SubmissionProxy is Ownable { mapping(address => OracleInfo) public whitelist; mapping(bytes32 feedHash => IFeed feed) public feeds; - mapping(bytes32 feedHash => uint8 threshold) thresholds; - mapping(bytes32 feedHash => uint256 lastSubmissionTime) lastSubmissionTimes; + mapping(bytes32 feedHash => uint8 threshold) public thresholds; + mapping(bytes32 feedHash => uint256 lastSubmissionTime) public lastSubmissionTimes; event OracleAdded(address oracle, uint256 expirationTime); event OracleRemoved(address oracle); @@ -61,7 +61,8 @@ contract SubmissionProxy is Ownable { error InvalidSignatureLength(); error InvalidFeed(); error ZeroAddressGiven(); - error AnswerTooOld(); + error AnswerOutdated(); + error AnswerSuperseded(); error InvalidProofFormat(); error InvalidProof(); error FeedHashNotFound(); @@ -396,6 +397,15 @@ contract SubmissionProxy is Ownable { } } + /** + * @notice Submit a batch of answers to multiple feeds. If any + * of the answers do not meet required conditions the + * whole batch is reverted. + * @param _feedHashes The hashes of the feeds + * @param _answers The submissions + * @param _timestamps The unixmilli timestamps of the proofs + * @param _proofs The proofs + */ function submitStrict( bytes32[] calldata _feedHashes, int256[] calldata _answers, @@ -411,13 +421,105 @@ contract SubmissionProxy is Ownable { uint256 feedsLength_ = _feedHashes.length; for (uint256 i = 0; i < feedsLength_; i++) { - submitSingle(_feedHashes[i], _answers[i], _timestamps[i], _proofs[i]); + submitStrictSingle(_feedHashes[i], _answers[i], _timestamps[i], _proofs[i]); + } + } + + /** + * @notice Submit a single answer to a feed. The answer is + * reverted if any of the required conditions are not met. + * @param _feedHash The hash of the feed + * @param _answer The submission + * @param _timestamp The unixmilli timestamp of the proof + * @param _proof The proof + */ + function submitStrictSingle(bytes32 _feedHash, int256 _answer, uint256 _timestamp, bytes calldata _proof) public { + if (_timestamp <= (block.timestamp - dataFreshness) * 1000) { + // answer is not fresh -> do not submit! + revert AnswerOutdated(); + } + + if (lastSubmissionTimes[_feedHash] >= _timestamp) { + // answer is superseded -> do not submit! + revert AnswerSuperseded(); + } + + (bytes[] memory proofs_, bool success_) = splitProofs(_proof); + if (!success_) { + // splitting proofs failed -> do not submit! + revert InvalidProofFormat(); + } + + if (address(feeds[_feedHash]) == address(0)) { + // feedHash not registered -> do not submit! + revert FeedHashNotFound(); + } + + if (keccak256(abi.encodePacked(feeds[_feedHash].name())) != _feedHash) { + // feedHash not matching with registered feed -> do not submit! + revert InvalidFeedHash(); + } + + bytes32 message_ = keccak256(abi.encodePacked(_answer, _timestamp, _feedHash)); + if (validateProof(_feedHash, message_, proofs_)) { + feeds[_feedHash].submit(_answer); + lastSubmissionTimes[_feedHash] = _timestamp; + } else { + revert InvalidProof(); + } + } + + /** + * @notice Submit a batch of answers to multiple feeds. The answers are + * ignored if they have been superseded. If any of the answers do not + * meet the rest of required conditions the whole batch is reverted. + * @param _feedHashes The hashes of the feeds + * @param _answers The submissions + * @param _timestamps The unixmilli timestamps of the proofs + * @param _proofs The proofs + */ + function submitWithoutSupersedValidation( + bytes32[] calldata _feedHashes, + int256[] calldata _answers, + uint256[] calldata _timestamps, + bytes[] calldata _proofs + ) external { + if ( + _feedHashes.length != _answers.length || _answers.length != _proofs.length + || _proofs.length != _timestamps.length || _feedHashes.length > maxSubmission + ) { + revert InvalidSubmissionLength(); + } + + uint256 feedsLength_ = _feedHashes.length; + for (uint256 i = 0; i < feedsLength_; i++) { + submitSingleWithoutSupersedValidation(_feedHashes[i], _answers[i], _timestamps[i], _proofs[i]); } } - function submitSingle(bytes32 _feedHash, int256 _answer, uint256 _timestamp, bytes calldata _proof) public { - if (_timestamp <= (block.timestamp - dataFreshness) * 1000 || lastSubmissionTimes[_feedHash] >= _timestamp) { - revert AnswerTooOld(); + /** + * @notice Submit a single submission to a feed. The submission is + * ignored if it has been superseded. If the submission does not + * meet the rest of required conditions the submission is reverted. + * @param _feedHash The hash of the feed + * @param _answer The submission + * @param _timestamp The unixmilli timestamp of the proof + * @param _proof The proof + */ + function submitSingleWithoutSupersedValidation( + bytes32 _feedHash, + int256 _answer, + uint256 _timestamp, + bytes calldata _proof + ) public { + if (lastSubmissionTimes[_feedHash] >= _timestamp) { + // answer is superseded -> skip submission + return; + } + + if (_timestamp <= (block.timestamp - dataFreshness) * 1000) { + // answer is not fresh -> do not submit! + revert AnswerOutdated(); } (bytes[] memory proofs_, bool success_) = splitProofs(_proof); diff --git a/contracts/v0.2/test/SubmissionProxy.t.sol b/contracts/v0.2/test/SubmissionProxy.t.sol index 88662874b..039fd8de9 100644 --- a/contracts/v0.2/test/SubmissionProxy.t.sol +++ b/contracts/v0.2/test/SubmissionProxy.t.sol @@ -433,6 +433,106 @@ contract SubmissionProxyTest is Test { IFeed(feeds_[0]).latestRoundData(); } + function test_SubmitStrict() public { + (address alice_, uint256 aliceSk_) = makeAddrAndKey("alice"); + (address bob_, uint256 bobSk_) = makeAddrAndKey("bob"); + (address celine_, uint256 celineSk_) = makeAddrAndKey("celine"); + (, uint256 dummySk_) = makeAddrAndKey("dummy"); + + submissionProxy.addOracle(alice_); + submissionProxy.addOracle(bob_); + submissionProxy.addOracle(celine_); + + uint256 numOracles_ = 1; + int256 submissionValue_ = 10; + ( + bytes32[] memory feedHashes_, + int256[] memory submissions_, + bytes[] memory proofs_, + uint256[] memory timestamps_, + address[] memory feeds_ + ) = prepareFeedsSubmissions(numOracles_, submissionValue_, dummySk_); + bytes32 hash_ = keccak256(abi.encodePacked(submissions_[0], timestamps_[0], feedHashes_[0])); + proofs_[0] = + abi.encodePacked(createProof(aliceSk_, hash_), createProof(bobSk_, hash_), createProof(celineSk_, hash_)); + + submissionProxy.setProofThreshold(feedHashes_[0], 100); // 100 % of the oracles must submit a valid proof + submissionProxy.submitStrict(feedHashes_, submissions_, timestamps_, proofs_); + + // don't raise `NoDataPresent` + IFeed(feeds_[0]).latestRoundData(); + } + + function test_submitWithoutSupersedValidation() public { + (address alice_, uint256 aliceSk_) = makeAddrAndKey("alice"); + (address bob_, uint256 bobSk_) = makeAddrAndKey("bob"); + (address celine_, uint256 celineSk_) = makeAddrAndKey("celine"); + (, uint256 dummySk_) = makeAddrAndKey("dummy"); + + submissionProxy.addOracle(alice_); + submissionProxy.addOracle(bob_); + submissionProxy.addOracle(celine_); + + uint256 numOracles_ = 1; + int256 submissionValue_ = 10; + ( + bytes32[] memory feedHashes_, + int256[] memory submissions_, + bytes[] memory proofs_, + uint256[] memory timestamps_, + address[] memory feeds_ + ) = prepareFeedsSubmissions(numOracles_, submissionValue_, dummySk_); + bytes32 hash_ = keccak256(abi.encodePacked(submissions_[0], timestamps_[0], feedHashes_[0])); + proofs_[0] = + abi.encodePacked(createProof(aliceSk_, hash_), createProof(bobSk_, hash_), createProof(celineSk_, hash_)); + + submissionProxy.setProofThreshold(feedHashes_[0], 100); // 100 % of the oracles must submit a valid proof + submissionProxy.submitWithoutSupersedValidation(feedHashes_, submissions_, timestamps_, proofs_); + + // don't raise `NoDataPresent` + IFeed(feeds_[0]).latestRoundData(); + } + + function test_submitWithoutSupersedValidationIgnoreLateSubmission() public { + + + (address alice_, uint256 aliceSk_) = makeAddrAndKey("alice"); + (address bob_, uint256 bobSk_) = makeAddrAndKey("bob"); + (address celine_, uint256 celineSk_) = makeAddrAndKey("celine"); + (, uint256 dummySk_) = makeAddrAndKey("dummy"); + + submissionProxy.addOracle(alice_); + submissionProxy.addOracle(bob_); + submissionProxy.addOracle(celine_); + + uint256 numOracles_ = 1; + int256 submissionValue_ = 10; + + ( + bytes32[] memory feedHashes_, + int256[] memory submissions_, + bytes[] memory proofs_, + uint256[] memory timestamps_, + address[] memory feeds_ + ) = prepareFeedsSubmissions(numOracles_, submissionValue_, dummySk_); + bytes32 hash_ = keccak256(abi.encodePacked(submissions_[0], timestamps_[0], feedHashes_[0])); + proofs_[0] = + abi.encodePacked(createProof(aliceSk_, hash_), createProof(bobSk_, hash_), createProof(celineSk_, hash_)); + + submissionProxy.setProofThreshold(feedHashes_[0], 100); // 100 % of the oracles must submit a valid proof + submissionProxy.submitWithoutSupersedValidation(feedHashes_, submissions_, timestamps_, proofs_); + + uint256[] memory oldTimestamps = new uint256[](timestamps_.length); + for (uint256 i = 0; i < timestamps_.length; i++) { + oldTimestamps[i] = timestamps_[i] -1; + } + // should not revert + submissionProxy.submitWithoutSupersedValidation(feedHashes_, submissions_, oldTimestamps, proofs_); + + // don't raise `NoDataPresent` + IFeed(feeds_[0]).latestRoundData(); + } + function prepareFeedsSubmissions(uint256 _numOracles, int256 _submissionValue, uint256 _oracleSk) private returns (bytes32[] memory, int256[] memory, bytes[] memory, uint256[] memory, address[] memory) diff --git a/contracts/v0.2/test/SubmitStrict.t.sol b/contracts/v0.2/test/SubmitStrict.t.sol index 898edfdc2..21d189f18 100644 --- a/contracts/v0.2/test/SubmitStrict.t.sol +++ b/contracts/v0.2/test/SubmitStrict.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import "forge-std/Vm.sol"; import {Test, console} from "forge-std/Test.sol"; -import { stdJson } from "forge-std/StdJson.sol"; +import {stdJson} from "forge-std/StdJson.sol"; import {SubmissionProxy} from "../src/SubmissionProxy.sol"; import {FeedRouter} from "../src/FeedRouter.sol"; import "forge-std/console.sol"; @@ -12,73 +12,68 @@ contract SubmitStrictTest is Test { using stdJson for string; function run() public { - string memory json = vm.readFile("submission.json"); + string memory json = vm.readFile("submission.json"); - string[] memory symbols = getsymbols(json); - bytes32[] memory feedHashes = gethashes(json); - int256[] memory valuesInt = getvalues(json); - uint256[] memory timestampsInt = gettimestamps(json); - bytes[] memory proofs = getproofs(json); + string[] memory symbols = getsymbols(json); + bytes32[] memory feedHashes = gethashes(json); + int256[] memory valuesInt = getvalues(json); + uint256[] memory timestampsInt = gettimestamps(json); + bytes[] memory proofs = getproofs(json); - console.log("submission"); - console.logBytes32(feedHashes[0]); - console.logInt(valuesInt[0]); - console.logUint(timestampsInt[0]); - console.logBytes(proofs[0]); + console.log("submission"); + console.logBytes32(feedHashes[0]); + console.logInt(valuesInt[0]); + console.logUint(timestampsInt[0]); + console.logBytes(proofs[0]); - SubmissionProxy sp = SubmissionProxy(0x3a251c738e19806A546815eb6065e139A8D65B4b); - sp.submitStrict( - feedHashes, - valuesInt, - timestampsInt, - proofs - ); + SubmissionProxy sp = SubmissionProxy(0x3a251c738e19806A546815eb6065e139A8D65B4b); + sp.submitStrict(feedHashes, valuesInt, timestampsInt, proofs); - console.log(block.timestamp); + console.log(block.timestamp); - FeedRouter fr = FeedRouter(0x653078F0D3a230416A59aA6486466470Db0190A2); - (uint64 roundId, int256 answer, uint256 blockTimestamp) = fr.latestRoundData(symbols[0]); + FeedRouter fr = FeedRouter(0x653078F0D3a230416A59aA6486466470Db0190A2); + (uint64 roundId, int256 answer, uint256 blockTimestamp) = fr.latestRoundData(symbols[0]); - console.log("latestRoundData"); - console.logUint(roundId); - console.logInt(answer); - console.logUint(blockTimestamp); + console.log("latestRoundData"); + console.logUint(roundId); + console.logInt(answer); + console.logUint(blockTimestamp); - require(block.timestamp == blockTimestamp, "Timestamps do not match"); + require(block.timestamp == blockTimestamp, "Timestamps do not match"); } function getsymbols(string memory json) public pure returns (string[] memory) { - bytes memory rawSymbols = json.parseRaw(".symbols"); - return abi.decode(rawSymbols, (string[])); + bytes memory rawSymbols = json.parseRaw(".symbols"); + return abi.decode(rawSymbols, (string[])); } function gethashes(string memory json) public pure returns (bytes32[] memory) { - bytes memory rawFeedHashes = json.parseRaw(".feedHashes"); + bytes memory rawFeedHashes = json.parseRaw(".feedHashes"); return abi.decode(rawFeedHashes, (bytes32[])); } function getvalues(string memory json) public pure returns (int256[] memory) { - bytes memory rawValues = json.parseRaw(".values"); - string[] memory values = abi.decode(rawValues, (string[])); - int256[] memory valuesInt = new int256[](values.length); - for (uint i = 0; i < values.length; i++) { - valuesInt[i] = vm.parseInt(values[i]); - } - return valuesInt; + bytes memory rawValues = json.parseRaw(".values"); + string[] memory values = abi.decode(rawValues, (string[])); + int256[] memory valuesInt = new int256[](values.length); + for (uint256 i = 0; i < values.length; i++) { + valuesInt[i] = vm.parseInt(values[i]); + } + return valuesInt; } function gettimestamps(string memory json) public pure returns (uint256[] memory) { - bytes memory rawTimestamps = json.parseRaw(".aggregateTimes"); - string[] memory timestamps = abi.decode(rawTimestamps, (string[])); - uint256[] memory timestampsInt = new uint256[](timestamps.length); - for (uint i = 0; i < timestamps.length; i++) { - timestampsInt[i] = vm.parseUint(timestamps[i]); - } - return timestampsInt; + bytes memory rawTimestamps = json.parseRaw(".aggregateTimes"); + string[] memory timestamps = abi.decode(rawTimestamps, (string[])); + uint256[] memory timestampsInt = new uint256[](timestamps.length); + for (uint256 i = 0; i < timestamps.length; i++) { + timestampsInt[i] = vm.parseUint(timestamps[i]); + } + return timestampsInt; } function getproofs(string memory json) public pure returns (bytes[] memory) { - bytes memory rawProofs = json.parseRaw(".proofs"); - return abi.decode(rawProofs, (bytes[])); + bytes memory rawProofs = json.parseRaw(".proofs"); + return abi.decode(rawProofs, (bytes[])); } }