Skip to content

Commit 31d5f4f

Browse files
committed
feat: add uniswap v3 as a native router venue
PoolType::UniswapV3 executor (QuoterV2 quotes, SwapRouter02 swaps), governable addresses in pallet-parameters, UniswapV3Api runtime API, and a lark deploy + mixed-swap validation CLI.
1 parent fa69631 commit 31d5f4f

37 files changed

Lines changed: 4913 additions & 13 deletions

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ mod stableswap;
4747
mod stableswap_curve_comparison;
4848
mod staking;
4949
mod transact_call_filter;
50+
mod uniswap_v3_router;
5051
mod utility;
5152
pub mod utils;
5253
mod vesting;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#![cfg(test)]
2+
3+
use crate::polkadot_test_net::*;
4+
use frame_support::assert_ok;
5+
use hex_literal::hex;
6+
use hydradx_runtime::evm::uniswap_v3_trade_executor::UniswapV3;
7+
use hydradx_runtime::{AssetId, Currencies, Parameters, Router, Runtime, RuntimeEvent, RuntimeOrigin};
8+
use hydradx_traits::router::{PoolType, Trade};
9+
use orml_traits::MultiCurrency;
10+
use pallet_broadcast::types::Filler;
11+
use pallet_route_executor::TradeExecution;
12+
use primitives::{Balance, EvmAddress};
13+
use sp_core::H160;
14+
15+
pub const PATH_TO_SNAPSHOT: &str = "uniswap-snapshot/SNAPSHOT";
16+
17+
const UNISWAP_V3_FACTORY: EvmAddress = H160(hex!("0000000000000000000000000000000000000000"));
18+
const UNISWAP_V3_SWAP_ROUTER: EvmAddress = H160(hex!("0000000000000000000000000000000000000000"));
19+
const UNISWAP_V3_QUOTER: EvmAddress = H160(hex!("0000000000000000000000000000000000000000"));
20+
21+
const ASSET_IN: AssetId = 0;
22+
const ASSET_OUT: AssetId = 20;
23+
const FEE_TIER: u32 = 3000;
24+
const SELL_AMOUNT: Balance = 1_000_000_000_000;
25+
26+
fn with_uniswap_v3(execution: impl FnOnce()) {
27+
TestNet::reset();
28+
hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| {
29+
assert_ok!(Parameters::set_uniswap_v3_addresses(
30+
RuntimeOrigin::root(),
31+
UNISWAP_V3_FACTORY,
32+
UNISWAP_V3_SWAP_ROUTER,
33+
UNISWAP_V3_QUOTER,
34+
));
35+
execution();
36+
});
37+
}
38+
39+
fn uniswap_route() -> Vec<Trade<AssetId>> {
40+
vec![Trade {
41+
pool: PoolType::UniswapV3(FEE_TIER),
42+
asset_in: ASSET_IN,
43+
asset_out: ASSET_OUT,
44+
}]
45+
}
46+
47+
#[test]
48+
#[ignore = "requires uniswap-snapshot/SNAPSHOT and deployment constants; see uniswap-snapshot/README.md"]
49+
fn calculate_out_given_in_should_return_positive_quote_when_pool_has_liquidity() {
50+
with_uniswap_v3(|| {
51+
let amount_out =
52+
UniswapV3::calculate_out_given_in(PoolType::UniswapV3(FEE_TIER), ASSET_IN, ASSET_OUT, SELL_AMOUNT)
53+
.expect("quote should succeed");
54+
assert!(amount_out > 0);
55+
});
56+
}
57+
58+
#[test]
59+
#[ignore = "requires uniswap-snapshot/SNAPSHOT and deployment constants; see uniswap-snapshot/README.md"]
60+
fn calculate_in_given_out_should_exceed_output_when_pool_charges_fee() {
61+
with_uniswap_v3(|| {
62+
let amount_out = SELL_AMOUNT / 2;
63+
let amount_in =
64+
UniswapV3::calculate_in_given_out(PoolType::UniswapV3(FEE_TIER), ASSET_IN, ASSET_OUT, amount_out)
65+
.expect("quote should succeed");
66+
assert!(amount_in > amount_out);
67+
});
68+
}
69+
70+
#[test]
71+
#[ignore = "requires uniswap-snapshot/SNAPSHOT and deployment constants; see uniswap-snapshot/README.md"]
72+
fn router_sell_should_increase_output_balance_when_routed_through_uniswap_v3() {
73+
with_uniswap_v3(|| {
74+
let before = Currencies::free_balance(ASSET_OUT, &ALICE.into());
75+
assert_ok!(Router::sell(
76+
RuntimeOrigin::signed(ALICE.into()),
77+
ASSET_IN,
78+
ASSET_OUT,
79+
SELL_AMOUNT,
80+
0,
81+
uniswap_route().try_into().unwrap(),
82+
));
83+
let after = Currencies::free_balance(ASSET_OUT, &ALICE.into());
84+
assert!(after > before);
85+
});
86+
}
87+
88+
#[test]
89+
#[ignore = "requires uniswap-snapshot/SNAPSHOT and deployment constants; see uniswap-snapshot/README.md"]
90+
fn router_buy_should_deliver_exact_output_when_routed_through_uniswap_v3() {
91+
with_uniswap_v3(|| {
92+
let buy_amount = SELL_AMOUNT / 2;
93+
let before = Currencies::free_balance(ASSET_OUT, &ALICE.into());
94+
assert_ok!(Router::buy(
95+
RuntimeOrigin::signed(ALICE.into()),
96+
ASSET_IN,
97+
ASSET_OUT,
98+
buy_amount,
99+
u128::MAX,
100+
uniswap_route().try_into().unwrap(),
101+
));
102+
let after = Currencies::free_balance(ASSET_OUT, &ALICE.into());
103+
assert_eq!(after - before, buy_amount);
104+
});
105+
}
106+
107+
#[test]
108+
#[ignore = "requires uniswap-snapshot/SNAPSHOT and deployment constants; see uniswap-snapshot/README.md"]
109+
fn router_sell_should_emit_uniswap_v3_filler_event() {
110+
with_uniswap_v3(|| {
111+
assert_ok!(Router::sell(
112+
RuntimeOrigin::signed(ALICE.into()),
113+
ASSET_IN,
114+
ASSET_OUT,
115+
SELL_AMOUNT,
116+
0,
117+
uniswap_route().try_into().unwrap(),
118+
));
119+
let emitted = frame_system::Pallet::<Runtime>::events().into_iter().any(|record| {
120+
matches!(
121+
record.event,
122+
RuntimeEvent::Broadcast(pallet_broadcast::Event::Swapped3 {
123+
filler_type: Filler::UniswapV3,
124+
..
125+
})
126+
)
127+
});
128+
assert!(emitted);
129+
});
130+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# uniswap-snapshot
2+
3+
Snapshot-backed integration tests for the Uniswap v3 router venue
4+
(`src/uniswap_v3_router.rs`). The tests load a scraped EVM state snapshot that
5+
already contains a deployed Uniswap v3 stack (Factory, SwapRouter02, QuoterV2,
6+
a pool with liquidity), then drive the runtime router against it — the same
7+
pattern as `aave_router.rs` / `evm-snapshot/`.
8+
9+
The tests are `#[ignore]`d until the `SNAPSHOT` artifact and the deployment
10+
constants are committed (the binary is too large to ship by default, and the
11+
addresses depend on the deployment used). Follow the steps below to produce them.
12+
13+
## 1. Bring up a chain with Uniswap v3 deployed
14+
15+
Use the local zombienet + Uniswap v3 deploy from the `ys-` branches (the
16+
`uniswap-v3-deploy` sibling repo). The chain must run **this** runtime
17+
(`feat/uniswap-v3-router`) so the snapshot is loadable, and must have:
18+
19+
- Factory, SwapRouter02, QuoterV2 deployed.
20+
- At least one pool created **and initialized** for an asset pair that maps to
21+
two registered Hydration assets, with liquidity minted (via the
22+
NonfungiblePositionManager) so quotes and swaps return non-zero.
23+
- `ALICE` funded with `ASSET_IN`.
24+
25+
Record the deployed addresses (the deploy writes them to
26+
`uniswap-v3-deploy/deployments/<network>/_addresses.json`).
27+
28+
## 2. Build the scraper
29+
30+
```bash
31+
cargo build --release -p scraper
32+
```
33+
34+
## 3. Scrape the EVM + registry + balances into a snapshot
35+
36+
```bash
37+
./target/release/scraper save-storage \
38+
--pallet EVM AssetRegistry System Tokens Omnipool Timestamp Parameters \
39+
--uri ws://127.0.0.1:9988 \
40+
--path integration-tests/uniswap-snapshot
41+
```
42+
43+
This writes `integration-tests/uniswap-snapshot/SNAPSHOT_<block>`. Rename it to
44+
`SNAPSHOT` (or update `PATH_TO_SNAPSHOT` in `src/uniswap_v3_router.rs`). Add
45+
`--slim` to drop most user accounts and keep the file small.
46+
47+
> Including the `Parameters` pallet means that if you call
48+
> `parameters.setUniswapV3Addresses` on the source chain **before** scraping,
49+
> the addresses are baked into the snapshot and `with_uniswap_v3` doesn't need
50+
> to set them — in that case the `UNISWAP_V3_*` constants below are only used as
51+
> a fallback.
52+
53+
## 4. Fill in the deployment constants
54+
55+
In `src/uniswap_v3_router.rs` set, from the deploy's `_addresses.json` and your
56+
asset registry:
57+
58+
- `UNISWAP_V3_FACTORY`, `UNISWAP_V3_SWAP_ROUTER`, `UNISWAP_V3_QUOTER`
59+
- `ASSET_IN`, `ASSET_OUT`, `FEE_TIER` — the pair + fee tier of the seeded pool
60+
- `SELL_AMOUNT` — comfortably below the pool's depth
61+
62+
## 5. Run the tests
63+
64+
```bash
65+
cargo test -p runtime-integration-tests uniswap_v3_router -- --ignored
66+
```
67+
68+
Drop the `#[ignore]` attributes once `SNAPSHOT` and the constants are committed.

pallets/broadcast/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pallet-broadcast"
3-
version = "1.7.0"
3+
version = "1.8.0"
44
authors = ["GalacticCouncil"]
55
edition = "2021"
66
license = "Apache 2.0"

pallets/broadcast/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub enum Filler {
1818
OTC(OtcOrderId),
1919
AAVE, // ICE(solution_id/block id), swapper: alice, filler: solver
2020
HSM,
21+
UniswapV3,
2122
}
2223

2324
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Copy, Debug, Eq, PartialEq, TypeInfo, MaxEncodedLen)]

pallets/parameters/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pallet-parameters"
3-
version = "1.2.0"
3+
version = "1.3.0"
44
authors = ['GalacticCouncil']
55
edition = "2021"
66
license = "Apache-2.0"

pallets/parameters/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ mod tests;
3737
pub use pallet::*;
3838

3939
use frame_support::pallet_prelude::*;
40+
use frame_system::pallet_prelude::OriginFor;
41+
use sp_core::H160;
4042

4143
#[frame_support::pallet]
4244
pub mod pallet {
@@ -56,6 +58,18 @@ pub mod pallet {
5658
#[pallet::getter(fn relay_parent_offset_override)]
5759
pub type RelayParentOffsetOverride<T> = StorageValue<_, bool, ValueQuery>;
5860

61+
#[pallet::storage]
62+
#[pallet::getter(fn uniswap_v3_factory)]
63+
pub type UniswapV3Factory<T> = StorageValue<_, H160, OptionQuery>;
64+
65+
#[pallet::storage]
66+
#[pallet::getter(fn uniswap_v3_swap_router)]
67+
pub type UniswapV3SwapRouter<T> = StorageValue<_, H160, OptionQuery>;
68+
69+
#[pallet::storage]
70+
#[pallet::getter(fn uniswap_v3_quoter)]
71+
pub type UniswapV3Quoter<T> = StorageValue<_, H160, OptionQuery>;
72+
5973
#[pallet::genesis_config]
6074
pub struct GenesisConfig<T: Config> {
6175
pub is_testnet: bool,
@@ -81,6 +95,24 @@ pub mod pallet {
8195
}
8296
}
8397

98+
#[pallet::call]
99+
impl<T: Config> Pallet<T> {
100+
#[pallet::call_index(0)]
101+
#[pallet::weight(T::DbWeight::get().writes(3))]
102+
pub fn set_uniswap_v3_addresses(
103+
origin: OriginFor<T>,
104+
factory: H160,
105+
swap_router: H160,
106+
quoter: H160,
107+
) -> DispatchResult {
108+
frame_system::ensure_root(origin)?;
109+
UniswapV3Factory::<T>::put(factory);
110+
UniswapV3SwapRouter::<T>::put(swap_router);
111+
UniswapV3Quoter::<T>::put(quoter);
112+
Ok(())
113+
}
114+
}
115+
84116
impl<T: Config> Pallet<T> {
85117
/// Set the flag. Only used for tests.
86118
#[cfg(feature = "std")]

runtime/hydradx/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hydradx-runtime"
3-
version = "428.0.0"
3+
version = "429.0.0"
44
authors = ["GalacticCouncil"]
55
edition = "2021"
66
license = "Apache 2.0"

0 commit comments

Comments
 (0)