Skip to content

Commit b633ee7

Browse files
authored
Merge pull request #8 from xdecentralix/erc4626
Erc4626
2 parents b2da3b1 + f86e4bb commit b633ee7

File tree

28 files changed

+1360
-257
lines changed

28 files changed

+1360
-257
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"abi": [
3+
{ "type": "function", "name": "asset", "inputs": [], "outputs": [{ "name": "", "type": "address" }], "stateMutability": "view" },
4+
{ "type": "function", "name": "totalAssets", "inputs": [], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
5+
6+
{ "type": "function", "name": "convertToAssets", "inputs": [{ "name": "shares", "type": "uint256" }], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
7+
{ "type": "function", "name": "convertToShares", "inputs": [{ "name": "assets", "type": "uint256" }], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
8+
9+
{ "type": "function", "name": "previewDeposit", "inputs": [{ "name": "assets", "type": "uint256" }], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
10+
{ "type": "function", "name": "previewMint", "inputs": [{ "name": "shares", "type": "uint256" }], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
11+
{ "type": "function", "name": "previewWithdraw", "inputs": [{ "name": "assets", "type": "uint256" }], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
12+
{ "type": "function", "name": "previewRedeem", "inputs": [{ "name": "shares", "type": "uint256" }], "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view" },
13+
14+
{ "type": "function", "name": "deposit", "inputs": [{ "name": "assets", "type": "uint256" }, { "name": "receiver", "type": "address" }], "outputs": [{ "name": "shares", "type": "uint256" }], "stateMutability": "nonpayable" },
15+
{ "type": "function", "name": "mint", "inputs": [{ "name": "shares", "type": "uint256" }, { "name": "receiver", "type": "address" }], "outputs": [{ "name": "assets", "type": "uint256" }], "stateMutability": "nonpayable" },
16+
{ "type": "function", "name": "withdraw", "inputs": [{ "name": "assets", "type": "uint256" }, { "name": "receiver", "type": "address" }, { "name": "owner", "type": "address" }], "outputs": [{ "name": "shares", "type": "uint256" }], "stateMutability": "nonpayable" },
17+
{ "type": "function", "name": "redeem", "inputs": [{ "name": "shares", "type": "uint256" }, { "name": "receiver", "type": "address" }, { "name": "owner", "type": "address" }], "outputs": [{ "name": "assets", "type": "uint256" }], "stateMutability": "nonpayable" }
18+
]
19+
}

crates/contracts/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,7 @@ fn main() {
16191619
.add_network_str(POLYGON, "0x61fFE014bA17989E743c5F6cB21bF9697530B21e")
16201620
// Not listed on Gnosis and Sepolia chains
16211621
});
1622+
generate_contract("IERC4626");
16221623
generate_contract_with_config("WETH9", |builder| {
16231624
// Note: the WETH address must be consistent with the one used by the ETH-flow
16241625
// contract

crates/contracts/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ include_contracts! {
104104
HooksTrampoline;
105105
IAavePool;
106106
IFlashLoanSolverWrapper;
107+
IERC4626;
107108
IRateProvider;
108109
ISwaprPair;
109110
IUniswapLikePair;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
use {
2+
crate::{
3+
boundary::{self, Result},
4+
domain::{eth, liquidity},
5+
infra::blockchain::Ethereum,
6+
},
7+
anyhow::Result as AnyResult,
8+
shared::sources::erc4626::registry::Erc4626Registry,
9+
solver::{
10+
liquidity::erc4626::{Erc4626LiquiditySource, Erc4626Order},
11+
liquidity_collector::{BackgroundInitLiquiditySource, LiquidityCollecting},
12+
},
13+
std::time::Duration,
14+
};
15+
16+
/// Builds the ERC4626 liquidity collector if enabled via
17+
/// configs/<chain>/erc4626.toml.
18+
pub async fn maybe_collector(eth: &Ethereum) -> AnyResult<Vec<Box<dyn LiquidityCollecting>>> {
19+
// Try to load per-chain config file; if missing or disabled, return empty.
20+
let chain = eth.chain();
21+
let chain_str = format!("{:?}", chain);
22+
let chain_lc = chain_str.to_lowercase();
23+
let primary_path = format!("configs/{}/erc4626.toml", chain_lc);
24+
let fallback_path = format!("../{}", primary_path);
25+
let web3 = boundary::web3(eth);
26+
let settlement = eth.contracts().settlement().clone();
27+
let registry: Erc4626Registry = match shared::sources::erc4626::registry::registry_from_file(
28+
std::path::Path::new(&primary_path),
29+
web3.clone(),
30+
) {
31+
Ok(reg) if reg.enabled() => {
32+
tracing::debug!(path = %primary_path, "Loaded ERC4626 registry from config file");
33+
reg
34+
}
35+
_ => {
36+
// Try fallback path when running from `services/` as CWD
37+
match shared::sources::erc4626::registry::registry_from_file(
38+
std::path::Path::new(&fallback_path),
39+
web3.clone(),
40+
) {
41+
Ok(reg) if reg.enabled() => {
42+
tracing::debug!(path = %fallback_path, "Loaded ERC4626 registry from fallback config file");
43+
reg
44+
}
45+
_ => {
46+
tracing::debug!(
47+
primary = %primary_path,
48+
fallback = %fallback_path,
49+
"ERC4626 registry disabled or config file not found; skipping source"
50+
);
51+
return Ok(vec![]);
52+
}
53+
}
54+
}
55+
};
56+
57+
let source = Erc4626LiquiditySource {
58+
web3,
59+
settlement,
60+
registry,
61+
};
62+
let init = move || {
63+
let source = source.clone();
64+
async move {
65+
tracing::debug!("initializing ERC4626 liquidity source");
66+
Ok(source)
67+
}
68+
};
69+
let collector =
70+
BackgroundInitLiquiditySource::new("erc4626", init, Duration::from_secs(5), None);
71+
Ok(vec![Box::new(collector)])
72+
}
73+
74+
pub fn to_domain(id: liquidity::Id, order: Erc4626Order) -> Result<liquidity::Liquidity> {
75+
// At this stage, amounts are populated during route realization; here we only
76+
// carry tokens and handler wiring
77+
let (a, b) = order.tokens.get();
78+
Ok(liquidity::Liquidity {
79+
id,
80+
gas: 90_000u64.into(),
81+
kind: liquidity::Kind::Erc4626(liquidity::erc4626::Edge {
82+
tokens: (eth::TokenAddress(a.into()), eth::TokenAddress(b.into())),
83+
}),
84+
})
85+
}
86+
87+
pub fn to_wrap_interaction(
88+
_input: &liquidity::MaxInput,
89+
output: &liquidity::ExactOutput,
90+
receiver: &eth::Address,
91+
) -> Result<eth::Interaction> {
92+
// encode IERC4626.mint(shares_out, receiver)
93+
let selector = hex_literal::hex!("94bf804d"); // mint(uint256,address)
94+
let mut shares = [0u8; 32];
95+
output.0.amount.0.to_big_endian(&mut shares);
96+
// Note: _input is intentionally not used here; it's used for bounded approval
97+
// generation elsewhere.
98+
tracing::debug!(
99+
shares_out = ?output.0.amount.0,
100+
receiver = ?receiver.0,
101+
target = ?output.0.token.0,
102+
"Encoding ERC4626 wrap interaction (mint)"
103+
);
104+
Ok(eth::Interaction {
105+
target: output.0.token.0.into(), // vault address as target
106+
value: eth::U256::zero().into(),
107+
call_data: [
108+
selector.as_slice(),
109+
&shares,
110+
[0; 12].as_slice(),
111+
receiver.0.as_bytes(),
112+
]
113+
.concat()
114+
.into(),
115+
})
116+
}
117+
118+
pub fn to_unwrap_interaction(
119+
_input: &liquidity::MaxInput,
120+
output: &liquidity::ExactOutput,
121+
receiver: &eth::Address,
122+
) -> Result<eth::Interaction> {
123+
// encode IERC4626.withdraw(assets_out, receiver, owner)
124+
let selector = hex_literal::hex!("b460af94"); // withdraw(uint256,address,address)
125+
let mut assets = [0u8; 32];
126+
output.0.amount.0.to_big_endian(&mut assets);
127+
tracing::debug!(
128+
assets_out = ?output.0.amount.0,
129+
receiver = ?receiver.0,
130+
target = ?output.0.token.0,
131+
"Encoding ERC4626 unwrap interaction (withdraw)"
132+
);
133+
Ok(eth::Interaction {
134+
target: output.0.token.0.into(), // vault or asset? For withdraw target is vault
135+
value: eth::U256::zero().into(),
136+
call_data: [
137+
selector.as_slice(),
138+
&assets,
139+
[0; 12].as_slice(),
140+
receiver.0.as_bytes(),
141+
[0; 12].as_slice(),
142+
receiver.0.as_bytes(),
143+
]
144+
.concat()
145+
.into(),
146+
})
147+
}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use {super::*, crate::domain::eth};
152+
153+
#[test]
154+
fn encode_wrap_and_unwrap() {
155+
let input = liquidity::MaxInput(eth::Asset {
156+
token: eth::H160::zero().into(),
157+
amount: 123.into(),
158+
});
159+
let output = liquidity::ExactOutput(eth::Asset {
160+
token: eth::H160::repeat_byte(0x11).into(),
161+
amount: 456.into(),
162+
});
163+
let receiver = &eth::Address(eth::H160::repeat_byte(0x22));
164+
165+
let wrap = to_wrap_interaction(&input, &output, receiver).unwrap();
166+
assert_eq!(&wrap.call_data.0[0..4], &hex_literal::hex!("94bf804d"));
167+
168+
let unwrap = to_unwrap_interaction(&input, &output, receiver).unwrap();
169+
assert_eq!(&unwrap.call_data.0[0..4], &hex_literal::hex!("b460af94"));
170+
}
171+
}

crates/driver/src/boundary/liquidity/mod.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use {
2727
};
2828

2929
pub mod balancer;
30+
pub mod erc4626;
3031
pub mod swapr;
3132
pub mod uniswap;
3233
pub mod zeroex;
@@ -126,13 +127,24 @@ impl Fetcher {
126127
.collect::<Vec<_>>(),
127128
);
128129

130+
// Optionally include ERC4626 liquidity source if configured
131+
let erc4626_sources = erc4626::maybe_collector(eth).await?;
132+
129133
Ok(Self {
130134
blocks: block_stream.clone(),
131135
inner: LiquidityCollector {
132-
liquidity_sources: [uni_v2, swapr, bal_v2, bal_v3, uni_v3, zeroex]
133-
.into_iter()
134-
.flatten()
135-
.collect(),
136+
liquidity_sources: [
137+
uni_v2,
138+
swapr,
139+
bal_v2,
140+
bal_v3,
141+
uni_v3,
142+
zeroex,
143+
erc4626_sources,
144+
]
145+
.into_iter()
146+
.flatten()
147+
.collect(),
136148
base_tokens: Arc::new(base_tokens),
137149
},
138150
swapr_routers,
@@ -184,6 +196,7 @@ impl Fetcher {
184196
Liquidity::BalancerV3GyroE(pool) => balancer::v3::gyro_e::to_domain(id, pool),
185197
Liquidity::LimitOrder(pool) => zeroex::to_domain(id, pool),
186198
Liquidity::Concentrated(pool) => uniswap::v3::to_domain(id, pool),
199+
Liquidity::Erc4626(order) => erc4626::to_domain(id, order),
187200
}
188201
// Ignore "bad" liquidity - this allows the driver to continue
189202
// solving with the other good stuff.

crates/driver/src/domain/competition/solution/encoding.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ pub fn tx(
182182
prices: auction.native_prices().clone(),
183183
};
184184

185+
// NOTE: ERC4626 bounded approvals are emitted via allowances() for Liquidity
186+
// interactions or as separate logic if needed.
187+
185188
// Add all interactions needed to move flash loaned tokens around
186189
// These interactions are executed before all other pre-interactions
187190
let flashloans = solution
@@ -417,6 +420,29 @@ pub fn liquidity_interaction(
417420
.swap(&input, &output, &settlement.address().into())
418421
.ok(),
419422
liquidity::Kind::ZeroEx(limit_order) => limit_order.to_interaction(&input).ok(),
423+
liquidity::Kind::Erc4626(edge) => {
424+
// Decide direction by tokens: input.0.token -> output.0.token
425+
let (sell, buy) = (input.0.token, output.0.token);
426+
if edge.tokens.0 == sell && edge.tokens.1 == buy {
427+
// Wrap: mint shares_out to settlement, with bounded approve emitted separately
428+
crate::boundary::liquidity::erc4626::to_wrap_interaction(
429+
&input,
430+
&output,
431+
&settlement.address().into(),
432+
)
433+
.ok()
434+
} else if edge.tokens.0 == buy && edge.tokens.1 == sell {
435+
// Unwrap: withdraw assets_out to settlement
436+
crate::boundary::liquidity::erc4626::to_unwrap_interaction(
437+
&input,
438+
&output,
439+
&settlement.address().into(),
440+
)
441+
.ok()
442+
} else {
443+
None
444+
}
445+
}
420446
}
421447
.ok_or(Error::InvalidInteractionExecution(Box::new(
422448
liquidity.clone(),
@@ -615,4 +641,87 @@ mod test {
615641
)
616642
);
617643
}
644+
645+
#[test]
646+
fn erc4626_wrap_emits_bounded_approval_before_mint() {
647+
// Build a minimal solution with a single ERC4626 wrap liquidity interaction
648+
use crate::domain::{
649+
competition::solution::{
650+
Interaction,
651+
interaction::Liquidity as InteractionLiquidity,
652+
slippage,
653+
},
654+
eth,
655+
liquidity as dl,
656+
};
657+
let asset = eth::H160::from_low_u64_be(1);
658+
let vault = eth::H160::from_low_u64_be(2);
659+
let settlement =
660+
contracts::dummy_contract!(contracts::GPv2Settlement, eth::H160::from_low_u64_be(3));
661+
let liquidity = dl::Liquidity {
662+
id: dl::Id(0),
663+
gas: eth::Gas(90_000.into()),
664+
kind: dl::Kind::Erc4626(dl::erc4626::Edge {
665+
tokens: (asset.into(), vault.into()),
666+
}),
667+
};
668+
let liq_interaction = Interaction::Liquidity(InteractionLiquidity {
669+
liquidity: liquidity.clone(),
670+
input: eth::Asset {
671+
token: asset.into(),
672+
amount: 100.into(),
673+
},
674+
output: eth::Asset {
675+
token: vault.into(),
676+
amount: 95.into(),
677+
},
678+
internalize: false,
679+
});
680+
681+
// Note: we validate via direct allowances() and interaction selector; no need
682+
// to build full Solution here.
683+
684+
// Encode to interactions list
685+
let interaction = liquidity_interaction(
686+
&InteractionLiquidity {
687+
liquidity: liquidity.clone(),
688+
input: eth::Asset {
689+
token: asset.into(),
690+
amount: 100.into(),
691+
},
692+
output: eth::Asset {
693+
token: vault.into(),
694+
amount: 95.into(),
695+
},
696+
internalize: false,
697+
},
698+
&slippage::Parameters {
699+
relative: num::rational::Ratio::from_integer(0.into()),
700+
max: None,
701+
min: None,
702+
prices: Default::default(),
703+
},
704+
&settlement,
705+
)
706+
.unwrap();
707+
708+
// Ensure allowance requires bounded amount
709+
let allowances = Interaction::Liquidity(InteractionLiquidity {
710+
liquidity,
711+
input: eth::Asset {
712+
token: asset.into(),
713+
amount: 100.into(),
714+
},
715+
output: eth::Asset {
716+
token: vault.into(),
717+
amount: 95.into(),
718+
},
719+
internalize: false,
720+
})
721+
.allowances();
722+
assert_eq!(allowances.len(), 1);
723+
assert_eq!(allowances[0].0.amount, 100.into());
724+
// Ensure interaction is a mint (selector 0x94bf804d)
725+
assert_eq!(&interaction.call_data.0[0..4], &hex!("94bf804d"));
726+
}
618727
}

0 commit comments

Comments
 (0)