Skip to content

Commit

Permalink
Merge pull request #12 from propeller-heads/tnl/ENG-3750-erc20-overwr…
Browse files Browse the repository at this point in the history
…ite-factory

feat: create ERC20OverwriteFactory utils
  • Loading branch information
tamaralipows authored Oct 23, 2024
2 parents 0118034 + e9f7bdf commit 8f7f022
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub mod state;
pub mod tycho;
pub mod uniswap_v2;
pub mod uniswap_v3;
mod vm;
pub mod vm;

/// A trait for converting types to and from `Bytes`.
///
Expand Down
216 changes: 216 additions & 0 deletions src/protocol/vm/erc20_overwrite_factory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// TODO: remove skip for clippy dead_code check
#![allow(dead_code)]
use crate::protocol::vm::utils::{get_contract_bytecode, get_storage_slot_index_at_key, SlotHash};
use ethers::{addressbook::Address, prelude::U256};
use std::{collections::HashMap, path::Path};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum FileError {
/// Occurs when the ABI file cannot be read
#[error("Malformed ABI error: {0}")]
MalformedABI(String),
/// Occurs when the parent directory of the current file cannot be retrieved
#[error("Structure error {0}")]
Structure(String),
/// Occurs when a bad file path was given, which cannot be converted to string.
#[error("File path conversion error {0}")]
FilePath(String),
}

pub struct GethOverwrite {
/// the formatted overwrites
pub state_diff: HashMap<String, String>,
/// the bytecode as a string
pub code: String,
}

pub type Overwrites = HashMap<SlotHash, U256>;

pub struct ERC20OverwriteFactory {
token_address: Address,
overwrites: Overwrites,
balance_slot: SlotHash,
allowance_slot: SlotHash,
total_supply_slot: SlotHash,
}

impl ERC20OverwriteFactory {
pub fn new(token_address: Address, token_slots: (SlotHash, SlotHash)) -> Self {
ERC20OverwriteFactory {
token_address,
overwrites: HashMap::new(),
balance_slot: token_slots.0,
allowance_slot: token_slots.1,
total_supply_slot: SlotHash::from_low_u64_be(2),
}
}

pub fn set_balance(&mut self, balance: U256, owner: Address) {
let storage_index = get_storage_slot_index_at_key(owner, self.balance_slot);
self.overwrites
.insert(storage_index, balance);
}

pub fn set_allowance(&mut self, allowance: U256, spender: Address, owner: Address) {
let owner_slot = get_storage_slot_index_at_key(owner, self.allowance_slot);
let storage_index = get_storage_slot_index_at_key(spender, owner_slot);
self.overwrites
.insert(storage_index, allowance);
}

pub fn set_total_supply(&mut self, supply: U256) {
self.overwrites
.insert(self.total_supply_slot, supply);
}

pub fn get_protosim_overwrites(&self) -> HashMap<Address, Overwrites> {
let mut result = HashMap::new();
result.insert(self.token_address, self.overwrites.clone());
result
}

pub fn get_geth_overwrites(&self) -> Result<HashMap<Address, GethOverwrite>, FileError> {
let mut formatted_overwrites = HashMap::new();

for (key, val) in &self.overwrites {
let hex_key = hex::encode(key.as_bytes());

let mut bytes = [0u8; 32];
val.to_big_endian(&mut bytes);
let hex_val = format!("0x{:0>64}", hex::encode(bytes));

formatted_overwrites.insert(hex_key, hex_val);
}

let erc20_abi_path = Path::new(file!())
.parent()
.ok_or_else(|| {
FileError::Structure(
"Failed to obtain parent directory of current file.".to_string(),
)
})?
.join("assets")
.join("ERC20.abi");

let code = format!(
"0x{}",
hex::encode(
get_contract_bytecode(erc20_abi_path.to_str().ok_or_else(|| {
FileError::FilePath("Failed to convert file path to string.".to_string())
})?)
.map_err(|_err| FileError::MalformedABI(
"Failed to read contract bytecode.".to_string()
))?
)
);

let mut result = HashMap::new();
result.insert(self.token_address, GethOverwrite { state_diff: formatted_overwrites, code });

Ok(result)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::vm::utils::SlotHash;

fn setup_factory() -> ERC20OverwriteFactory {
let token_address = Address::random();
let balance_slot = SlotHash::random();
let allowance_slot = SlotHash::random();
ERC20OverwriteFactory::new(token_address, (balance_slot, allowance_slot))
}

#[test]
fn test_set_balance() {
let mut factory = setup_factory();
let owner = Address::random();
let balance = U256::from(1000);

factory.set_balance(balance, owner);

assert_eq!(factory.overwrites.len(), 1);
assert!(factory
.overwrites
.values()
.any(|&v| v == balance));
}

#[test]
fn test_set_allowance() {
let mut factory = setup_factory();
let owner = Address::random();
let spender = Address::random();
let allowance = U256::from(500);

factory.set_allowance(allowance, spender, owner);

assert_eq!(factory.overwrites.len(), 1);
assert!(factory
.overwrites
.values()
.any(|&v| v == allowance));
}

#[test]
fn test_set_total_supply() {
let mut factory = setup_factory();
let supply = U256::from(1_000_000);

factory.set_total_supply(supply);

assert_eq!(factory.overwrites.len(), 1);
assert_eq!(factory.overwrites[&factory.total_supply_slot], supply);
}

#[test]
fn test_get_protosim_overwrites() {
let mut factory = setup_factory();
let supply = U256::from(1_000_000);
factory.set_total_supply(supply);

let overwrites = factory.get_protosim_overwrites();

assert_eq!(overwrites.len(), 1);
assert!(overwrites.contains_key(&factory.token_address));
assert_eq!(overwrites[&factory.token_address].len(), 1);
assert_eq!(overwrites[&factory.token_address][&factory.total_supply_slot], supply);
}

#[test]
fn test_get_geth_overwrites() {
let mut factory = setup_factory();

let storage_slot = SlotHash::from_low_u64_be(1);
let val = U256::from(123456);
factory
.overwrites
.insert(storage_slot, val);

let result = factory
.get_geth_overwrites()
.expect("Failed to get geth overwrites");

assert_eq!(result.len(), 1);

let geth_overwrite = result
.get(&factory.token_address)
.expect("Missing token address");
assert_eq!(geth_overwrite.state_diff.len(), 1);

let expected_key =
String::from("0000000000000000000000000000000000000000000000000000000000000001");
let expected_val =
String::from("0x000000000000000000000000000000000000000000000000000000000001e240");
assert_eq!(
geth_overwrite
.state_diff
.get(&expected_key),
Some(&expected_val)
);
assert_eq!(geth_overwrite.code.len(), 8752);
}
}
3 changes: 2 additions & 1 deletion src/protocol/vm/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod utils;
mod erc20_overwrite_factory;
pub mod utils;
60 changes: 58 additions & 2 deletions src/protocol/vm/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
use ethabi::{self, decode, ParamType};
use ethers::{
abi::Abi,
core::utils::keccak256,
providers::{Http, Middleware, Provider, ProviderError},
types::H160,
types::{Address, H160, H256},
};
use hex::FromHex;
use mini_moka::sync::Cache;
Expand All @@ -18,7 +19,6 @@ use std::{
path::Path,
sync::{Arc, LazyLock},
};

use thiserror::Error;

#[derive(Debug, Error)]
Expand Down Expand Up @@ -128,6 +128,62 @@ fn parse_solidity_error_message(data: &str) -> String {
format!("Failed to decode: {}", data)
}

pub type SlotHash = H256;

/// Get storage slot index of a value stored at a certain key in a mapping
///
/// # Arguments
///
/// * `key`: Key in a mapping. Can be any H160 value (such as an address).
/// * `mapping_slot`: An `H256` representing the storage slot at which the mapping itself is stored.
/// See the examples for more explanation.
///
/// # Returns
///
/// An `H256` representing the index of a storage slot where the value at the given
/// key is stored.
///
/// # Examples
///
/// If a mapping is declared as a first variable in Solidity code, its storage slot
/// is 0 (e.g. `balances` in our mocked ERC20 contract). Here's how to compute
/// a storage slot where balance of a given account is stored:
///
/// ```
/// use protosim::protocol::vm::utils::{get_storage_slot_index_at_key, H256};
/// use ethers::types::Address;
/// let address: Address = "0xC63135E4bF73F637AF616DFd64cf701866BB2628".parse().expect("Invalid address");
/// get_storage_slot_index_at_key(address, H256::from_low_u64_be(0));
/// ```
///
/// For nested mappings, we need to apply the function twice. An example of this is
/// `allowances` in ERC20. It is a mapping of form:
/// `HashMap<Owner, HashMap<Spender, U256>>`. In our mocked ERC20 contract, `allowances`
/// is a second variable, so it is stored at slot 1. Here's how to get a storage slot
/// where an allowance of `address_spender` to spend `address_owner`'s money is stored:
///
/// ```
/// use protosim::protocol::vm::utils::{get_storage_slot_index_at_key, H256};
/// use ethers::types::Address;
/// let address_spender: Address = "0xC63135E4bF73F637AF616DFd64cf701866BB2628".parse().expect("Invalid address");
/// let address_owner: Address = "0x6F4Feb566b0f29e2edC231aDF88Fe7e1169D7c05".parse().expect("Invalid address");
/// get_storage_slot_index_at_key(address_spender, get_storage_slot_index_at_key(address_owner, H256::from_low_u64_be(1)));
/// ```
///
/// # See Also
///
/// [Solidity Storage Layout documentation](https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html#mappings-and-dynamic-arrays)
pub fn get_storage_slot_index_at_key(key: Address, mapping_slot: SlotHash) -> SlotHash {
let mut key_bytes = key.as_bytes().to_vec();
key_bytes.resize(32, 0); // Right pad with zeros

let mut mapping_slot_bytes = [0u8; 32];
mapping_slot_bytes.copy_from_slice(mapping_slot.as_bytes());

let slot_bytes = keccak256([&key_bytes[..], &mapping_slot_bytes[..]].concat());
SlotHash::from_slice(&slot_bytes)
}

fn get_solidity_panic_codes() -> HashMap<u64, String> {
let mut panic_codes = HashMap::new();
panic_codes.insert(0, "GenericCompilerPanic".to_string());
Expand Down

0 comments on commit 8f7f022

Please sign in to comment.