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_UNEXPECTED: &str = "Unexpected error";
237const ERR_PATH_TOO_LONG: &str = "Swap path exceeds MaxSwapPathLength";
238const ERR_INVALID_ASSET_ENCODING: &str = "Failed to SCALE-decode asset kind";
239
240impl<const ADDRESS: u16, Runtime> AssetConversion<ADDRESS, Runtime>
241where
242 Runtime: pallet_asset_conversion::Config + pallet_revive::Config,
243 alloy::primitives::U256: TryInto<<Runtime as pallet_asset_conversion::Config>::Balance>,
244 alloy::primitives::U256: TryFrom<<Runtime as pallet_asset_conversion::Config>::Balance>,
245{
246 fn caller_account_id(
248 env: &impl Ext<T = Runtime>,
249 ) -> Result<<Runtime as frame_system::Config>::AccountId, Error> {
250 env.caller()
251 .account_id()
252 .map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
253 .cloned()
254 }
255
256 fn decode_asset_kind(
258 data: &[u8],
259 ) -> Result<<Runtime as pallet_asset_conversion::Config>::AssetKind, Error> {
260 <Runtime as pallet_asset_conversion::Config>::AssetKind::decode(&mut &data[..])
261 .map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_ASSET_ENCODING.into() }))
262 }
263
264 fn validated_path_len<T>(path: &[T]) -> Result<u32, Error> {
266 let len = path.len() as u32;
267 let max = <Runtime as pallet_asset_conversion::Config>::MaxSwapPathLength::get();
268 if len > max {
269 return Err(Error::Revert(Revert { reason: ERR_PATH_TOO_LONG.into() }));
270 }
271 Ok(len)
272 }
273
274 fn to_balance(
275 value: alloy::primitives::U256,
276 ) -> Result<<Runtime as pallet_asset_conversion::Config>::Balance, Error> {
277 value
278 .try_into()
279 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
280 }
281
282 fn to_u256(
283 value: <Runtime as pallet_asset_conversion::Config>::Balance,
284 ) -> Result<alloy::primitives::U256, Error> {
285 alloy::primitives::U256::try_from(value)
286 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
287 }
288
289 fn swap_exact_tokens_for_tokens(
290 call: &IAssetConversion::swapExactTokensForTokensCall,
291 env: &mut impl Ext<T = Runtime>,
292 ) -> Result<Vec<u8>, Error> {
293 let path_len = Self::validated_path_len(&call.path)?;
294 env.charge(
295 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_exact_tokens_for_tokens(
296 path_len,
297 ),
298 )?;
299 let path: Vec<_> =
300 call.path.iter().map(|e| Self::decode_asset_kind(e)).collect::<Result<_, _>>()?;
301
302 let sender = Self::caller_account_id(env)?;
303 let send_to = env.to_account_id(&H160(call.sendTo.0 .0));
304
305 let amount_out = <pallet_asset_conversion::Pallet<Runtime> as Swap<
306 <Runtime as frame_system::Config>::AccountId,
307 >>::swap_exact_tokens_for_tokens(
308 sender,
309 path,
310 Self::to_balance(call.amountIn)?,
311 Some(Self::to_balance(call.amountOutMin)?),
312 send_to,
313 call.keepAlive,
314 )?;
315
316 Ok(IAssetConversion::swapExactTokensForTokensCall::abi_encode_returns(&Self::to_u256(
317 amount_out,
318 )?))
319 }
320
321 fn swap_tokens_for_exact_tokens(
322 call: &IAssetConversion::swapTokensForExactTokensCall,
323 env: &mut impl Ext<T = Runtime>,
324 ) -> Result<Vec<u8>, Error> {
325 let path_len = Self::validated_path_len(&call.path)?;
326 env.charge(
327 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_tokens_for_exact_tokens(
328 path_len,
329 ),
330 )?;
331 let path: Vec<_> =
332 call.path.iter().map(|e| Self::decode_asset_kind(e)).collect::<Result<_, _>>()?;
333
334 let sender = Self::caller_account_id(env)?;
335 let send_to = env.to_account_id(&H160(call.sendTo.0 .0));
336
337 let amount_in = <pallet_asset_conversion::Pallet<Runtime> as Swap<
338 <Runtime as frame_system::Config>::AccountId,
339 >>::swap_tokens_for_exact_tokens(
340 sender,
341 path,
342 Self::to_balance(call.amountOut)?,
343 Some(Self::to_balance(call.amountInMax)?),
344 send_to,
345 call.keepAlive,
346 )?;
347
348 Ok(IAssetConversion::swapTokensForExactTokensCall::abi_encode_returns(&Self::to_u256(
349 amount_in,
350 )?))
351 }
352
353 fn quote_exact_tokens_for_tokens(
354 call: &IAssetConversion::quoteExactTokensForTokensCall,
355 env: &mut impl Ext<T = Runtime>,
356 ) -> Result<Vec<u8>, Error> {
357 env.charge(
362 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_exact_tokens_for_tokens(
363 2,
364 ),
365 )?;
366
367 let asset1 = Self::decode_asset_kind(&call.asset1)?;
368 let asset2 = Self::decode_asset_kind(&call.asset2)?;
369
370 let quoted =
371 <pallet_asset_conversion::Pallet<Runtime> as QuotePrice>::quote_price_exact_tokens_for_tokens(
372 asset1,
373 asset2,
374 Self::to_balance(call.amount)?,
375 call.includeFee,
376 )
377 .ok_or(Error::Revert(Revert { reason: ERR_POOL_NOT_FOUND.into() }))?;
378
379 Ok(IAssetConversion::quoteExactTokensForTokensCall::abi_encode_returns(&Self::to_u256(
380 quoted,
381 )?))
382 }
383
384 fn quote_tokens_for_exact_tokens(
385 call: &IAssetConversion::quoteTokensForExactTokensCall,
386 env: &mut impl Ext<T = Runtime>,
387 ) -> Result<Vec<u8>, Error> {
388 env.charge(
390 <Runtime as pallet_asset_conversion::Config>::WeightInfo::swap_tokens_for_exact_tokens(
391 2,
392 ),
393 )?;
394
395 let asset1 = Self::decode_asset_kind(&call.asset1)?;
396 let asset2 = Self::decode_asset_kind(&call.asset2)?;
397
398 let quoted =
399 <pallet_asset_conversion::Pallet<Runtime> as QuotePrice>::quote_price_tokens_for_exact_tokens(
400 asset1,
401 asset2,
402 Self::to_balance(call.amount)?,
403 call.includeFee,
404 )
405 .ok_or(Error::Revert(Revert { reason: ERR_POOL_NOT_FOUND.into() }))?;
406
407 Ok(IAssetConversion::quoteTokensForExactTokensCall::abi_encode_returns(&Self::to_u256(
408 quoted,
409 )?))
410 }
411
412 fn create_pool(
413 call: &IAssetConversion::createPoolCall,
414 env: &mut impl Ext<T = Runtime>,
415 ) -> Result<Vec<u8>, Error> {
416 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::create_pool())?;
417
418 let asset1 = Self::decode_asset_kind(&call.asset1)?;
419 let asset2 = Self::decode_asset_kind(&call.asset2)?;
420
421 let sender = Self::caller_account_id(env)?;
422
423 <pallet_asset_conversion::Pallet<Runtime> as MutateLiquidity<
424 <Runtime as frame_system::Config>::AccountId,
425 >>::create_pool(&sender, asset1, asset2)?;
426
427 Ok(Vec::new())
428 }
429
430 fn add_liquidity(
431 call: &IAssetConversion::addLiquidityCall,
432 env: &mut impl Ext<T = Runtime>,
433 ) -> Result<Vec<u8>, Error> {
434 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::add_liquidity())?;
435
436 let asset1 = Self::decode_asset_kind(&call.asset1)?;
437 let asset2 = Self::decode_asset_kind(&call.asset2)?;
438
439 let sender = Self::caller_account_id(env)?;
440 let mint_to = env.to_account_id(&H160(call.mintTo.0 .0));
441
442 let lp_tokens = <pallet_asset_conversion::Pallet<Runtime> as MutateLiquidity<
443 <Runtime as frame_system::Config>::AccountId,
444 >>::add_liquidity(
445 &sender,
446 AddLiquidityAsset {
447 asset: asset1,
448 amount_desired: Self::to_balance(call.amount1Desired)?,
449 amount_min: Self::to_balance(call.amount1Min)?,
450 },
451 AddLiquidityAsset {
452 asset: asset2,
453 amount_desired: Self::to_balance(call.amount2Desired)?,
454 amount_min: Self::to_balance(call.amount2Min)?,
455 },
456 &mint_to,
457 )?;
458
459 Ok(IAssetConversion::addLiquidityCall::abi_encode_returns(&Self::to_u256(lp_tokens)?))
460 }
461
462 fn remove_liquidity(
463 call: &IAssetConversion::removeLiquidityCall,
464 env: &mut impl Ext<T = Runtime>,
465 ) -> Result<Vec<u8>, Error> {
466 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::remove_liquidity())?;
467
468 let asset1 = Self::decode_asset_kind(&call.asset1)?;
469 let asset2 = Self::decode_asset_kind(&call.asset2)?;
470
471 let sender = Self::caller_account_id(env)?;
472 let withdraw_to = env.to_account_id(&H160(call.withdrawTo.0 .0));
473
474 let (amount1, amount2) = <pallet_asset_conversion::Pallet<Runtime> as MutateLiquidity<
475 <Runtime as frame_system::Config>::AccountId,
476 >>::remove_liquidity(
477 &sender,
478 asset1,
479 asset2,
480 Self::to_balance(call.lpTokenBurn)?,
481 Self::to_balance(call.amount1MinReceive)?,
482 Self::to_balance(call.amount2MinReceive)?,
483 &withdraw_to,
484 )?;
485
486 Ok(IAssetConversion::removeLiquidityCall::abi_encode_returns(
487 &IAssetConversion::removeLiquidityReturn {
488 amount1: Self::to_u256(amount1)?,
489 amount2: Self::to_u256(amount2)?,
490 },
491 ))
492 }
493
494 fn get_reserves(
495 call: &IAssetConversion::getReservesCall,
496 env: &mut impl Ext<T = Runtime>,
497 ) -> Result<Vec<u8>, Error> {
498 env.charge(<Runtime as pallet_asset_conversion::Config>::WeightInfo::get_reserves())?;
499
500 let asset1 = Self::decode_asset_kind(&call.asset1)?;
501 let asset2 = Self::decode_asset_kind(&call.asset2)?;
502
503 let (reserve1, reserve2) = pallet_asset_conversion::Pallet::<Runtime>::get_reserves(
504 asset1, asset2,
505 )
506 .map_err(|e| match e {
507 pallet_asset_conversion::Error::InvalidAssetPair => {
508 Error::Revert(Revert { reason: ERR_INVALID_ASSET_PAIR.into() })
509 },
510 pallet_asset_conversion::Error::PoolEmpty => {
511 Error::Revert(Revert { reason: ERR_POOL_EMPTY.into() })
512 },
513 pallet_asset_conversion::Error::PoolExists |
516 pallet_asset_conversion::Error::WrongDesiredAmount |
517 pallet_asset_conversion::Error::AmountOneLessThanMinimal |
518 pallet_asset_conversion::Error::AmountTwoLessThanMinimal |
519 pallet_asset_conversion::Error::ReserveLeftLessThanMinimal |
520 pallet_asset_conversion::Error::AmountOutTooHigh |
521 pallet_asset_conversion::Error::PoolNotFound |
522 pallet_asset_conversion::Error::Overflow |
523 pallet_asset_conversion::Error::AssetOneDepositDidNotMeetMinimum |
524 pallet_asset_conversion::Error::AssetTwoDepositDidNotMeetMinimum |
525 pallet_asset_conversion::Error::AssetOneWithdrawalDidNotMeetMinimum |
526 pallet_asset_conversion::Error::AssetTwoWithdrawalDidNotMeetMinimum |
527 pallet_asset_conversion::Error::OptimalAmountLessThanDesired |
528 pallet_asset_conversion::Error::InsufficientLiquidityMinted |
529 pallet_asset_conversion::Error::ZeroLiquidity |
530 pallet_asset_conversion::Error::ZeroAmount |
531 pallet_asset_conversion::Error::ProvidedMinimumNotSufficientForSwap |
532 pallet_asset_conversion::Error::ProvidedMaximumNotSufficientForSwap |
533 pallet_asset_conversion::Error::InvalidPath |
534 pallet_asset_conversion::Error::NonUniquePath |
535 pallet_asset_conversion::Error::IncorrectPoolAssetId |
536 pallet_asset_conversion::Error::BelowMinimum => {
537 frame_support::defensive!("get_reserves returned unexpected error");
538 Error::Revert(Revert { reason: ERR_UNEXPECTED.into() })
539 },
540 })?;
541
542 Ok(IAssetConversion::getReservesCall::abi_encode_returns(
543 &IAssetConversion::getReservesReturn {
544 reserve1: Self::to_u256(reserve1)?,
545 reserve2: Self::to_u256(reserve2)?,
546 },
547 ))
548 }
549}