Skip to content

Commit

Permalink
Add price feed rounds (#64)
Browse files Browse the repository at this point in the history
* add price feed rounds

* remove unused state var

* fmt and lint

* add round unit tests

* fix tests

* lint

* update bindings

* pass oracle info for bindings

* update bindings

* update bin

* remove unused constants
  • Loading branch information
iFrostizz authored Sep 12, 2024
1 parent a7f03de commit 0e9a294
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 108 deletions.
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 = git@github.com: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");
}

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.
*/

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

0 comments on commit 0e9a294

Please sign in to comment.