Skip to content

Commit

Permalink
Move over documentations from the other PR
Browse files Browse the repository at this point in the history
  • Loading branch information
m30m committed Feb 1, 2024
1 parent dc17445 commit 28d0a88
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 35 deletions.
9 changes: 9 additions & 0 deletions per_sdk/protocols/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Opportunity Monitor

The monitor 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 liquidation server. If they wish to act on an opportunity, they can simply construct a signature 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 integration work. The monitor service is important in enabling this, as it publishes the all the necessary information that searchers need for signing their intent on executing the liquidations.

Each protocol that integrates with Express Relay and the LiquidationAdapter workflow must provide code that publishes liquidation opportunities; the example file for the TokenVault dummy contract is found in `/protocols`. Some common types are defined in `utils/types_liquidation_adapter.py`, and standard functions for accessing Pyth prices can be found in `utils/pyth_prices.py`. The exact interface of the methods in the protocol's file is not important, but it should have a similar entrypoint with the same command line arguments and general behavior of sending liquidation opportunities to the liquidation server when specified.

The party that runs the monitor can run the protocol-provided file to surface liquidation opportunities to the liquidation server.
10 changes: 10 additions & 0 deletions per_sdk/searcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Searcher Integration

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

1. Simple integration with LiquidationAdapter via an Externally Owned Account (EOA)
2. Advanced integration with PER using a customized searcher contract

Option 2 requires searchers 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 additional work per each protocol that the searcher wants to integrate with.

Meanwhile, option 1 requires much less work and does not require contract deployment by the searcher. For option 1, the searcher submits their bid on the liquidation opportunity to the liquidation server, which handles transaction submission, routing the liquidation logic to the protocol and also performs safety checks to ensure that the searcher is paying and receiving the appropriate amounts, as specified in the liquidation opportunity structure. 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 using the liquidation server, and if they wish to bid on a liquidation opportunity, they can submit it via the same server. `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).
20 changes: 11 additions & 9 deletions per_sdk/searcher/searcher_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import web3
from eth_abi import encode
from eth_account.datastructures import SignedMessage
from web3.auto import w3


class UserLiquidationParams(TypedDict):
class BidInfo(TypedDict):
bid: int
valid_until: int

Expand All @@ -16,24 +17,23 @@ def construct_signature_liquidator(
address: str,
liq_calldata: bytes,
value: int,
bid: int,
valid_until: int,
bid_info: BidInfo,
secret_key: str,
):
) -> SignedMessage:
"""
Constructs a signature for a liquidator's transaction to submit to the LiquidationAdapter contract.
Constructs a signature for a liquidator's bid to submit to the liquidation server.
Args:
repay_tokens: A list of tuples (token address, amount) representing the tokens to repay.
receipt_tokens: A list of tuples (token address, amount) representing the tokens to receive.
address: The address of the LiquidationAdapter contract.
address: The address of the protocol contract for the liquidation.
liq_calldata: The calldata for the liquidation method call.
value: The value for the liquidation method call.
bid: The amount of native token to bid on this opportunity.
valid_until: The timestamp at which the transaction will expire.
secret_key: A 0x-prefixed hex string representing the liquidator's private key.
Returns:
An web3 ECDSASignature object, representing the liquidator's signature.
A SignedMessage object, representing the liquidator's signature.
"""

digest = encode(
Expand All @@ -45,9 +45,11 @@ def construct_signature_liquidator(
"uint256",
"uint256",
],
[repay_tokens, receipt_tokens, address, liq_calldata, value, bid],
[repay_tokens, receipt_tokens, address, liq_calldata, value, bid_info["bid"]],
)
msg_data = web3.Web3.solidity_keccak(
["bytes", "uint256"], [digest, bid_info["valid_until"]]
)
msg_data = web3.Web3.solidity_keccak(["bytes", "uint256"], [digest, valid_until])
signature = w3.eth.account.signHash(msg_data, private_key=secret_key)

return signature
66 changes: 40 additions & 26 deletions per_sdk/searcher/simple_searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import httpx
from eth_account import Account

from per_sdk.searcher.searcher_utils import (
UserLiquidationParams,
construct_signature_liquidator,
)
from per_sdk.searcher.searcher_utils import BidInfo, construct_signature_liquidator
from per_sdk.utils.endpoints import (
LIQUIDATION_SERVER_ENDPOINT_BID,
LIQUIDATION_SERVER_ENDPOINT_GETOPPS,
Expand All @@ -24,8 +21,21 @@
def assess_liquidation_opportunity(
default_bid: int,
opp: LiquidationOpportunity,
) -> UserLiquidationParams | None:
user_liquidation_params: UserLiquidationParams = {
) -> BidInfo | None:
"""
Assesses whether a liquidation opportunity is worth liquidating; if so, returns the bid and valid_until timestamp. Otherwise returns None.
This function determines whether the given opportunity deals with the specified repay and receipt tokens that the searcher wishes to transact in and whether it is profitable to execute the liquidation.
There are many ways to evaluate this, but the most common way is to check that the value of the amount the searcher will receive from the liquidation exceeds the value of the amount repaid.
Individual searchers will have their own methods to determine market impact and the profitability of conducting a liquidation. This function can be expanded to include external prices to perform this evaluation.
If the opporutnity is deemed worthwhile, this function can return a bid amount representing the amount of native token to bid on this opportunity, and a timestamp representing the time at which the transaction will expire.
Otherwise, this function can return None.
Args:
default_bid: The default amount of bid for liquidation opportunities.
opp: A LiquidationOpportunity object, representing a single liquidation opportunity.
Returns:
If the opportunity is deemed worthwhile, this function can return a BidInfo object, representing the user's bid and the timestamp at which the user's bid should expire. If the LiquidationOpportunity is not deemed worthwhile, this function can return None.
"""
user_liquidation_params = {
"bid": default_bid,
"valid_until": VALID_UNTIL,
}
Expand All @@ -42,8 +52,17 @@ class OpportunityBid(TypedDict):


def create_liquidation_transaction(
opp: LiquidationOpportunity, sk_liquidator: str, valid_until: int, bid: int
opp: LiquidationOpportunity, sk_liquidator: str, bid_info: BidInfo
) -> OpportunityBid:
"""
Creates a bid for a liquidation opportunity.
Args:
opp: A LiquidationOpportunity object, representing a single liquidation opportunity.
sk_liquidator: A 0x-prefixed hex string representing the liquidator's private key.
bid_info: necessary information for the liquidation bid
Returns:
An OpportunityBid object which can be sent to the liquidation server
"""
repay_tokens = [
(opp["contract"], int(opp["amount"])) for opp in opp["repay_tokens"]
]
Expand All @@ -60,21 +79,20 @@ def create_liquidation_transaction(
opp["contract"],
liq_calldata,
int(opp["value"]),
bid,
valid_until,
bid_info,
sk_liquidator,
)

json_body = {
opportunity_bid = {
"opportunity_id": opp["opportunity_id"],
"permission_key": opp["permission_key"],
"bid_amount": str(bid),
"valid_until": str(valid_until),
"bid_amount": str(bid_info["bid"]),
"valid_until": str(bid_info["valid_until"]),
"liquidator": liquidator,
"signature": bytes(signature_liquidator.signature).hex(),
}

return json_body
return opportunity_bid


async def main():
Expand Down Expand Up @@ -109,15 +127,17 @@ async def main():
log_handler.setFormatter(formatter)
logger.addHandler(log_handler)

params = {"chain_id": args.chain_id}
sk_liquidator = args.private_key
liquidator = Account.from_key(sk_liquidator).address
logger.info("Liquidator address: %s", liquidator)
client = httpx.AsyncClient()
while True:
try:
accounts_liquidatable = (
await client.get(LIQUIDATION_SERVER_ENDPOINT_GETOPPS, params=params)
await client.get(
LIQUIDATION_SERVER_ENDPOINT_GETOPPS,
params={"chain_id": args.chain_id},
)
).json()
except Exception as e:
logger.error(e)
Expand All @@ -126,24 +146,18 @@ async def main():

logger.debug("Found %d liquidation opportunities", len(accounts_liquidatable))
for liquidation_opp in accounts_liquidatable:
user_liquidation_params = assess_liquidation_opportunity(
args.bid, liquidation_opp
)

if user_liquidation_params is not None:
bid, valid_until = (
user_liquidation_params["bid"],
user_liquidation_params["valid_until"],
)
bid_info = assess_liquidation_opportunity(args.bid, liquidation_opp)

if bid_info is not None:

tx = create_liquidation_transaction(
liquidation_opp, sk_liquidator, valid_until, bid
liquidation_opp, sk_liquidator, bid_info
)

resp = await client.post(LIQUIDATION_SERVER_ENDPOINT_BID, json=tx)
logger.info(
"Submitted bid amount %s for opportunity %s, server response: %s",
bid,
bid_info["bid"],
liquidation_opp["opportunity_id"],
resp.text,
)
Expand Down

0 comments on commit 28d0a88

Please sign in to comment.