feat(substreams): add substreams for Uniswap v2 and v3
This commit is contained in:
60
substreams/ethereum-uniswap-v3/src/storage/constants.rs
Normal file
60
substreams/ethereum-uniswap-v3/src/storage/constants.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use hex_literal::hex;
|
||||
|
||||
use super::pool_storage::StorageLocation;
|
||||
|
||||
const SLOT0: [u8; 32] = hex!("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
|
||||
const LIQUIDITY_SLOT: StorageLocation = StorageLocation {
|
||||
name: "liquidity",
|
||||
slot: hex!("0000000000000000000000000000000000000000000000000000000000000004"),
|
||||
offset: 0,
|
||||
number_of_bytes: 16,
|
||||
signed: false,
|
||||
};
|
||||
|
||||
const PROTOCOL_FEES_TOKEN_0_SLOT: StorageLocation = StorageLocation {
|
||||
name: "protocol_fees/token0",
|
||||
slot: hex!("0000000000000000000000000000000000000000000000000000000000000003"),
|
||||
offset: 0,
|
||||
number_of_bytes: 16,
|
||||
signed: false,
|
||||
};
|
||||
|
||||
const PROTOCOL_FEES_TOKEN_1_SLOT: StorageLocation = StorageLocation {
|
||||
name: "protocol_fees/token1",
|
||||
slot: hex!("0000000000000000000000000000000000000000000000000000000000000003"),
|
||||
offset: 16,
|
||||
number_of_bytes: 16,
|
||||
signed: false,
|
||||
};
|
||||
|
||||
const SQRT_PRICE_X96_SLOT: StorageLocation = StorageLocation {
|
||||
name: "sqrt_price_x96",
|
||||
slot: SLOT0,
|
||||
offset: 0,
|
||||
number_of_bytes: 20,
|
||||
signed: false,
|
||||
};
|
||||
|
||||
const CURRENT_TICK_SLOT: StorageLocation =
|
||||
StorageLocation { name: "tick", slot: SLOT0, offset: 20, number_of_bytes: 3, signed: true };
|
||||
|
||||
const FEE_PROTOCOL_SLOT: StorageLocation = StorageLocation {
|
||||
name: "fee_protocol",
|
||||
slot: SLOT0,
|
||||
offset: 29,
|
||||
number_of_bytes: 1,
|
||||
signed: false,
|
||||
};
|
||||
|
||||
pub(crate) const TICKS_MAP_SLOT: [u8; 32] =
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000005");
|
||||
|
||||
pub(crate) const TRACKED_SLOTS: [StorageLocation; 6] = [
|
||||
LIQUIDITY_SLOT,
|
||||
PROTOCOL_FEES_TOKEN_0_SLOT,
|
||||
PROTOCOL_FEES_TOKEN_1_SLOT,
|
||||
SQRT_PRICE_X96_SLOT,
|
||||
CURRENT_TICK_SLOT,
|
||||
FEE_PROTOCOL_SLOT,
|
||||
];
|
||||
4
substreams/ethereum-uniswap-v3/src/storage/mod.rs
Normal file
4
substreams/ethereum-uniswap-v3/src/storage/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod pool_storage;
|
||||
|
||||
pub mod constants;
|
||||
mod utils;
|
||||
132
substreams/ethereum-uniswap-v3/src/storage/pool_storage.rs
Normal file
132
substreams/ethereum-uniswap-v3/src/storage/pool_storage.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::{
|
||||
pb::tycho::evm::v1::{Attribute, ChangeType},
|
||||
storage::utils,
|
||||
};
|
||||
|
||||
use substreams::scalar::BigInt;
|
||||
use substreams_ethereum::pb::eth::v2::StorageChange;
|
||||
|
||||
use super::{constants::TICKS_MAP_SLOT, utils::read_bytes};
|
||||
|
||||
/// `StorageLocation` is a struct that represents a specific location within a contract's storage
|
||||
/// associated with a name.
|
||||
///
|
||||
/// # Fields
|
||||
///
|
||||
/// * `name` - A string slice (`&str`) reference representing the unique name associated with this
|
||||
/// storage location.
|
||||
/// * `slot` - A fixed-size byte array `[u8; 32]` representing the slot in the contract storage
|
||||
/// where this data is stored. This acts as a primary identifier for the location of the data.
|
||||
/// * `offset` - A usize value indicating the offset in bytes from the start of the slot. This
|
||||
/// allows for fine-grained control and access within a single slot.
|
||||
/// * `number_of_bytes` - A usize value indicating the size of the data in bytes.
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct StorageLocation<'a> {
|
||||
pub name: &'a str,
|
||||
pub slot: [u8; 32],
|
||||
pub offset: usize,
|
||||
pub number_of_bytes: usize,
|
||||
pub signed: bool,
|
||||
}
|
||||
|
||||
pub struct UniswapPoolStorage<'a> {
|
||||
pub storage_changes: &'a Vec<StorageChange>,
|
||||
}
|
||||
|
||||
impl<'a> UniswapPoolStorage<'a> {
|
||||
pub fn new(storage_changes: &'a Vec<StorageChange>) -> UniswapPoolStorage<'a> {
|
||||
Self { storage_changes }
|
||||
}
|
||||
|
||||
/// Iterates through storage changes and checks for modifications in the provided list of
|
||||
/// storage locations. For each change, it compares the old and new values at the specified
|
||||
/// offset and length for that location. If a change is detected, it's added to the returned
|
||||
/// `Attribute` list.
|
||||
///
|
||||
/// Arguments:
|
||||
/// locations: Vec<&StorageLocation> - A vector of references to StorageLocation objects
|
||||
/// that define the slots, offsets, and lengths to be checked for changes.
|
||||
///
|
||||
/// Returns:
|
||||
/// `Vec<Attribute>`: A vector containing Attributes for each change detected in the tracked
|
||||
/// slots. Returns an empty vector if no changes are detected.
|
||||
pub fn get_changed_attributes(&self, locations: Vec<&StorageLocation>) -> Vec<Attribute> {
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
// For each storage change, check if it changes a tracked slot.
|
||||
// If it does, add the attribute to the list of attributes
|
||||
for change in self.storage_changes {
|
||||
for storage_location in locations.iter() {
|
||||
// Check if the change slot matches the tracked slot
|
||||
if change.key == storage_location.slot {
|
||||
let old_data = read_bytes(
|
||||
&change.old_value,
|
||||
storage_location.offset,
|
||||
storage_location.number_of_bytes,
|
||||
);
|
||||
let new_data = read_bytes(
|
||||
&change.new_value,
|
||||
storage_location.offset,
|
||||
storage_location.number_of_bytes,
|
||||
);
|
||||
|
||||
// Check if there is a change in the data
|
||||
if old_data != new_data {
|
||||
let value = match storage_location.signed {
|
||||
true => BigInt::from_signed_bytes_be(new_data),
|
||||
false => BigInt::from_unsigned_bytes_be(new_data),
|
||||
};
|
||||
attributes.push(Attribute {
|
||||
name: storage_location.name.to_string(),
|
||||
value: value.to_signed_bytes_le(),
|
||||
change: ChangeType::Update.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attributes
|
||||
}
|
||||
|
||||
/// Iterates over a list of tick indexes and checks for modifications in the list of
|
||||
/// storage changes. If a relevent change is detected, it's added to the returned `Attribute`
|
||||
/// list.
|
||||
///
|
||||
/// Arguments:
|
||||
/// ticks_idx: `Vec<&BigInt>` - A vector of references to tick indexes as BigInt objects.
|
||||
///
|
||||
/// Returns:
|
||||
/// `Vec<Attribute>`: A vector containing Attributes for each change detected. Returns an
|
||||
/// empty vector if no changes are detected.
|
||||
///
|
||||
/// Note: Currently, we only track the net-liquidity attribute for each tick.
|
||||
pub fn get_ticks_changes(&self, ticks_idx: Vec<&BigInt>) -> Vec<Attribute> {
|
||||
let mut storage_locs = Vec::new();
|
||||
let mut tick_names = Vec::new();
|
||||
|
||||
// First, create all the names and push them into tick_names.
|
||||
// We need this to keep the references to the names alive until we call
|
||||
// `get_changed_attributes()`
|
||||
for tick_idx in ticks_idx.iter() {
|
||||
tick_names.push(format!("ticks/{}/net-liquidity", tick_idx));
|
||||
}
|
||||
|
||||
// Then, iterate over ticks_idx and tick_names simultaneously
|
||||
for (tick_idx, tick_name) in ticks_idx.iter().zip(tick_names.iter()) {
|
||||
let tick_slot =
|
||||
utils::calc_map_slot(&utils::left_pad_from_bigint(tick_idx), &TICKS_MAP_SLOT);
|
||||
|
||||
storage_locs.push(StorageLocation {
|
||||
name: tick_name,
|
||||
slot: tick_slot,
|
||||
offset: 16,
|
||||
number_of_bytes: 16,
|
||||
signed: true,
|
||||
});
|
||||
}
|
||||
|
||||
self.get_changed_attributes(storage_locs.iter().collect())
|
||||
}
|
||||
}
|
||||
165
substreams/ethereum-uniswap-v3/src/storage/utils.rs
Normal file
165
substreams/ethereum-uniswap-v3/src/storage/utils.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use substreams::scalar::BigInt;
|
||||
use tiny_keccak::{Hasher, Keccak};
|
||||
|
||||
pub fn calc_map_slot(map_index: &[u8; 32], base_slot: &[u8; 32]) -> [u8; 32] {
|
||||
let mut output = [0u8; 32];
|
||||
let mut hasher = Keccak::v256();
|
||||
hasher.update(map_index);
|
||||
hasher.update(base_slot);
|
||||
hasher.finalize(&mut output);
|
||||
output
|
||||
}
|
||||
|
||||
pub fn left_pad_from_bigint(input: &BigInt) -> [u8; 32] {
|
||||
if input.lt(&BigInt::zero()) {
|
||||
return left_pad(&input.to_signed_bytes_be(), 255);
|
||||
}
|
||||
|
||||
left_pad(&input.to_signed_bytes_be(), 0)
|
||||
}
|
||||
|
||||
pub fn left_pad(input: &[u8], padding_value: u8) -> [u8; 32] {
|
||||
if input.len() > 32 {
|
||||
panic!("cannot convert vec<u8> to H256");
|
||||
}
|
||||
let mut data = [padding_value; 32];
|
||||
let offset = 32 - input.len();
|
||||
data[offset..(input.len() + offset)].copy_from_slice(input);
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
pub fn read_bytes(buf: &[u8], offset: usize, number_of_bytes: usize) -> &[u8] {
|
||||
let buf_length = buf.len();
|
||||
if buf_length < number_of_bytes {
|
||||
panic!(
|
||||
"attempting to read {number_of_bytes} bytes in buffer size {buf_size}",
|
||||
number_of_bytes = number_of_bytes,
|
||||
buf_size = buf.len()
|
||||
)
|
||||
}
|
||||
|
||||
if offset > (buf_length - 1) {
|
||||
panic!(
|
||||
"offset {offset} exceeds buffer size {buf_size}",
|
||||
offset = offset,
|
||||
buf_size = buf.len()
|
||||
)
|
||||
}
|
||||
|
||||
let end = buf_length - 1 - offset;
|
||||
let start_opt = (end + 1).checked_sub(number_of_bytes);
|
||||
if start_opt.is_none() {
|
||||
panic!(
|
||||
"number of bytes {number_of_bytes} with offset {offset} exceeds buffer size
|
||||
{buf_size}",
|
||||
number_of_bytes = number_of_bytes,
|
||||
offset = offset,
|
||||
buf_size = buf.len()
|
||||
)
|
||||
}
|
||||
let start = start_opt.unwrap();
|
||||
|
||||
&buf[start..=end]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::storage::utils::{left_pad, read_bytes};
|
||||
use hex_literal::hex;
|
||||
use std::{fmt::Write, num::ParseIntError};
|
||||
|
||||
#[test]
|
||||
fn left_pad_lt_32_bytes() {
|
||||
let input = hex!("dd62ed3e");
|
||||
assert_eq!(
|
||||
hex!("00000000000000000000000000000000000000000000000000000000dd62ed3e"),
|
||||
left_pad(&input, 0)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_pad_eq_32_bytes() {
|
||||
let input = hex!("00000a0000000000005d000000000000000000000000000000000000dd62ed3e");
|
||||
assert_eq!(
|
||||
hex!("00000a0000000000005d000000000000000000000000000000000000dd62ed3e"),
|
||||
left_pad(&input, 0)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn left_pad_gt_32_bytes() {
|
||||
let input = hex!("070000000a0000000000005d000000000000000000000000000000000000dd62ed3e");
|
||||
let _ = left_pad(&input, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn read_bytes_buf_too_small() {
|
||||
let buf = decode_hex("ff").unwrap();
|
||||
let offset = 0;
|
||||
let number_of_bytes = 3;
|
||||
let _ = read_bytes(&buf, offset, number_of_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_one_byte_with_no_offset() {
|
||||
let buf = decode_hex("aabb").unwrap();
|
||||
let offset = 0;
|
||||
let number_of_bytes = 1;
|
||||
assert_eq!(read_bytes(&buf, offset, number_of_bytes), hex!("bb"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_one_byte_with_offset() {
|
||||
let buf = decode_hex("aabb").unwrap();
|
||||
let offset = 1;
|
||||
let number_of_bytes = 1;
|
||||
assert_eq!(read_bytes(&buf, offset, number_of_bytes), hex!("aa"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn read_bytes_overflow() {
|
||||
let buf = decode_hex("aabb").unwrap();
|
||||
let offset = 1;
|
||||
let number_of_bytes = 2;
|
||||
let _ = read_bytes(&buf, offset, number_of_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_bytes_with_no_offset() {
|
||||
let buf =
|
||||
decode_hex("ffffffffffffffffffffecb6826b89a60000000000000000000013497d94765a").unwrap();
|
||||
let offset = 0;
|
||||
let number_of_bytes = 16;
|
||||
let out = read_bytes(&buf, offset, number_of_bytes);
|
||||
assert_eq!(encode_hex(out), "0000000000000000000013497d94765a".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_byte_with_big_offset() {
|
||||
let buf =
|
||||
decode_hex("0100000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let offset = 31;
|
||||
let number_of_bytes = 1;
|
||||
let out = read_bytes(&buf, offset, number_of_bytes);
|
||||
assert_eq!(encode_hex(out), "01".to_string());
|
||||
}
|
||||
|
||||
fn decode_hex(s: &str) -> Result<Vec<u8>, ParseIntError> {
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn encode_hex(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
write!(&mut s, "{:02x}", b).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user