feat(substreams): add substreams for Uniswap v2 and v3

This commit is contained in:
zizou
2024-10-11 12:57:34 +02:00
parent 58455a1188
commit 73d48236ba
70 changed files with 16697 additions and 1 deletions

View 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,
];

View File

@@ -0,0 +1,4 @@
pub mod pool_storage;
pub mod constants;
mod utils;

View 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())
}
}

View 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
}
}