Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add price feed rounds #64

Merged
merged 11 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "contracts/lib/solidity-merkle-trees"]
path = contracts/lib/solidity-merkle-trees
url = [email protected]:michaelkaplan13/solidity-merkle-trees.git
[submodule "contracts/lib/chainlink"]
path = contracts/lib/chainlink
url = https://github.com/smartcontractkit/chainlink
201 changes: 156 additions & 45 deletions abi-bindings/go/PriceFeedImporter/PriceFeedImporter.go

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contracts/lib/chainlink
Submodule chainlink added at 883c9f
1 change: 1 addition & 0 deletions contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@solidity-merkle-trees=lib/solidity-merkle-trees/src/
@subnet-evm=lib/teleporter/contracts/lib/subnet-evm/contracts/
@chainlink=lib/chainlink/contracts/src/v0.8/shared/
58 changes: 45 additions & 13 deletions contracts/src/PriceFeedImporter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
pragma solidity 0.8.18;

import {EVMEventInfo, EventImporter} from "./EventImporter.sol";
import {AggregatorV3Interface} from "@chainlink/interfaces/AggregatorV3Interface.sol";

/**
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
Expand All @@ -15,19 +16,28 @@ import {EVMEventInfo, EventImporter} from "./EventImporter.sol";
/**
* @notice An example EventImporter implementation that imports the latest price feed data from another blockchain.
*/
contract PriceFeedImporter is EventImporter {
contract PriceFeedImporter is EventImporter, AggregatorV3Interface {
struct Round {
int256 answer;
uint256 updatedAt;
}

bytes32 public constant ANSWER_UPDATED_EVENT_SIGNATURE = keccak256("AnswerUpdated(int256,uint256,uint256)");

// Price feed information
uint8 public immutable decimals;
string public description;
uint256 public immutable version;

// Blockchain ID of the oracle chain.
bytes32 public immutable sourceBlockchainID;

// Address of the Aggregator contract on the source blockchain.
address public immutable sourceOracleAggregator;

// Latest answer information.
int256 public currentAnswer;
uint80 public roundID;
uint256 public updatedAt;
// Rounds
uint80 public latestRoundID;
mapping(uint80 => Round) public rounds;

// The block and transaction on the source blockchain where the latest answer was updated.
uint256 public latestSourceBlockNumber;
Expand Down Expand Up @@ -63,17 +73,32 @@ contract PriceFeedImporter is EventImporter {
_;
}

constructor(bytes32 sourceBlockchainID_, address sourceOracleAggregator_) {
constructor(
bytes32 sourceBlockchainID_,
address sourceOracleAggregator_,
uint8 decimals_,
string memory description_,
uint256 version_
) {
sourceBlockchainID = sourceBlockchainID_;
sourceOracleAggregator = sourceOracleAggregator_;
decimals = decimals_;
description = description_;
version = version_;
}

// solhint-disable-next-line private-vars-leading-underscore
function getRoundData(uint80 _roundID) public view returns (uint80, int256, uint256, uint256, uint80) {
Round memory round = rounds[_roundID];
require(round.updatedAt != 0, "No data");
return (_roundID, round.answer, round.updatedAt, round.updatedAt, _roundID);
}

/**
* @notice Returns the latest round data if available.
*/
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
require(updatedAt != 0, "No data");
return (roundID, currentAnswer, updatedAt, updatedAt, roundID);
function latestRoundData() public view returns (uint80, int256, uint256, uint256, uint80) {
return getRoundData(latestRoundID);
}

function _onEventImport(EVMEventInfo memory eventInfo)
Expand All @@ -84,15 +109,22 @@ contract PriceFeedImporter is EventImporter {
_onlyMoreRecentEvents(eventInfo)
{
// Update the latest answer.
currentAnswer = int256(uint256(eventInfo.log.topics[1]));
roundID = uint80(uint256(eventInfo.log.topics[2]));
updatedAt = uint256(bytes32(eventInfo.log.data));
uint80 roundID = uint80(uint256(eventInfo.log.topics[2]));
if (roundID <= latestRoundID && latestRoundID != 0) {
revert("roundID should be monotonically increasing");
}
Comment on lines +113 to +115
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Events should be published monotonically increasingly. This is not true apparently on startup because the blocks catch-up operation is done asynchronously to the tip following one, thanks @cam-schultz !
We may want to modify this logic because of the catch-up so that it can accept any height, but not allow overwrites.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An issue arising from that is that there might be some missing rounds although the last one is saved. Some smart contracts relying on this information to calculate i.e. the median price may get surprised by this behavior.
If we assume that any gap can only happen at the startup and that it would be filled quickly, we could save them in a "buffer" and only respond with the getRoundData method if none of the previous round is missing.

iFrostizz marked this conversation as resolved.
Show resolved Hide resolved

int256 answer = int256(uint256(eventInfo.log.topics[1]));
uint256 updatedAt = uint256(bytes32(eventInfo.log.data));
Round memory round = Round({answer: answer, updatedAt: updatedAt});
rounds[roundID] = round;
latestRoundID = roundID;

// Update the latest source block information.
latestSourceBlockNumber = eventInfo.blockNumber;
latestSourceTxIndex = eventInfo.txIndex;
latestSourceLogIndex = eventInfo.logIndex;

emit AnswerUpdated(currentAnswer, roundID, updatedAt);
emit AnswerUpdated(answer, roundID, updatedAt);
}
}
1 change: 0 additions & 1 deletion contracts/src/RLPUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {RLPReader} from "@solidity-merkle-trees/trie/ethereum/RLPReader.sol";
* THIS IS AN EXAMPLE LIBRARY THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/

Copy link
Contributor Author

@iFrostizz iFrostizz Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happened when formatting

library RLPUtils {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;
Expand Down
82 changes: 82 additions & 0 deletions contracts/test/EventImporterTests.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// (c) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// SPDX-License-Identifier: Ecosystem

pragma solidity 0.8.18;

import {Test} from "forge-std/Test.sol";
import {WarpBlockHash, IWarpMessenger} from "@subnet-evm/contracts/interfaces/IWarpMessenger.sol";
import {PriceFeedImporter} from "../src/PriceFeedImporter.sol";

contract EventImporterTest is Test {
address public constant WARP_PRECOMPILE_ADDRESS = 0x0200000000000000000000000000000000000005;
bytes32 public constant DEFAULT_BLOCKCHAIN_ID =
bytes32(hex"55e1fcfdde01f9f6d4c16fa2ed89ce65a8669120a86f321eef121891cab61241");
address public constant SOURCE_ORACLE_AGGREGATOR = 0x154baB1FC1D87fF641EeD0E9Bc0f8a50D880D2B6;

uint8 public constant DECIMALS = 18;
string public description = "WarpETHUSDAggregator";
uint256 public constant VERSION = 1;

PriceFeedImporter public priceFeedImporter;

event AnswerUpdated(int256 currentAnswer, uint80 roundID, uint256 updatedAt);
event EventImported(
bytes32 indexed sourceBlockchainID,
bytes32 indexed sourceBlockHash,
address indexed loggerAddress,
uint256 txIndex,
uint256 logIndex
);

function setUp() public virtual {
priceFeedImporter = new PriceFeedImporter(DEFAULT_BLOCKCHAIN_ID, SOURCE_ORACLE_AGGREGATOR, DECIMALS, description, VERSION);
}

function testImportEvent() public {
// Mock the Warp precompile returning a valid WarpBlockHash.
bytes32 blockHash = 0x2d9215bce478eb82bfd35f7e9bdc9d76e1814e8d7b5aa10ab05e2f17d145c0cf;
bytes memory encodedBlockHeader =
hex"f9027ba05e92b0416d56a052ea29c5267627d489290dd2f87bae4887c56084452349598ca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940100000000000000000000000000000000000000a0425ff44c06b858d5ea72ce4261f8174d1a6d729a2ffdadddaa94abe58b37597da000d275e4cce63b7fe0facd8f477d3cf879b7992c3fed522e01ec5bc6b05345f7a07149d37617782b645919054a607d47ffc590e8c8827c616e792b22caf8fb527fb90100202000000000000000000000801000a120020004000080002184810040000008002050000000004000500400200c1004084000000103010200200e02000808000090c0000024800204802009006020200008000081000000000050808002080000210011021008000000000100000800000000400000082000000010000007010800000400000000000014010000000802020490040002094020004800404010000000401008000401000600002000124000180008008008800001800400000002040002000600000000002001020050004018000000007080002024000020008200000020000081000100040000400000020000000000020000020001000000018402b60ce083e4e1c0831ca6e38466452e8db8560000000000095c3f0000000000000000000000000010c6a90000000000000000000000000004ee9900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218505d21dba008080";
assertEq(blockHash, keccak256(encodedBlockHeader));
vm.mockCall(
WARP_PRECOMPILE_ADDRESS,
abi.encodeCall(IWarpMessenger.getVerifiedWarpBlockHash, (0)),
abi.encode(WarpBlockHash({blockHash: blockHash, sourceChainID: DEFAULT_BLOCKCHAIN_ID}), true)
);

// Assemble the Merkle proof against the receipts root of the block header.
bytes[] memory proof = new bytes[](3);
proof[0] =
hex"f901d180a0cb5cc5565987800aeba057b0af7a83c7d46c3e16d08a0b2e5e97c663af8c0249a091ca9b1a8700045443e3cebc160679c2a8443dafd8f3bd33797836b401f5c0daa0f334d4c8361313ace856c4cd2f762c7954b0e280635e21cf7c5d8a0365b229f7a05a7dda2d8aa8908be6452e5ea4f2176d02c6c59418e8ade5d49cee1b1dab43e7a0ed2f24945ae33093877231e08d6cfc4584151642e49c59ad4cc7b6c6a9be30e3a09a1e4d23e6722be8df80f8e0b0a6978f868e2d09f9a8999c69349012425cb1a8a0d4639be4cfb077205fe324498b3b82165e4f26680b257b402aaf844287986d7ba02bf31869288dd26d1524809068622f88b2d8a7d4ba41e6b3d71103a72fbf81cfa0c8a339bcdd687538e2d6428ec10a9b763d8803c47b58796813fad9325bce9583a0853e20adc93f1f305cae89c704b98dd6ea5ebd2725cbd334a254c496fa25874fa0c2dc37de20dfd81248b7c7373e3858c935cb1377ce96c861fddfe3c6498b22b9a00592cdc2487293d7f933bac6750638a20ce822e652abe776ed20c69f354cd9dba0d472513604ab02f424f3a737de868348662e9caedf883b4213d0b76dbc7c9ae3a043d6dd9e9b84b58f3d23e0dc2c1fbdef9c99e6307431b849fe666696be6345c78080";
proof[1] =
hex"f905ab20b905a7f905a40183162ae9b9010000000000000000000000000000000000000000000000000000000100000000000000100000000000000000000000000000000000000200000000000000000000000000000000000000000001002000000000000001000000000000000000000000000000020000000000000000000800000000000000000000000000000001000000000000000000000000000000000000000480000000000000000000400000000000001000000000000200000000000000000008000008000000000000000000000000000000000000000000000000004010000000000000000000000020000000000000000000000000000000000000000000000000000000000001000000f90499f9035c94154bab1fc1d87ff641eed0e9bc0f8a50d880d2b6f842a0f6a97944f31ea060dfde0566e4167c1a1082551e64b60ecb14d599a9d023d451a0000000000000000000000000000000000000000000000000000000000002be00b90300000000000000000000000000000000000000000000000000000006010e05e000000000000000000000000000e5b37dc608c73852f9c0f56e30f8d74d89b51c5500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000f4fc72042f23c3a2b6da6ebfecf0b6e30001538902000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000600bc7bda0000000000000000000000000000000000000000000000000000000600bc8b1c4000000000000000000000000000000000000000000000000000000600c0b0c17000000000000000000000000000000000000000000000000000000600d9b39e5000000000000000000000000000000000000000000000000000000600f2e28c6000000000000000000000000000000000000000000000000000000600f9c1e30000000000000000000000000000000000000000000000000000000600f9c1e3000000000000000000000000000000000000000000000000000000060105dd20a8000000000000000000000000000000000000000000000000000006010e05e000000000000000000000000000000000000000000000000000000006010e05e000000000000000000000000000000000000000000000000000000006010e05e000000000000000000000000000000000000000000000000000000006013c1415ba000000000000000000000000000000000000000000000000000006014367c00800000000000000000000000000000000000000000000000000000601bf96ddf600000000000000000000000000000000000000000000000000000601bf96ddf6000000000000000000000000000000000000000000000000000006023b6e36e00000000000000000000000000000000000000000000000000000000000000010010f0d0e0c040903080a000b0607020500000000000000000000000000000000f89b94154bab1fc1d87ff641eed0e9bc0f8a50d880d2b6f863a00109fc6f55cf40689f02fbaad7af7fe7bbac8a3d2186600afc7d3e10cac60271a0000000000000000000000000000000000000000000000000000000000002be00a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000066452e8df89b94154bab1fc1d87ff641eed0e9bc0f8a50d880d2b6f863a00559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5fa0000000000000000000000000000000000000000000000000000006010e05e000a0000000000000000000000000000000000000000000000000000000000002be00a00000000000000000000000000000000000000000000000000000000066452e8d";
proof[2] =
hex"f851a016195dc5b5fc0021cbfee701b341d49721eb17357a1b354ecf55a61cab521b5c80808080808080a085f248d30b0ce90e5d44a50650a12dffc585c1cc2cb9474050891fa23e5306ac8080808080808080";

// Current answer should revert since no event has been imported yet.
vm.expectRevert("No data");
priceFeedImporter.latestRoundData();

// Import the event.
vm.expectEmit(true, true, true, true);
emit AnswerUpdated(6_601_600_000_000, 179_712, 1_715_809_933);
vm.expectEmit(true, true, true, true);
emit EventImported(DEFAULT_BLOCKCHAIN_ID, blockHash, 0x154baB1FC1D87fF641EeD0E9Bc0f8a50D880D2B6, 8, 2);
priceFeedImporter.importEvent(bytes32(0), encodedBlockHeader, 8, proof, 2);

// Verify the latest round data.
(uint80 roundID, int256 currentAnswer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =
priceFeedImporter.latestRoundData();
assertEq(roundID, 179_712);
assertEq(currentAnswer, 6_601_600_000_000);
assertEq(startedAt, 1_715_809_933);
assertEq(updatedAt, 1_715_809_933);
assertEq(answeredInRound, 179_712);

assertEq(priceFeedImporter.latestSourceBlockNumber(), 45_485_280);
assertEq(priceFeedImporter.latestSourceTxIndex(), 8);
assertEq(priceFeedImporter.latestSourceLogIndex(), 2);
}
}
Loading