Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault simulator #11

Merged
merged 41 commits into from
Feb 4, 2024
Merged
Changes from 34 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
32871f1
Fix comment
m30m Jan 23, 2024
95111ff
Initialize project
m30m Jan 23, 2024
dbee4dc
First version of the simulator
m30m Jan 28, 2024
bbce0ee
Add to pre-commit formatting
m30m Jan 28, 2024
c6295bf
Refactor setup script
m30m Jan 28, 2024
aa5d4be
Add setupTestnet script
m30m Jan 28, 2024
41e5fff
Small changes to TokenVault contract for easier integration with the …
m30m Jan 28, 2024
f481b5f
Use price feed id as the name of WETH contract for consistency
m30m Jan 28, 2024
20cb96a
Update docs
m30m Jan 28, 2024
1087605
Update .gitignore
m30m Jan 28, 2024
80cef21
Update token vault searcher
m30m Jan 28, 2024
2efc4eb
Add value field to Adapter structs and logic
m30m Jan 29, 2024
a7bb735
Add new command to setup an already funded searcher account
m30m Jan 31, 2024
d7b2c21
Clean up errros and add signatures
m30m Jan 31, 2024
6d43b0b
Implement bid on opportunity endpoint
m30m Jan 31, 2024
c843126
Fix on searcher and vault monitor
m30m Jan 31, 2024
f4ff294
Setup poetry and replace autopep8 with black
m30m Jan 31, 2024
de0a858
Rename beacon server to liquidation server
m30m Jan 31, 2024
3bb95dd
Fix ci for poetry
m30m Jan 31, 2024
5894023
Fix CI part 2
m30m Jan 31, 2024
139c010
Update poetry version
m30m Jan 31, 2024
596054d
Adjust requirements
m30m Jan 31, 2024
326975e
Adjust versioning again
m30m Jan 31, 2024
2869b88
Final fix
m30m Jan 31, 2024
5903deb
Get hermes endpoint as parameter in simulator
m30m Jan 31, 2024
fb20273
Setup the new account from scratch
m30m Jan 31, 2024
0e4d3e0
Add interval option for simulator to create vaults periodically
m30m Jan 31, 2024
8d13521
Improve logging on per_sdk
m30m Jan 31, 2024
6a80dac
Reduce ETH amounts to be more reasonable in actual testnets
m30m Feb 1, 2024
84407e9
Correct usage of chain_id in scripts
m30m Feb 1, 2024
d3c0d07
Minor refactor
m30m Feb 1, 2024
f407a48
Fix api docs
m30m Feb 1, 2024
dc17445
Remove the opportunity upon sucessfull submission
m30m Feb 1, 2024
28d0a88
Move over documentations from the other PR
m30m Feb 1, 2024
394d8a9
Address comments
m30m Feb 3, 2024
31deb9b
Move error handling logic partly into auction
m30m Feb 4, 2024
1c06ebd
Add build scripts
m30m Feb 4, 2024
b82e4d0
Use config for poll interval
m30m Feb 4, 2024
f12571b
Temp serde
m30m Feb 3, 2024
20c6e04
Direct parsing of U256 and Signature using serde custom module
m30m Feb 4, 2024
e3041a0
Add comment on why token names are pricefeed ids
m30m Feb 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/actions/python-poetry/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Python Poetry
description: Sets up a Python environment with Poetry

inputs:
python-version:
required: false
description: Python version
default: "3.11"
poetry-version:
required: false
description: Poetry version
default: "1.6.1"

runs:
using: composite
steps:
- uses: actions/setup-python@v2
with:
python-version: ${{ inputs.python-version }}
- uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: ${{ inputs.poetry-version }}
- run: poetry -C per_sdk install
shell: sh
2 changes: 1 addition & 1 deletion .github/workflows/ci-pre-commit.yml
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ jobs:
with:
# Need to grab the history of the PR
fetch-depth: 0
- uses: actions/setup-python@v2
- uses: ./.github/actions/python-poetry
- uses: actions-rs/toolchain@v1
with:
profile: minimal
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -15,3 +15,7 @@ api_keys.py
# env files
*.env
per_multicall/latestEnvironment.json

**/target/
node_modules
.idea
28 changes: 25 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -24,9 +24,31 @@ repos:
entry: cargo +nightly-2023-07-23 fmt --manifest-path ./auction-server/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: auction-server
- repo: local
hooks:
# Hooks for vault-simulator
- id: cargo-fmt-vault-simulator
name: Cargo format for vault simulator
language: "rust"
entry: cargo +nightly-2023-07-23 fmt --manifest-path ./vault-simulator/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: vault-simulator

# for python files
- repo: https://github.com/hhatto/autopep8
rev: "v2.0.4"
- repo: local
hooks:
- id: autopep8
- id: isort
name: isort
entry: poetry -C per_sdk run isort --profile=black per_sdk
pass_filenames: false
language: system
- id: black
name: black
entry: poetry -C per_sdk run black per_sdk
pass_filenames: false
language: system
- id: pyflakes
name: pyflakes
entry: poetry -C per_sdk run pyflakes per_sdk
pass_filenames: false
language: system
1 change: 0 additions & 1 deletion auction-server/.gitignore

This file was deleted.

3 changes: 2 additions & 1 deletion auction-server/config.sample.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
chains:
development:
geth_rpc_addr: http://localhost:8545
contract_addr: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
per_contract: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
adapter_contract: 0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e
legacy_tx: false
23 changes: 21 additions & 2 deletions auction-server/src/api.rs
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@ use {
LocalWallet,
Signer,
},
types::Bytes,
},
futures::future::join_all,
std::{
@@ -58,6 +59,7 @@ use {
},
Arc,
},
time::Duration,
},
tower_http::cors::CorsLayer,
utoipa::{
@@ -90,6 +92,12 @@ pub enum RestError {
BadParameters(String),
/// The chain id is not supported
InvalidChainId,
/// The simulation failed
SimulationError {
#[schema(value_type=String)]
result: Bytes,
reason: String,
},
/// The order was not found
OpportunityNotFound,
/// The server cannot currently communicate with the blockchain, so is not able to verify
@@ -108,6 +116,11 @@ impl IntoResponse for RestError {
RestError::InvalidChainId => {
(StatusCode::BAD_REQUEST, "The chain id is not supported").into_response()
}
RestError::SimulationError { result, reason } => (
StatusCode::BAD_REQUEST,
format!("Simulation failed: {} ({})", result, reason),
)
.into_response(),
RestError::OpportunityNotFound => (
StatusCode::NOT_FOUND,
"Order with the specified id was not found",
@@ -141,6 +154,7 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> {
paths(
rest::bid,
marketplace::submit_opportunity,
marketplace::bid_opportunity,
marketplace::fetch_opportunities,
),
components(
@@ -165,15 +179,16 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> {

let chain_store: Result<HashMap<ChainId, ChainStore>> = join_all(config.chains.iter().map(
|(chain_id, chain_config)| async move {
let provider =
Provider::<Http>::try_from(chain_config.geth_rpc_addr.clone()).map_err(|err| {
let mut provider = Provider::<Http>::try_from(chain_config.geth_rpc_addr.clone())
.map_err(|err| {
anyhow!(
"Failed to connect to chain({chain_id}) at {rpc_addr}: {:?}",
err,
chain_id = chain_id,
rpc_addr = chain_config.geth_rpc_addr
)
})?;
provider.set_interval(Duration::from_secs(1));
m30m marked this conversation as resolved.
Show resolved Hide resolved
let id = provider.get_chainid().await?.as_u64();
Ok((
chain_id.clone(),
@@ -212,6 +227,10 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> {
"/liquidation/fetch_opportunities",
get(marketplace::fetch_opportunities),
)
.route(
"/liquidation/bid_opportunity",
post(marketplace::bid_opportunity),
)
.layer(CorsLayer::permissive())
.with_state(server_store);

124 changes: 92 additions & 32 deletions auction-server/src/api/marketplace.rs
Original file line number Diff line number Diff line change
@@ -4,7 +4,10 @@ use {
rest::handle_bid,
RestError,
},
liquidation_adapter::make_liquidator_calldata,
liquidation_adapter::{
make_liquidator_calldata,
parse_revert_error,
},
state::Store,
},
axum::{
@@ -23,7 +26,10 @@ use {
Deserialize,
Serialize,
},
std::sync::Arc,
std::{
str::FromStr,
sync::Arc,
},
utoipa::ToSchema,
uuid::Uuid,
};
@@ -54,11 +60,25 @@ pub struct LiquidationOpportunity {
/// Calldata for the contract call.
#[schema(example = "0xdeadbeef", value_type=String)]
calldata: Bytes,
/// The value to send with the contract call.
#[schema(example = "1")]
value: String,
m30m marked this conversation as resolved.
Show resolved Hide resolved

repay_tokens: Vec<TokenQty>,
receipt_tokens: Vec<TokenQty>,
}

/// A submitted liquidation opportunity ready to be executed.
/// If a searcher signs the opportunity and have approved enough tokens to liquidation adapter, by calling this contract with the given calldata and structures, they will receive the tokens specified in the receipt_tokens field, and will send the tokens specified in the repay_tokens field.
m30m marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Serialize, Deserialize, ToSchema, Clone)]
pub struct LiquidationOpportunityWithId {
/// The opportunity unique id
opportunity_id: Uuid,
/// opportunity data
#[serde(flatten)]
opportunity: LiquidationOpportunity,
}

impl From<(Address, U256)> for TokenQty {
fn from(token: (Address, U256)) -> Self {
TokenQty {
@@ -86,7 +106,7 @@ fn parse_tokens(tokens: Vec<TokenQty>) -> Result<Vec<(Address, U256)>, RestError
///
/// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database and will be available for bidding.
#[utoipa::path(post, path = "/liquidation/submit_opportunity", request_body = LiquidationOpportunity, responses(
(status = 200, description = "Opportunity was stored succesfuly", body = String),
(status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String),
(status = 400, response=RestError)
),)]
pub async fn submit_opportunity(
@@ -103,20 +123,23 @@ pub async fn submit_opportunity(

//TODO: Verify if the call actually works

let id = Uuid::new_v4();
store.liquidation_store.opportunities.write().await.insert(
opportunity.permission_key.clone(),
crate::state::VerifiedLiquidationOpportunity {
id: Uuid::new_v4(),
id,
chain_id: opportunity.chain_id.clone(),
permission_key: opportunity.permission_key,
contract: opportunity.contract,
calldata: opportunity.calldata,
value: U256::from_dec_str(opportunity.value.as_str())
.map_err(|_| RestError::BadParameters("Invalid value".to_string()))?,
repay_tokens,
receipt_tokens,
},
);

Ok("OK".to_string())
Ok(id.to_string())
}

/// Fetch all liquidation opportunities ready to be exectued.
@@ -126,29 +149,33 @@ pub async fn submit_opportunity(
),)]
pub async fn fetch_opportunities(
State(store): State<Arc<Store>>,
) -> Result<axum::Json<Vec<LiquidationOpportunity>>, RestError> {
let opportunities: Vec<LiquidationOpportunity> = store
) -> Result<axum::Json<Vec<LiquidationOpportunityWithId>>, RestError> {
let opportunities: Vec<LiquidationOpportunityWithId> = store
.liquidation_store
.opportunities
.read()
.await
.values()
.cloned()
.map(|opportunity| LiquidationOpportunity {
permission_key: opportunity.permission_key,
chain_id: opportunity.chain_id,
contract: opportunity.contract,
calldata: opportunity.calldata,
repay_tokens: opportunity
.repay_tokens
.into_iter()
.map(TokenQty::from)
.collect(),
receipt_tokens: opportunity
.receipt_tokens
.into_iter()
.map(TokenQty::from)
.collect(),
.map(|opportunity| LiquidationOpportunityWithId {
opportunity_id: opportunity.id,
opportunity: LiquidationOpportunity {
permission_key: opportunity.permission_key,
chain_id: opportunity.chain_id,
contract: opportunity.contract,
calldata: opportunity.calldata,
value: opportunity.value.to_string(),
repay_tokens: opportunity
.repay_tokens
.into_iter()
.map(TokenQty::from)
.collect(),
receipt_tokens: opportunity
.receipt_tokens
.into_iter()
.map(TokenQty::from)
.collect(),
},
})
.collect();

@@ -174,8 +201,8 @@ pub struct OpportunityBid {
liquidator: Address,
#[schema(
example = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
,value_type=String)]
signature: Signature,
)]
signature: String,
m30m marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(Clone, Copy)]
@@ -187,8 +214,13 @@ pub struct VerifiedOpportunityBid {
pub signature: Signature,
}

/// Bid on liquidation opportunity
#[utoipa::path(post, path = "/liquidation/bid_opportunity", request_body=OpportunityBid, responses(
(status = 200, description = "Bid Result", body = String),
(status = 400, response=RestError)
),)]
pub async fn bid_opportunity(
store: Arc<Store>,
State(store): State<Arc<Store>>,
Json(opportunity_bid): Json<OpportunityBid>,
) -> Result<String, RestError> {
let opportunities = store.liquidation_store.opportunities.read().await;
@@ -202,31 +234,59 @@ pub async fn bid_opportunity(
"Invalid opportunity_id".to_string(),
));
}

let chain_store = store
.chains
.get(&liquidation.chain_id)
.ok_or(RestError::InvalidChainId)?;

let bid_amount = U256::from_dec_str(opportunity_bid.bid_amount.as_str())
.map_err(|_| RestError::BadParameters("Invalid bid_amount".to_string()))?;
let valid_until = U256::from_dec_str(opportunity_bid.valid_until.as_str())
.map_err(|_| RestError::BadParameters("Invalid valid_until".to_string()))?;

let signature = Signature::from_str(opportunity_bid.signature.as_str())
.map_err(|_| RestError::BadParameters("Invalid signature".to_string()))?;
let verified_liquidation_bid = VerifiedOpportunityBid {
opportunity_id: opportunity_bid.opportunity_id,
bid_amount,
valid_until,
liquidator: opportunity_bid.liquidator,
signature: opportunity_bid.signature,
signature,
};

let per_calldata = make_liquidator_calldata(liquidation.clone(), verified_liquidation_bid)
.map_err(|e| RestError::BadParameters(e.to_string()))?;

handle_bid(
let per_calldata = make_liquidator_calldata(
liquidation.clone(),
verified_liquidation_bid,
chain_store.provider.clone(),
chain_store.config.adapter_contract,
)
.await
.map_err(|e| RestError::BadParameters(e.to_string()))?;
match handle_bid(
store.clone(),
crate::api::rest::ParsedBid {
permission_key: liquidation.permission_key.clone(),
chain_id: liquidation.chain_id.clone(),
contract: liquidation.contract,
contract: chain_store.config.adapter_contract,
calldata: per_calldata,
bid_amount: verified_liquidation_bid.bid_amount,
},
)
.await
{
Ok(_) => Ok("OK".to_string()),
Err(e) => match e {
RestError::SimulationError { result, reason } => {
let parsed = parse_revert_error(result.clone());
match parsed {
Some(decoded) => Err(RestError::BadParameters(decoded)),
None => {
tracing::info!("Could not parse revert reason: {}", reason);
Err(RestError::SimulationError { result, reason })
}
}
}
_ => Err(e),
},
}
}
Loading