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

beacon interface #10

Closed
wants to merge 18 commits into from
29 changes: 9 additions & 20 deletions auction-server/src/api/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ pub async fn surface(
/// Get liquidation opportunities
///
// #[axum_macros::debug_handler]
#[utoipa::path(get, path = "/getOpps",
#[utoipa::path(get, path = "/getOpps",
params(
("chain_id" = String, Query, description = "Chain ID to retrieve opportunities for"),
("contract" = Option<String>, Query, description = "Contract address to filter by")
Expand All @@ -192,11 +192,11 @@ pub async fn surface(
,)]
pub async fn get_opps(
State(store): State<Arc<Store>>,
Query(params): Query<GetOppsParams>,
) -> Result<Json<Vec<Opportunity>>, RestError> {
Query(params): Query<GetOppsParams>
) -> Result<Json<Vec<Opportunity>>, RestError> {
let chain_id = params.chain_id;
let contract = params.contract;

let chain_store = store
.chains
.get(&chain_id)
Expand All @@ -210,28 +210,17 @@ pub async fn get_opps(
.parse::<Address>()
.map_err(|_| RestError::BadParameters("Invalid contract address".to_string()))?;

opps = chain_store
.opps
.write()
.await
.entry(key)
.or_default()
.to_vec();
}
opps = chain_store.opps.write().await.entry(key).or_default().to_vec();
},
None => {
// TODO: fix this double write access, to make this work
let opps_dict = chain_store.opps.write().await;

for key in opps_dict.keys() {
let opps_key = chain_store
.opps
.write()
.await
.entry(key.clone())
.or_default()
.clone();
let opps_key = chain_store.opps.write().await.entry(key.clone()).or_default().clone();
opps.extend(opps_key);
}
}
}
Ok(Json(opps))
}
}
9 changes: 9 additions & 0 deletions beacon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Beacon

The Beacon is the off-chain service that exposes liquidation opportunities on integrated protocols to searchers. Protocols surface their liquidatable vaults/accounts along with the calldata and the token denominations and amounts involved in the transaction. Searchers can query these opportunities from the Beacon server. If they wish to act on an opportunity, they can simply construct a transaction based off the information in the opportunity.

The LiquidationAdapter contract that is part of the Express Relay on-chain stack allows searchers to perform liquidations across different protocols without needing to deploy their own contracts or perform bespoke engineering work. The Beacon service is important in enabling this, as it disseminates the calldata that searchers need to include in the transactions they construct.

Each protocol that integrates with Express Relay and the LiquidationAdapter workflow must provide code that handles getting liquidatable accounts; the template and example files for this are found in `/protocols`. Some common types are defined in `utils/types_liquidation_adapter.py`, and standard functions for accessing Pyth Hermes prices can be found in `utils/pyth_prices.py`.

The party that runs the beacon can run the protocol-provided file to get and surface liquidatable accounts to the Beacon server.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need more step to step documentation on what a protocol needs to do for integration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this readme should contain documentation for the protocol integration just for the Beacon server? other integration steps should be documented elsewhere, and we can unify in a single README/doc located higher-up in the repo?

63 changes: 33 additions & 30 deletions beacon/protocols/beacon_TokenVault.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
from eth_abi import encode
import json
from typing import TypedDict
import argparse

from beacon.utils.pyth_prices import *
from beacon.utils.types_liquidation_adapter import *

TOKEN_VAULT_ADDRESS = "0x72A22FfcAfa6684d4EE449620270ac05afE963d0"
CHAIN_RPC_ENDPOINT = "http://localhost:8545"


class LiquidationAccount(TypedDict):
Expand All @@ -30,15 +30,10 @@ def get_vault_abi():
return data['abi']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the address in this function make sense?



"""
get_accounts() is the first method that the protocol should implement. It should take no arguments and return all the open accounts in the protocol in the form of a list of objects of type LiquidationAccount (defined above). Each LiquidationAccount object represents an account/vault in the protocol.
This function can be implemented in any way, but it should be able to return all the open accounts in the protocol. For some protocols, this may be easily doable by just querying on-chain state; however, most protocols will likely need to maintain or access an off-chain indexer to get the list of all open accounts.
"""


async def get_accounts() -> list[LiquidationAccount]:
async def get_accounts(rpc_url: str) -> list[LiquidationAccount]:
abi = get_vault_abi()
w3 = web3.AsyncWeb3(web3.AsyncHTTPProvider(CHAIN_RPC_ENDPOINT))
w3 = web3.AsyncWeb3(web3.AsyncHTTPProvider(rpc_url))
token_vault = w3.eth.contract(
address=TOKEN_VAULT_ADDRESS,
abi=abi)
Expand Down Expand Up @@ -125,13 +120,6 @@ def create_liquidation_opp(
return opp


"""
get_liquidatable(accounts, prices) is the second method that the protocol should implement. It should take two arguments: account--a list of Account (defined above) objects--and prices--a dictionary of Pyth prices.
accounts should be the list of all open accounts in the protocol (i.e. the output of get_accounts()).
prices should be a dictionary of Pyth prices, where the keys are Pyth feed IDs and the values are PriceFeed objects. prices can be retrieved from the provided price retrieval functions.
This function should return a lists of liquidation opportunities. Each opportunity should be of the form LiquidationOpportunity defined above.
"""


def get_liquidatable(accounts: list[LiquidationAccount],
prices: dict[str,
Expand All @@ -156,23 +144,38 @@ def get_liquidatable(accounts: list[LiquidationAccount],


async def main():
# get all accounts
accounts = await get_accounts()
parser = argparse.ArgumentParser()
parser.add_argument("--operator_api_key", type=str, required=True, help="Operator API key, used to authenticate the surface post request")
parser.add_argument("--rpc_url", type=str, required=True, help="Chain RPC endpoint, used to fetch on-chain data via get_accounts")
parser.add_argument("--beacon_server_url", type=str, help="Beacon server endpoint; if provided, will send liquidation opportunities to the beacon server; otherwise, will just print them out")
args = parser.parse_args()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need some general flags like --help too. I suggest using some higher level library like typer:
https://typer.tiangolo.com/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argparse has help built in. what are the benefits of using typer? seems like it's less widely adopted/well-known than argparse, and it doesn't offer as easy support for mutually exclusive arguments like argparse does


# get prices
pyth_price_feed_ids = await get_price_feed_ids()
pyth_prices_latest = []
i = 0
cntr = 100
while len(pyth_price_feed_ids[i:i + cntr]) > 0:
pyth_prices_latest += await get_pyth_prices_latest(pyth_price_feed_ids[i:i + cntr])
i += cntr
pyth_prices_latest = dict(pyth_prices_latest)

# get liquidatable accounts
liquidatable = get_liquidatable(accounts, pyth_prices_latest)

print(liquidatable)
feed_ids = ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"] # TODO: should this be automated rather than hardcoded?
price_feed_client = PriceFeedClient(feed_ids)

ws_call = price_feed_client.ws_pyth_prices()
task = asyncio.create_task(ws_call)

client = httpx.AsyncClient()

await asyncio.sleep(2)

while True:
# get all accounts
accounts = await get_accounts(args.rpc_url)

liquidatable = get_liquidatable(accounts, price_feed_client.prices_dict)

if args.beacon_server_url:
resp = await client.post(
args.beacon_server_url,
json=liquidatable
)
print(f"Response, post to beacon: {resp.text}")
else:
print(liquidatable)
await asyncio.sleep(2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if these sleeps are necessary. We have a bunch of network requests in the middle, so each loop naturally takes some hundreds of milliseconds.


if __name__ == "__main__":
asyncio.run(main())
109 changes: 67 additions & 42 deletions beacon/protocols/beacon_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,116 @@
from eth_abi import encode
import json
from typing import TypedDict
import argparse

from beacon.utils.pyth_prices import *
from beacon.utils.types_liquidation_adapter import *

PROTOCOL_ADDRESS = "{INSERT}"
CHAIN_RPC_ENDPOINT = "http://localhost:8545"

"""
The protocol should implement a class called LiquidationAccount. This will be the type of the objects in the list returned by get_accounts() and fed into get_liquidatable.
This class should contain all the relevant information about a vault/account on this protocol that is necessary for identifying whether it is eligible for liquidation and constructing a LiquidationOpportunity object.
"""



class LiquidationAccount(TypedDict):
"""
LiquidationAccount is a TypedDict that represents an account/vault in the protocol.

The protocol should implement a class called LiquidationAccount. This will be the type of the objects in the list returned by get_accounts() and fed into get_liquidatable.
This class should contain all the relevant information about a vault/account on this protocol that is necessary for identifying whether it is eligible for liquidation and constructing a LiquidationOpportunity object.
"""

# Keys of the TypedDict and their types
pass


"""
get_accounts() is the first method that the protocol should implement. It should take no arguments and return all the open accounts in the protocol in the form of a list of objects of type LiquidationAccount (defined above). Each LiquidationAccount object represents an account/vault in the protocol.
This function can be implemented in any way, but it should be able to return all the open accounts in the protocol. For some protocols, this may be easily doable by just querying on-chain state; however, most protocols will likely need to maintain or access an off-chain indexer to get the list of all open accounts.
"""


async def get_accounts() -> list[LiquidationAccount]:

async def get_accounts(rpc_url: str) -> list[LiquidationAccount]:
"""
Returns all the open accounts in the protocol in the form of a list of type LiquidationAccount.

get_accounts(rpc_url) is the first method that the protocol should implement. It should take the RPC URL of the chain as an argument and return all the open accounts in the protocol in the form of a list of objects of type LiquidationAccount (defined above). Each LiquidationAccount object represents an account/vault in the protocol.
This function can be implemented in any way, but it should be able to return all the open accounts in the protocol. For some protocols, this may be easily doable by just querying on-chain state; however, most protocols will likely need to maintain or access an off-chain indexer to get the list of all open accounts.
"""

# Fetch all vaults from on-chain state/indexer
# Filter to just active vaults
# Return list of LiquidationAccount
# TODO: complete
pass


"""
create_liquidation_opp is an optional helper function to construct a LiquidationOpportunity from a LiquidationAccount and a set of relevant Pyth PriceFeeds.
If you choose to implement this function, you can call it within get_liquidatable whenever you find a LiquidationAccount eligible for liquidation.
"""


def create_liquidation_opp(
account: LiquidationAccount,
prices: list[PriceFeed]) -> LiquidationOpportunity:
pass
"""
Constructs a LiquidationOpportunity object from a LiquidationAccount object and a set of relevant Pyth PriceFeeds.

This is an optional helper function you can implement. If you choose to do so, you can call it within get_liquidatable whenever you find a LiquidationAccount eligible for liquidation.
"""

pass

"""
get_liquidatable(accounts, prices) is the second method that the protocol should implement. It should take two arguments: account--a list of LiquidationAccount (defined above) objects--and prices--a dictionary of Pyth prices.
accounts should be the list of all open accounts in the protocol (i.e. the output of get_accounts()).
prices should be a dictionary of Pyth prices, where the keys are Pyth feed IDs and the values are PriceFeed objects. prices can be retrieved from the provided price retrieval functions.
This function should return a list of type LiquidationOpportunity.
"""


def get_liquidatable(accounts: list[LiquidationAccount],
prices: dict[str,
PriceFeed]) -> (list[LiquidationOpportunity]):
"""
Filters list of LiquidationAccount types to return a list of LiquidationOpportunity types.

get_liquidatable(accounts, prices) is the second method that the protocol should implement. It should take two arguments: account--a list of LiquidationAccount (defined above) objects--and prices--a dictionary of Pyth prices.
accounts should be the list of all open accounts in the protocol (i.e. the output of get_accounts()).
prices should be a dictionary of Pyth prices, where the keys are Pyth feed IDs and the values are PriceFeed objects. prices can be retrieved from the provided price retrieval functions.
This function should return a list of type LiquidationOpportunity.
"""

# Iterate through accounts
# Determine if account is eligible for liquidation; if so, construct an object of type LiquidationOpportunity and add it to the list
# Return the list of type LiquidationOpportunity containing all the valid liquidation opportunities
pass


"""
The main loop below is a good mechanism to check if your implementations of the functions above are working properly.
"""


async def main():
# get all accounts
accounts = await get_accounts()
"""
main is a good mechanism to check if your implementations of the functions above are working properly.
"""
parser = argparse.ArgumentParser()
parser.add_argument("--operator_api_key", type=str, required=True, help="Operator API key, used to authenticate the surface post request")
parser.add_argument("--rpc_url", type=str, required=True, help="Chain RPC endpoint, used to fetch on-chain data via get_accounts")
parser.add_argument("--beacon_server_url", type=str, help="Beacon server endpoint; if provided, will send liquidation opportunities to the beacon server; otherwise, will just print them out")
args = parser.parse_args()

# get prices
pyth_price_feed_ids = await get_price_feed_ids()
pyth_prices_latest = []
i = 0
cntr = 100
while len(pyth_price_feed_ids[i:i + cntr]) > 0:
pyth_prices_latest += await get_pyth_prices_latest(pyth_price_feed_ids[i:i + cntr])
i += cntr
pyth_prices_latest = dict(pyth_prices_latest)

# get liquidatable accounts
liquidatable = get_liquidatable(
accounts, pyth_prices_latest)

print(liquidatable)
feed_ids = [] # TODO: specify initial price feeds to subscribe to
price_feed_client = PriceFeedClient(feed_ids)

ws_call = price_feed_client.ws_pyth_prices()
task = asyncio.create_task(ws_call)

client = httpx.AsyncClient()

await asyncio.sleep(2)

while True:
# get all accounts
accounts = await get_accounts(args.rpc_url)

liquidatable = get_liquidatable(accounts, price_feed_client.prices_dict)

if args.beacon_server_url:
# this post request will not work without an operator API key; however, this should work fine if get_liquidatable returns the correct type
resp = await client.post(
args.beacon_server_url,
json=liquidatable
)
print(f"Response, post to beacon: {resp.text}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lots of error handling is needed here:

  • beacon is down
  • url is invalid
  • request is invalid

else:
print(liquidatable)
await asyncio.sleep(2)

if __name__ == "__main__":
asyncio.run(main())
10 changes: 10 additions & 0 deletions beacon/searcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Searcher

Searchers can integrate with Express Relay by one of two means:

1. LiquidationAdapter Contract interaction from an EOA
2. Bespoke integration via a deployed contract

Option 2 requires bespoke work to handle individual protocol interfaces and smart contract risk, and it is similar in nature to how many searchers currently do liquidations via their own deployed contracts--searchers can now call into their smart contracts via the Express Relay workflow. This option allows for greater customization by the searcher, but requires bespoke work per protocol that the searcher wants to integrate with.

Meanwhile, option 1 requires much less bespoke work and does not require contract deployment by the searcher. For option 1, the searcher submits liquidation transactions to the LiquidationAdapter contract, which handles routing the liquidation logic to the protocol and also performs some basic safety checks to ensure that the searcher is paying and receiving the appropriate amounts. The searcher can submit transactions signed by their EOA that has custody of the tokens they wish to repay with. Searchers can listen to liquidation opportunities at the Beacon server, and if they wish to submit a liquidation transaction through Express Relay, they can submit it to the auction server endpoint. `searcher_template.py` contains a template for the actions that a searcher may wish to perform, namely getting and assessing opportunities at the Beacon server and constructing and sending a liquidation. Helper functions related to constructing the signature for the LiquidationAdapter contract are in `searcher_utils.py`. A sample workflow is in `searcherA.py` (note: this example lacks any serious evaluation of opportunities, and it simply carries out a liquidation if the opportunity is available).
Loading
Loading