1#![cfg_attr(not(feature = "std"), no_std)]
27
28extern crate alloc;
29
30use alloc::vec::Vec;
31use codec::Decode;
32use core::marker::PhantomData;
33use frame_support::traits::Get;
34use pallet_asset_conversion::{
35 weights::WeightInfo as _, AddLiquidityAsset, MutateLiquidity, QuotePrice, Swap,
36};
37use pallet_revive::precompiles::{
38 alloy::{
39 self,
40 sol_types::{Revert, SolCall},
41 },
42 AddressMatcher, Error, Ext, Precompile, H160,
43};
44
45#[cfg(test)]
46mod mock;
47#[cfg(test)]
48mod tests;
49
50alloy::sol! {
51 interface IAssetConversion {
57 function swapExactTokensForTokens(
65 bytes[] calldata path,
66 uint256 amountIn,
67 uint256 amountOutMin,
68 address sendTo,
69 bool keepAlive
70 ) external returns (uint256 amountOut);
71
72 function swapTokensForExactTokens(
80 bytes[] calldata path,
81 uint256 amountOut,
82 uint256 amountInMax,
83 address sendTo,
84 bool keepAlive
85 ) external returns (uint256 amountIn);
86
87 function quoteExactTokensForTokens(
94 bytes calldata asset1,
95 bytes calldata asset2,
96 uint256 amount,
97 bool includeFee
98 ) external view returns (uint256);
99
100 function quoteTokensForExactTokens(
107 bytes calldata asset1,
108 bytes calldata asset2,
109 uint256 amount,
110 bool includeFee
111 ) external view returns (uint256);
112
113 function createPool(
117 bytes calldata asset1,
118 bytes calldata asset2
119 ) external;
120
121 function addLiquidity(
131 bytes calldata asset1,
132 bytes calldata asset2,
133 uint256 amount1Desired,
134 uint256 amount2Desired,
135 uint256 amount1Min,
136 uint256 amount2Min,
137 address mintTo
138 ) external returns (uint256 lpTokensMinted);
139
140 function removeLiquidity(
150 bytes calldata asset1,
151 bytes calldata asset2,
152 uint256 lpTokenBurn,
153 uint256 amount1MinReceive,
154 uint256 amount2MinReceive,
155 address withdrawTo
156 ) external returns (uint256 amount1, uint256 amount2);
157
158 function getReserves(
164 bytes calldata asset1,
165 bytes calldata asset2
166 ) external view returns (uint256 reserve1, uint256 reserve2);
167 }
168}
169
170pub struct AssetConversion<const ADDRESS: u16, Runtime> {
174 _phantom: PhantomData<Runtime>,
175}
176
177impl<const ADDRESS: u16, Runtime> Precompile for AssetConversion<ADDRESS, Runtime>
178where
179 Runtime: pallet_asset_conversion::Config + pallet_revive::Config,
180 alloy::primitives::U256: TryInto<<Runtime as pallet_asset_conversion::Config>::Balance>,
181 alloy::primitives::U256: TryFrom<<Runtime as pallet_asset_conversion::Config>::Balance>,
182{
183 type T = Runtime;
184 type Interface = IAssetConversion::IAssetConversionCalls;
185 const MATCHER: AddressMatcher =
186 AddressMatcher::Fixed(core::num::NonZero::new(ADDRESS).unwrap());
187 const HAS_CONTRACT_INFO: bool = false;
188
189 fn call(
190 _address: &[u8; 20],
191 input: &Self::Interface,
192 env: &mut impl Ext<T = Self::T>,
193 ) -> Result<Vec<u8>, Error> {
194 use IAssetConversion::IAssetConversionCalls;
195
196 frame_support::ensure!(
197 !env.is_delegate_call(),
198 pallet_revive::Error::<Self::T>::PrecompileDelegateDenied,
199 );
200
201 match input {
202 IAssetConversionCalls::swapExactTokensForTokens(_) |
203 IAssetConversionCalls::swapTokensForExactTokens(_) |
204 IAssetConversionCalls::createPool(_) |
205 IAssetConversionCalls::addLiquidity(_) |
206 IAssetConversionCalls::removeLiquidity(_)
207 if env.is_read_only() =>
208 {
209 Err(Error::Error(pallet_revive::Error::<Self::T>::StateChangeDenied.into()))
210 },
211 IAssetConversionCalls::swapExactTokensForTokens(call) => {
212 Self::swap_exact_tokens_for_tokens(call, env)
213 },
214 IAssetConversionCalls::swapTokensForExactTokens(call) => {
215 Self::swap_tokens_for_exact_tokens(call, env)
216 },
217 IAssetConversionCalls::quoteExactTokensForTokens(call) => {
218 Self::quote_exact_tokens_for_tokens(call, env)
219 },
220 IAssetConversionCalls::quoteTokensForExactTokens(call) => {
221 Self::quote_tokens_for_exact_tokens(call, env)
222 },
223 IAssetConversionCalls::createPool(call) => Self::create_pool(call, env),
224 IAssetConversionCalls::addLiquidity(call) => Self::add_liquidity(call, env),
225 IAssetConversionCalls::removeLiquidity(call) => Self::remove_liquidity(call, env),
226 IAssetConversionCalls::getReserves(call) => Self::get_reserves(call, env),
227 }
228 }
229}
230
231const ERR_INVALID_CALLER: &str = "Invalid caller";
232const ERR_BALANCE_CONVERSION_FAILED: &str = "Balance conversion failed";
233const ERR_INVALID_ASSET_PAIR: &str = "Invalid asset pair";
234const ERR_POOL_NOT_FOUND: &str = "Pool does not exist or has no liquidity";
235const ERR_POOL_EMPTY: &str = "Pool exists but has no liquidity";
236const ERR_POOL_UNAVAILABLE: &str = "Pool is frozen or blocked";
237const ERR_UNEXPECTED: &str = "Unexpected error";
238const ERR_PATH_TOO_LONG: &str = "Swap path exceeds MaxSwapPathLength";
239const ERR_INVALID_ASSET_ENCODING: &str = "Failed to SCALE-decode asset kind";
240
241impl<const ADDRESS: u16, Runtime> AssetConversion<ADDRESS, Runtime>
242where
243 Runtime: pallet_asset_conversion::Config + pallet_revive::Config,
244 alloy::primitives::U256: TryInto<<Runtime as pallet_asset_conversion::Config>::Balance>,
245 alloy::primitives::U256: TryFrom<<Runtime as pallet_asset_conversion::Config>::Balance>,
246{
247 fn caller_account_id(
249 env: &impl Ext<T = Runtime>,
250 ) -> Result<<Runtime as frame_system::Config>::AccountId, Error> {
251 env.caller()
252 .account_id()
253 .map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
254 .cloned()
255 }
256
257 fn decode_asset_kind(
259 data: &[u8],
260 ) -> Result<<Runtime as pallet_asset_conversion::Config>::AssetKind, Error> {
261 <Runtime as pallet_asset_conversion::Config>::AssetKind::decode(&mut &data[..])
262 .map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_ASSET_ENCODING.into() }))
263 }
264
265 fn validated_path_len<T>(path: &[T]) -> Result<u32, Error> {
267 let len = path.len() as u32;
268 let max = <Runtime as pallet_asset_conversion::Config>::MaxSwapPathLength::get();
269 if len > max {
270 return Err(Error::Revert(Revert { reason: ERR_PATH_TOO_LONG.into() }));
271 }
272 Ok(len)
273 }
274
275 fn to_balance(
276 value: alloy::primitives::U256,
277 ) -> Result<<Runtime as pallet_asset_conversion::Config>::Balance, Error> {
278 value
279 .try_into()
280 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
281 }
282
283 fn to_u256(
284 value: <Runtime as pallet_asset_conversion::Config>::Balance,
285 ) -> Result<alloy::primitives::U256, Error> {
286 alloy::primitives::U256::try_from(value)
287 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
288 }
289
290 fn swap_exact_tokens_for_tokens(
291 call: &IAssetConversion::swapExactTokensForTokensCall,
292 env: &mut impl Ext<T = Runtime>,
293 ) -> Result<Vec<u8>, Error> {
294 let path_len = Self::validated_path_len(&call.path)?;
295 env.charge(
296 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_exact_tokens_for_tokens(
297 path_len,
298 ),
299 )?;
300 let path: Vec<_> =
301 call.path.iter().map(|e| Self::decode_asset_kind(e)).collect::<Result<_, _>>()?;
302
303 let sender = Self::caller_account_id(env)?;
304 let send_to = env.to_account_id(&H160(call.sendTo.0 .0));
305
306 let amount_out = <pallet_asset_conversion::Pallet<Runtime> as Swap<
307 <Runtime as frame_system::Config>::AccountId,
308 >>::swap_exact_tokens_for_tokens(
309 sender,
310 path,
311 Self::to_balance(call.amountIn)?,
312 Some(Self::to_balance(call.amountOutMin)?),
313 send_to,
314 call.keepAlive,
315 )?;
316
317 Ok(IAssetConversion::swapExactTokensForTokensCall::abi_encode_returns(&Self::to_u256(
318 amount_out,
319 )?))
320 }
321
322 fn swap_tokens_for_exact_tokens(
323 call: &IAssetConversion::swapTokensForExactTokensCall,
324 env: &mut impl Ext<T = Runtime>,
325 ) -> Result<Vec<u8>, Error> {
326 let path_len = Self::validated_path_len(&call.path)?;
327 env.charge(
328 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_tokens_for_exact_tokens(
329 path_len,
330 ),
331 )?;
332 let path: Vec<_> =
333 call.path.iter().map(|e| Self::decode_asset_kind(e)).collect::<Result<_, _>>()?;
334
335 let sender = Self::caller_account_id(env)?;
336 let send_to = env.to_account_id(&H160(call.sendTo.0 .0));
337
338 let amount_in = <pallet_asset_conversion::Pallet<Runtime> as Swap<
339 <Runtime as frame_system::Config>::AccountId,
340 >>::swap_tokens_for_exact_tokens(
341 sender,
342 path,
343 Self::to_balance(call.amountOut)?,
344 Some(Self::to_balance(call.amountInMax)?),
345 send_to,
346 call.keepAlive,
347 )?;
348
349 Ok(IAssetConversion::swapTokensForExactTokensCall::abi_encode_returns(&Self::to_u256(
350 amount_in,
351 )?))
352 }
353
354 fn quote_exact_tokens_for_tokens(
355 call: &IAssetConversion::quoteExactTokensForTokensCall,
356 env: &mut impl Ext<T = Runtime>,
357 ) -> Result<Vec<u8>, Error> {
358 env.charge(
363 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_exact_tokens_for_tokens(
364 2,
365 ),
366 )?;
367
368 let asset1 = Self::decode_asset_kind(&call.asset1)?;
369 let asset2 = Self::decode_asset_kind(&call.asset2)?;
370
371 let quoted =
372 <pallet_asset_conversion::Pallet<Runtime> as QuotePrice>::quote_price_exact_tokens_for_tokens(
373 asset1,
374 asset2,
375 Self::to_balance(call.amount)?,
376 call.includeFee,
377 )
378 .ok_or(Error::Revert(Revert { reason: ERR_POOL_NOT_FOUND.into() }))?;
379
380 Ok(IAssetConversion::quoteExactTokensForTokensCall::abi_encode_returns(&Self::to_u256(
381 quoted,
382 )?))
383 }
384
385 fn quote_tokens_for_exact_tokens(
386 call: &IAssetConversion::quoteTokensForExactTokensCall,
387 env: &mut impl Ext<T = Runtime>,
388 ) -> Result<Vec<u8>, Error> {
389 env.charge(
391 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_tokens_for_exact_tokens(
392 2,
393 ),
394 )?;
395
396 let asset1 = Self::decode_asset_kind(&call.asset1)?;
397 let asset2 = Self::decode_asset_kind(&call.asset2)?;
398
399 let quoted =
400 <pallet_asset_conversion::Pallet<Runtime> as QuotePrice>::quote_price_tokens_for_exact_tokens(
401 asset1,
402 asset2,
403 Self::to_balance(call.amount)?,
404 call.includeFee,
405 )
406 .ok_or(Error::Revert(Revert { reason: ERR_POOL_NOT_FOUND.into() }))?;
407
408 Ok(IAssetConversion::quoteTokensForExactTokensCall::abi_encode_returns(&Self::to_u256(
409 quoted,
410 )?))
411 }
412
413 fn create_pool(
414 call: &IAssetConversion::createPoolCall,
415 env: &mut impl Ext<T = Runtime>,
416 ) -> Result<Vec<u8>, Error> {
417 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::create_pool())?;
418
419 let asset1 = Self::decode_asset_kind(&call.asset1)?;
420 let asset2 = Self::decode_asset_kind(&call.asset2)?;
421
422 let sender = Self::caller_account_id(env)?;
423
424 <pallet_asset_conversion::Pallet<Runtime> as MutateLiquidity<
425 <Runtime as frame_system::Config>::AccountId,
426 >>::create_pool(&sender, asset1, asset2)?;
427
428 Ok(Vec::new())
429 }
430
431 fn add_liquidity(
432 call: &IAssetConversion::addLiquidityCall,
433 env: &mut impl Ext<T = Runtime>,
434 ) -> Result<Vec<u8>, Error> {
435 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::add_liquidity())?;
436
437 let asset1 = Self::decode_asset_kind(&call.asset1)?;
438 let asset2 = Self::decode_asset_kind(&call.asset2)?;
439
440 let sender = Self::caller_account_id(env)?;
441 let mint_to = env.to_account_id(&H160(call.mintTo.0 .0));
442
443 let lp_tokens = <pallet_asset_conversion::Pallet<Runtime> as MutateLiquidity<
444 <Runtime as frame_system::Config>::AccountId,
445 >>::add_liquidity(
446 &sender,
447 AddLiquidityAsset {
448 asset: asset1,
449 amount_desired: Self::to_balance(call.amount1Desired)?,
450 amount_min: Self::to_balance(call.amount1Min)?,
451 },
452 AddLiquidityAsset {
453 asset: asset2,
454 amount_desired: Self::to_balance(call.amount2Desired)?,
455 amount_min: Self::to_balance(call.amount2Min)?,
456 },
457 &mint_to,
458 )?;
459
460 Ok(IAssetConversion::addLiquidityCall::abi_encode_returns(&Self::to_u256(lp_tokens)?))
461 }
462
463 fn remove_liquidity(
464 call: &IAssetConversion::removeLiquidityCall,
465 env: &mut impl Ext<T = Runtime>,
466 ) -> Result<Vec<u8>, Error> {
467 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::remove_liquidity())?;
468
469 let asset1 = Self::decode_asset_kind(&call.asset1)?;
470 let asset2 = Self::decode_asset_kind(&call.asset2)?;
471
472 let sender = Self::caller_account_id(env)?;
473 let withdraw_to = env.to_account_id(&H160(call.withdrawTo.0 .0));
474
475 let (amount1, amount2) = <pallet_asset_conversion::Pallet<Runtime> as MutateLiquidity<
476 <Runtime as frame_system::Config>::AccountId,
477 >>::remove_liquidity(
478 &sender,
479 asset1,
480 asset2,
481 Self::to_balance(call.lpTokenBurn)?,
482 Self::to_balance(call.amount1MinReceive)?,
483 Self::to_balance(call.amount2MinReceive)?,
484 &withdraw_to,
485 )?;
486
487 Ok(IAssetConversion::removeLiquidityCall::abi_encode_returns(
488 &IAssetConversion::removeLiquidityReturn {
489 amount1: Self::to_u256(amount1)?,
490 amount2: Self::to_u256(amount2)?,
491 },
492 ))
493 }
494
495 fn get_reserves(
496 call: &IAssetConversion::getReservesCall,
497 env: &mut impl Ext<T = Runtime>,
498 ) -> Result<Vec<u8>, Error> {
499 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::get_reserves())?;
500
501 let asset1 = Self::decode_asset_kind(&call.asset1)?;
502 let asset2 = Self::decode_asset_kind(&call.asset2)?;
503
504 let (reserve1, reserve2) = pallet_asset_conversion::Pallet::<Runtime>::get_reserves(
505 asset1, asset2,
506 )
507 .map_err(|e| match e {
508 pallet_asset_conversion::Error::InvalidAssetPair => {
509 Error::Revert(Revert { reason: ERR_INVALID_ASSET_PAIR.into() })
510 },
511 pallet_asset_conversion::Error::PoolEmpty => {
512 Error::Revert(Revert { reason: ERR_POOL_EMPTY.into() })
513 },
514 pallet_asset_conversion::Error::PoolUnavailable => {
515 Error::Revert(Revert { reason: ERR_POOL_UNAVAILABLE.into() })
516 },
517 pallet_asset_conversion::Error::PoolExists |
520 pallet_asset_conversion::Error::WrongDesiredAmount |
521 pallet_asset_conversion::Error::AmountOneLessThanMinimal |
522 pallet_asset_conversion::Error::AmountTwoLessThanMinimal |
523 pallet_asset_conversion::Error::ReserveLeftLessThanMinimal |
524 pallet_asset_conversion::Error::AmountOutTooHigh |
525 pallet_asset_conversion::Error::PoolNotFound |
526 pallet_asset_conversion::Error::Overflow |
527 pallet_asset_conversion::Error::AssetOneDepositDidNotMeetMinimum |
528 pallet_asset_conversion::Error::AssetTwoDepositDidNotMeetMinimum |
529 pallet_asset_conversion::Error::AssetOneWithdrawalDidNotMeetMinimum |
530 pallet_asset_conversion::Error::AssetTwoWithdrawalDidNotMeetMinimum |
531 pallet_asset_conversion::Error::OptimalAmountLessThanDesired |
532 pallet_asset_conversion::Error::InsufficientLiquidityMinted |
533 pallet_asset_conversion::Error::ZeroLiquidity |
534 pallet_asset_conversion::Error::ZeroAmount |
535 pallet_asset_conversion::Error::ProvidedMinimumNotSufficientForSwap |
536 pallet_asset_conversion::Error::ProvidedMaximumNotSufficientForSwap |
537 pallet_asset_conversion::Error::InvalidPath |
538 pallet_asset_conversion::Error::NonUniquePath |
539 pallet_asset_conversion::Error::IncorrectPoolAssetId |
540 pallet_asset_conversion::Error::BelowMinimum |
541 pallet_asset_conversion::Error::FeeTooHigh => {
542 frame_support::defensive!("get_reserves returned unexpected error");
543 Error::Revert(Revert { reason: ERR_UNEXPECTED.into() })
544 },
545 })?;
546
547 Ok(IAssetConversion::getReservesCall::abi_encode_returns(
548 &IAssetConversion::getReservesReturn {
549 reserve1: Self::to_u256(reserve1)?,
550 reserve2: Self::to_u256(reserve2)?,
551 },
552 ))
553 }
554}