Skip to content

Commit

Permalink
Implement IBC path unwinding.
Browse files Browse the repository at this point in the history
  • Loading branch information
dowlandaiello committed Jul 23, 2024
1 parent d355138 commit ba2b45e
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 19 deletions.
13 changes: 7 additions & 6 deletions local-interchaintest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
base_chain: String::from("neutron"),
dest_chain: String::from("osmosis"),
};
let uosmo = Denom::Local {
base_chain: String::from("osmosis"),
base_denom: String::from("uosmo"),
};
let uosmo_ntrn = Denom::Interchain {
base_denom: String::from("uosmo"),
base_chain: String::from("osmosis"),
dest_chain: String::from("neutron"),
};
let uosmo = Denom::Local {
base_chain: String::from("osmosis"),
base_denom: String::from("uosmo"),
};

TestRunner::new(&mut ctx, args)
.start()?
Expand Down Expand Up @@ -289,7 +289,7 @@ fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
.with_denom(bruhtoken_osmo.clone(), 100000000000)
.with_pool(
untrn.clone(),
bruhtoken.clone(),
uosmo_ntrn.clone(),
Pool::Astroport(
AstroportPoolBuilder::default()
.with_balance_asset_a(10000000u128)
Expand All @@ -310,7 +310,7 @@ fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
),
)
.with_pool(
uosmo_ntrn.clone(),
untrn.clone(),
bruhtoken.clone(),
Pool::Astroport(
AstroportPoolBuilder::default()
Expand All @@ -319,6 +319,7 @@ fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
.build()?,
),
)

.with_arbbot()
.with_test(Box::new(tests::test_osmo_arb) as TestFn)
.build()?,
Expand Down
11 changes: 6 additions & 5 deletions local-interchaintest/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,11 @@ impl<'a> TestRunner<'a> {
.as_str(),
&ntrn_to_osmo,
&osmo_to_ntrn,
)?;
util::create_arbs_file()?;
util::create_netconfig()?;
util::create_denom_file(&self.denom_map)?;
)
.expect("Failed to create deployments file");
util::create_arbs_file().expect("Failed to create arbs file");
util::create_netconfig().expect("Failed to create net config");
util::create_denom_file(&self.denom_map).expect("Failed to create denom file");

let statuses = self.test_statuses.clone();

Expand Down Expand Up @@ -624,7 +625,7 @@ pub fn with_arb_bot_output(test: OwnedTestFn) -> TestResult {
.arg("--deployments_file")
.arg("deployments_file.json")
.arg("--net_config")
.arg("net_config.json")
.arg("net_config_test.json")
.arg("--base_denom")
.arg("untrn")
.arg("--denom_file")
Expand Down
2 changes: 1 addition & 1 deletion local-interchaintest/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pub(crate) fn create_netconfig() -> Result<(), Box<dyn Error + Send + Sync>> {
.create(true)
.truncate(true)
.write(true)
.open("../net_config.json")?;
.open("../net_config_test.json")?;

f.write_all(
serde_json::json!({
Expand Down
4 changes: 4 additions & 0 deletions src/strategies/naive.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,15 @@ async def next_legs(

prev_pool = path[-1]

logger.debug(f"exploring route: %s", fmt_route(path))

# This leg **must** be connected to the previous
# by construction, so therefore, if any
# of the denoms match the starting denom, we are
# finished, and the circuit is closed
if len(path) > 1 and src == prev_pool.out_asset():
logger.debug(f"discovered route: %s", fmt_route(path))

if (
len(required_leg_types - set((fmt_route_leg(leg) for leg in path))) > 0
or len(path) < depth
Expand Down
60 changes: 53 additions & 7 deletions src/strategies/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Defines common utilities shared across arbitrage strategies.
"""

import json
import operator
from functools import reduce
from decimal import Decimal
Expand All @@ -23,6 +24,8 @@
try_multiple_clients_fatal,
try_multiple_clients,
DENOM_QUANTITY_ABORT_ARB,
denom_route,
denom_info_on_chain,
)
from src.scheduler import Ctx
from cosmos.base.v1beta1 import coin_pb2
Expand Down Expand Up @@ -343,10 +346,10 @@ async def recover_funds(
await transfer(
r,
curr_leg.in_asset(),
route[-1].out_asset(),
curr_leg,
backtracked[0],
ctx,
to_swap,
to_transfer,
)

resp = await quantities_for_route_profit(
Expand Down Expand Up @@ -382,24 +385,65 @@ async def transfer(
succeeded.
"""

src_channel_id = prev_leg.backend.chain_transfer_channel_ids[leg.backend.chain_id]
denom_infos_on_dest = await denom_info_on_chain(
prev_leg.backend.chain_id,
denom,
leg.backend.chain_id,
ctx.http_session,
ctx.denom_map,
)

if not denom_infos_on_dest:
raise ValueError(
f"Missing denom info for transfer {denom} ({prev_leg.backend.chain_id}) -> {leg.backend.chain_id}"
)

ibc_route = await denom_route(
prev_leg.backend.chain_id,
denom,
leg.backend.chain_id,
denom_infos_on_dest[0].denom,
ctx.http_session,
)

if not ibc_route or len(ibc_route) == 0:
raise ValueError(f"No route from {denom} to {leg.backend.chain_id}")

src_channel_id = ibc_route[0].channel
sender_addr = str(
Address(ctx.wallet.public_key(), prefix=prev_leg.backend.chain_prefix)
Address(ctx.wallet.public_key(), prefix=ibc_route[0].from_chain.bech32_prefix)
)
receiver_addr = str(
Address(ctx.wallet.public_key(), prefix=leg.backend.chain_prefix)
Address(ctx.wallet.public_key(), prefix=ibc_route[0].to_chain.bech32_prefix)
)

memo: Optional[str] = None

for ibc_leg in reversed(ibc_route[1:]):
memo = json.dumps(
{
"forward": {
"receiver": "pfm",
"port": ibc_leg.port,
"channel": ibc_leg.channel,
"timeout": "10m",
"retries": 2,
"next": memo,
}
}
)

await transfer_raw(
denom,
prev_leg.backend.chain_id,
ibc_route[0].from_chain.chain_id,
prev_leg.backend.chain_fee_denom,
src_channel_id,
leg.backend.chain_id,
ibc_route[0].to_chain.chain_id,
sender_addr,
receiver_addr,
ctx,
swap_balance,
memo=memo,
)


Expand All @@ -413,6 +457,7 @@ async def transfer_raw(
receiver_addr: str,
ctx: Ctx[Any],
swap_balance: int,
memo: Optional[str] = None,
route: Optional[Route] = None,
) -> None:
"""
Expand All @@ -429,6 +474,7 @@ async def transfer_raw(
sender=sender_addr,
receiver=receiver_addr,
timeout_timestamp=time.time_ns() + 600 * 10**9,
memo=memo,
)
msg.token.CopyFrom(
coin_pb2.Coin( # pylint: disable=maybe-no-member
Expand Down
149 changes: 149 additions & 0 deletions src/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,48 @@ def int_to_decimal(n: int) -> Decimal:
return Decimal(n) / (Decimal(10) ** Decimal(18))


@dataclass
class ChainInfo:
"""
Contains basic information about a chain.
"""

chain_name: str
chain_id: str
pfm_enabled: bool
supports_memo: bool
bech32_prefix: str
fee_asset: str
chain_type: str
pretty_name: str


@dataclass
class DenomRouteLeg:
"""
Represents an IBC transfer as a leg of a greater IBC transfer
from some src denom on a src chain to a dest denom on a dest chain.
"""

# The origin and destination chains
src_chain: str
dest_chain: str

# The origin and destination denoms
src_denom: str
dest_denom: str

# The current leg in and out chain and denoms
from_chain: ChainInfo
to_chain: ChainInfo

denom_in: str
denom_out: str

port: str
channel: str


@dataclass
class DenomChainInfo:
"""
Expand Down Expand Up @@ -380,6 +422,113 @@ async def denom_info_on_chain(
return None


async def denom_route(
src_chain: str,
src_denom: str,
dest_chain: str,
dest_denom: str,
session: aiohttp.ClientSession,
denom_map: Optional[dict[str, list[dict[str, str]]]] = None,
) -> Optional[list[DenomRouteLeg]]:
"""
Gets a neutron denom's denom and channel on/to another chain.
"""

head = {"accept": "application/json", "content-type": "application/json"}

async with session.post(
"https://api.skip.money/v2/fungible/route",
headers=head,
json={
"amount_in": "1",
"source_asset_denom": src_denom,
"source_asset_chain_id": src_chain,
"dest_asset_denom": dest_denom,
"dest_asset_chain_id": dest_chain,
"allow_multi_tx": True,
"allow_unsafe": False,
"bridges": ["IBC"],
},
) as resp:
if resp.status != 200:
return None

ops = (await resp.json())["operations"]

# The transfer includes a swap or some other operation
# we can't handle
if any(("transfer" not in op for op in ops)):
return None

transfer_info = ops["transfer"]

from_chain_info = await chain_info(
transfer_info["from_chain_id"], session, denom_map
)
to_chain_info = await chain_info(
transfer_info["to_chain_id"], session, denom_map
)

if not from_chain_info or not to_chain_info:
return None

return [
DenomRouteLeg(
src_chain=src_chain,
dest_chain=dest_chain,
src_denom=src_denom,
dest_denom=dest_denom,
from_chain=from_chain_info,
to_chain=to_chain_info,
denom_in=transfer_info["denom_in"],
denom_out=transfer_info["denom_out"],
port=transfer_info["port"],
channel=transfer_info["channel"],
)
for op in ops
]


async def chain_info(
chain_id: str,
session: aiohttp.ClientSession,
denom_map: Optional[dict[str, list[dict[str, str]]]] = None,
) -> Optional[ChainInfo]:
"""
Gets basic information about a cosmos chain.
"""

head = {"accept": "application/json", "content-type": "application/json"}

async with session.get(
"https://api.skip.money/v2/info/chains",
headers=head,
json={
"chain_ids": [chain_id],
},
) as resp:
if resp.status != 200:
return None

chains = (await resp.json())["chains"]

if len(chains) == 0:
return None

chain = chains[0]

return ChainInfo(
chain_name=chain["chain_name"],
chain_id=chain["chain_id"],
pfm_enabled=chain["pfm_enabled"],
supports_memo=chain["supports_memo"],
bech32_prefix=chain["bech32_prefix"],
fee_asset=chain["fee_asset"],
chain_type=chain["chain_type"],
pretty_name=chain["pretty_name"],
)


@dataclass
class ContractInfo:
"""
Expand Down

0 comments on commit ba2b45e

Please sign in to comment.