Skip to content

Commit

Permalink
Merge pull request #17 from propeller-heads/vm/dc/ENG-3753-protosim-c…
Browse files Browse the repository at this point in the history
…ontract

feat: Protosim contract and Adapter contract
  • Loading branch information
dianacarvalho1 authored Oct 28, 2024
2 parents a3706c9 + a5cc501 commit 7746337
Show file tree
Hide file tree
Showing 8 changed files with 659 additions and 54 deletions.
3 changes: 2 additions & 1 deletion src/evm/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use super::{
};

/// An error representing any transaction simulation result other than successful execution
#[derive(Debug, Display)]
#[derive(Debug, Display, Clone)]
pub enum SimulationError {
/// Something went wrong while getting storage; might be caused by network issues.
/// Retrying may help.
Expand All @@ -42,6 +42,7 @@ pub enum SimulationError {
}

/// A result of a successful transaction simulation
#[derive(Debug, Clone, Default)]
pub struct SimulationResult {
/// Output of transaction execution as bytes
pub result: bytes::Bytes,
Expand Down
218 changes: 218 additions & 0 deletions src/protocol/vm/adapter_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// TODO: remove skip for clippy dead_code check
#![allow(dead_code)]

use crate::{
evm::account_storage::StateUpdate,
protocol::vm::{
errors::ProtosimError, models::Capability, protosim_contract::ProtosimContract,
},
};
use ethers::{
abi::{Address, Token},
types::U256,
};
use revm::{primitives::Address as rAddress, DatabaseRef};
use std::collections::{HashMap, HashSet};

#[derive(Debug)]
pub struct Trade {
received_amount: U256,
gas_used: U256,
price: f64,
}

/// An implementation of `ProtosimContract` specific to the `AdapterContract` ABI interface,
/// providing methods for price calculations, token swaps, capability checks, and more.
///
/// This struct facilitates interaction with the `AdapterContract` by encoding and decoding data
/// according to its ABI specification. Each method corresponds to a function in the adapter
/// contract's interface, enabling seamless integration with Protosim’s simulation environment.
///
/// # Methods
/// - `price`: Calculates price information for a token pair within the adapter.
/// - `swap`: Simulates a token swap operation, returning details about the trade and state updates.
/// - `get_limits`: Retrieves the trade limits for a given token pair.
/// - `get_capabilities`: Checks the capabilities of the adapter for a specific token pair.
/// - `min_gas_usage`: Queries the minimum gas usage required for operations within the adapter.
impl<D: DatabaseRef + std::clone::Clone> ProtosimContract<D>
where
D::Error: std::fmt::Debug,
{
pub async fn price(
&self,
pair_id: String,
sell_token: Address,
buy_token: Address,
amounts: Vec<u64>,
block: u64,
overwrites: Option<HashMap<rAddress, HashMap<U256, U256>>>,
) -> Result<Vec<f64>, ProtosimError> {
let args = vec![
self.hexstring_to_bytes(&pair_id)?,
Token::Address(sell_token),
Token::Address(buy_token),
Token::Array(
amounts
.into_iter()
.map(|a| Token::Uint(U256::from(a)))
.collect(),
),
];

let res = self
.call("price", args, block, None, overwrites, None, U256::zero())
.await?
.return_value;
// returning just floats - the python version returns Fractions (not sure why)
let price = self.calculate_price(res[0].clone())?;
Ok(price)
}

#[allow(clippy::too_many_arguments)]
pub async fn swap(
&self,
pair_id: String,
sell_token: Address,
buy_token: Address,
is_buy: bool,
amount: U256,
block: u64,
overwrites: Option<HashMap<rAddress, HashMap<U256, U256>>>,
) -> Result<(Trade, HashMap<revm::precompile::Address, StateUpdate>), ProtosimError> {
let args = vec![
self.hexstring_to_bytes(&pair_id)?,
Token::Address(sell_token),
Token::Address(buy_token),
Token::Bool(is_buy),
Token::Uint(amount),
];

let res = self
.call("swap", args, block, None, overwrites, None, U256::zero())
.await?;
let received_amount = res.return_value[0]
.clone()
.into_uint()
.unwrap();
let gas_used = res.return_value[1]
.clone()
.into_uint()
.unwrap();
let price = self
.calculate_price(res.return_value[2].clone())
.unwrap()[0];

Ok((Trade { received_amount, gas_used, price }, res.simulation_result.state_updates))
}

pub async fn get_limits(
&self,
pair_id: String,
sell_token: Address,
buy_token: Address,
block: u64,
overwrites: Option<HashMap<rAddress, HashMap<U256, U256>>>,
) -> Result<(u64, u64), ProtosimError> {
let args = vec![
self.hexstring_to_bytes(&pair_id)?,
Token::Address(sell_token),
Token::Address(buy_token),
];

let res = self
.call("getLimits", args, block, None, overwrites, None, U256::zero())
.await?
.return_value;
Ok((
res[0]
.clone()
.into_uint()
.unwrap()
.as_u64(),
res[1]
.clone()
.into_uint()
.unwrap()
.as_u64(),
))
}

pub async fn get_capabilities(
&self,
pair_id: String,
sell_token: Address,
buy_token: Address,
) -> Result<HashSet<Capability>, ProtosimError> {
let args = vec![
self.hexstring_to_bytes(&pair_id)?,
Token::Address(sell_token),
Token::Address(buy_token),
];

let res = self
.call("getCapabilities", args, 1, None, None, None, U256::zero())
.await?
.return_value;
let capabilities: HashSet<Capability> = res
.into_iter()
.filter_map(|token| {
if let Token::Uint(value) = token {
Capability::from_uint(value).ok()
} else {
None
}
})
.collect();

Ok(capabilities)
}

pub async fn min_gas_usage(&self) -> Result<u64, ProtosimError> {
let res = self
.call("minGasUsage", vec![], 1, None, None, None, U256::zero())
.await?
.return_value;
Ok(res[0]
.clone()
.into_uint()
.unwrap()
.as_u64())
}

fn hexstring_to_bytes(&self, pair_id: &str) -> Result<Token, ProtosimError> {
let bytes = hex::decode(pair_id).map_err(|_| {
ProtosimError::EncodingError(format!("Invalid hex string: {}", pair_id))
})?;
Ok(Token::FixedBytes(bytes))
}

fn calculate_price(&self, value: Token) -> Result<Vec<f64>, ProtosimError> {
if let Token::Array(fractions) = value {
// Map over each `Token::Tuple` in the array
fractions
.into_iter()
.map(|fraction_token| {
if let Token::Tuple(ref components) = fraction_token {
let numerator = components[0]
.clone()
.into_uint()
.unwrap();
let denominator = components[1]
.clone()
.into_uint()
.unwrap();
if denominator.is_zero() {
Err(ProtosimError::DecodingError("Denominator is zero".to_string()))
} else {
Ok((numerator.as_u128() as f64) / (denominator.as_u128() as f64))
}
} else {
Err(ProtosimError::DecodingError("Invalid fraction tuple".to_string()))
}
})
.collect()
} else {
Err(ProtosimError::DecodingError("Expected Token::Array".to_string()))
}
}
}
19 changes: 4 additions & 15 deletions src/protocol/vm/erc20_overwrite_factory.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
// 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 crate::protocol::vm::{
errors::FileError,
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
Expand Down
69 changes: 69 additions & 0 deletions src/protocol/vm/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// TODO: remove skips for clippy

use crate::evm::simulation::SimulationError;
use serde_json::Error as SerdeError;
use std::io;
use thiserror::Error;

/// Represents the outer-level, user-facing errors of the Protosim package.
///
/// `ProtosimError` encompasses all possible errors that can occur in the package,
/// wrapping lower-level errors in a user-friendly way for easier handling and display.
///
/// Variants:
/// - `AbiError`: Represents an error when loading the ABI file, encapsulating a `FileError`.
/// - `EncodingError`: Denotes an error in encoding data.
/// - `SimulationFailure`: Wraps errors that occur during simulation, containing a
/// `SimulationError`.
/// - `DecodingError`: Indicates an error in decoding data.
#[derive(Error, Debug)]
pub enum ProtosimError {
#[error("ABI loading error: {0}")]
AbiError(FileError),
#[error("Encoding error: {0}")]
EncodingError(String),
#[error("Simulation failure error: {0}")]
SimulationFailure(SimulationError),
#[error("Decoding error: {0}")]
DecodingError(String),
}

#[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),
#[error("I/O error {0}")]
Io(io::Error),
#[error("Json parsing error {0}")]
Parse(SerdeError),
}

impl From<io::Error> for FileError {
fn from(err: io::Error) -> Self {
FileError::Io(err)
}
}

impl From<SerdeError> for FileError {
fn from(err: SerdeError) -> Self {
FileError::Parse(err)
}
}

impl From<FileError> for ProtosimError {
fn from(err: FileError) -> Self {
ProtosimError::AbiError(err)
}
}
impl From<SimulationError> for ProtosimError {
fn from(err: SimulationError) -> Self {
ProtosimError::SimulationFailure(err)
}
}
4 changes: 4 additions & 0 deletions src/protocol/vm/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
mod adapter_contract;
mod constants;
mod engine;
mod erc20_overwrite_factory;
mod errors;
mod models;
mod protosim_contract;
pub mod utils;
36 changes: 36 additions & 0 deletions src/protocol/vm/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// TODO: remove skip for clippy dead_code check
use crate::protocol::vm::errors::ProtosimError;
use ethers::abi::Uint;

#[allow(dead_code)]
#[derive(Eq, PartialEq, Hash)]
pub enum Capability {
SellSide = 0,
BuySide = 1,
PriceFunction = 2,
FeeOnTransfer = 3,
ConstantPrice = 4,
TokenBalanceIndependent = 5,
ScaledPrice = 6,
HardLimits = 7,
MarginalPrice = 8,
}

impl Capability {
pub fn from_uint(value: Uint) -> Result<Self, ProtosimError> {
match value.as_u32() {
0 => Ok(Capability::SellSide),
1 => Ok(Capability::BuySide),
2 => Ok(Capability::PriceFunction),
3 => Ok(Capability::FeeOnTransfer),
4 => Ok(Capability::ConstantPrice),
5 => Ok(Capability::TokenBalanceIndependent),
6 => Ok(Capability::ScaledPrice),
7 => Ok(Capability::HardLimits),
8 => Ok(Capability::MarginalPrice),
_ => {
Err(ProtosimError::DecodingError(format!("Unexpected Capability value: {}", value)))
}
}
}
}
Loading

0 comments on commit 7746337

Please sign in to comment.