mirror of
https://github.com/Instadapp/aave-protocol-v2.git
synced 2024-07-29 21:47:30 +00:00
567 lines
19 KiB
Solidity
567 lines
19 KiB
Solidity
// SPDX-License-Identifier: agpl-3.0
|
|
pragma solidity 0.6.12;
|
|
pragma experimental ABIEncoderV2;
|
|
|
|
import {PercentageMath} from '../protocol/libraries/math/PercentageMath.sol';
|
|
import {SafeMath} from '../dependencies/openzeppelin/contracts/SafeMath.sol';
|
|
import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol';
|
|
import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol';
|
|
import {SafeERC20} from '../dependencies/openzeppelin/contracts/SafeERC20.sol';
|
|
import {Ownable} from '../dependencies/openzeppelin/contracts/Ownable.sol';
|
|
import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol';
|
|
import {DataTypes} from '../protocol/libraries/types/DataTypes.sol';
|
|
import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol';
|
|
import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol';
|
|
import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol';
|
|
import {FlashLoanReceiverBase} from '../flashloan/base/FlashLoanReceiverBase.sol';
|
|
import {IBaseUniswapAdapter} from './interfaces/IBaseUniswapAdapter.sol';
|
|
|
|
/**
|
|
* @title BaseUniswapAdapter
|
|
* @notice Implements the logic for performing assets swaps in Uniswap V2
|
|
* @author Aave
|
|
**/
|
|
abstract contract BaseUniswapAdapter is FlashLoanReceiverBase, IBaseUniswapAdapter, Ownable {
|
|
using SafeMath for uint256;
|
|
using PercentageMath for uint256;
|
|
using SafeERC20 for IERC20;
|
|
|
|
// Max slippage percent allowed
|
|
uint256 public constant override MAX_SLIPPAGE_PERCENT = 3000; // 30%
|
|
// FLash Loan fee set in lending pool
|
|
uint256 public constant override FLASHLOAN_PREMIUM_TOTAL = 9;
|
|
// USD oracle asset address
|
|
address public constant override USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96;
|
|
|
|
address public immutable override WETH_ADDRESS;
|
|
IPriceOracleGetter public immutable override ORACLE;
|
|
IUniswapV2Router02 public immutable override UNISWAP_ROUTER;
|
|
|
|
constructor(
|
|
ILendingPoolAddressesProvider addressesProvider,
|
|
IUniswapV2Router02 uniswapRouter,
|
|
address wethAddress
|
|
) public FlashLoanReceiverBase(addressesProvider) {
|
|
ORACLE = IPriceOracleGetter(addressesProvider.getPriceOracle());
|
|
UNISWAP_ROUTER = uniswapRouter;
|
|
WETH_ADDRESS = wethAddress;
|
|
}
|
|
|
|
/**
|
|
* @dev Given an input asset amount, returns the maximum output amount of the other asset and the prices
|
|
* @param amountIn Amount of reserveIn
|
|
* @param reserveIn Address of the asset to be swap from
|
|
* @param reserveOut Address of the asset to be swap to
|
|
* @return uint256 Amount out of the reserveOut
|
|
* @return uint256 The price of out amount denominated in the reserveIn currency (18 decimals)
|
|
* @return uint256 In amount of reserveIn value denominated in USD (8 decimals)
|
|
* @return uint256 Out amount of reserveOut value denominated in USD (8 decimals)
|
|
*/
|
|
function getAmountsOut(
|
|
uint256 amountIn,
|
|
address reserveIn,
|
|
address reserveOut
|
|
)
|
|
external
|
|
view
|
|
override
|
|
returns (
|
|
uint256,
|
|
uint256,
|
|
uint256,
|
|
uint256,
|
|
address[] memory
|
|
)
|
|
{
|
|
AmountCalc memory results = _getAmountsOutData(reserveIn, reserveOut, amountIn);
|
|
|
|
return (
|
|
results.calculatedAmount,
|
|
results.relativePrice,
|
|
results.amountInUsd,
|
|
results.amountOutUsd,
|
|
results.path
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev Returns the minimum input asset amount required to buy the given output asset amount and the prices
|
|
* @param amountOut Amount of reserveOut
|
|
* @param reserveIn Address of the asset to be swap from
|
|
* @param reserveOut Address of the asset to be swap to
|
|
* @return uint256 Amount in of the reserveIn
|
|
* @return uint256 The price of in amount denominated in the reserveOut currency (18 decimals)
|
|
* @return uint256 In amount of reserveIn value denominated in USD (8 decimals)
|
|
* @return uint256 Out amount of reserveOut value denominated in USD (8 decimals)
|
|
*/
|
|
function getAmountsIn(
|
|
uint256 amountOut,
|
|
address reserveIn,
|
|
address reserveOut
|
|
)
|
|
external
|
|
view
|
|
override
|
|
returns (
|
|
uint256,
|
|
uint256,
|
|
uint256,
|
|
uint256,
|
|
address[] memory
|
|
)
|
|
{
|
|
AmountCalc memory results = _getAmountsInData(reserveIn, reserveOut, amountOut);
|
|
|
|
return (
|
|
results.calculatedAmount,
|
|
results.relativePrice,
|
|
results.amountInUsd,
|
|
results.amountOutUsd,
|
|
results.path
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev Swaps an exact `amountToSwap` of an asset to another
|
|
* @param assetToSwapFrom Origin asset
|
|
* @param assetToSwapTo Destination asset
|
|
* @param amountToSwap Exact amount of `assetToSwapFrom` to be swapped
|
|
* @param minAmountOut the min amount of `assetToSwapTo` to be received from the swap
|
|
* @return the amount received from the swap
|
|
*/
|
|
function _swapExactTokensForTokens(
|
|
address assetToSwapFrom,
|
|
address assetToSwapTo,
|
|
uint256 amountToSwap,
|
|
uint256 minAmountOut,
|
|
bool useEthPath
|
|
) internal returns (uint256) {
|
|
uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom);
|
|
uint256 toAssetDecimals = _getDecimals(assetToSwapTo);
|
|
|
|
uint256 fromAssetPrice = _getPrice(assetToSwapFrom);
|
|
uint256 toAssetPrice = _getPrice(assetToSwapTo);
|
|
|
|
uint256 expectedMinAmountOut =
|
|
amountToSwap
|
|
.mul(fromAssetPrice.mul(10**toAssetDecimals))
|
|
.div(toAssetPrice.mul(10**fromAssetDecimals))
|
|
.percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(MAX_SLIPPAGE_PERCENT));
|
|
|
|
require(expectedMinAmountOut < minAmountOut, 'minAmountOut exceed max slippage');
|
|
|
|
// Approves the transfer for the swap. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix.
|
|
IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), 0);
|
|
IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), amountToSwap);
|
|
|
|
address[] memory path;
|
|
if (useEthPath) {
|
|
path = new address[](3);
|
|
path[0] = assetToSwapFrom;
|
|
path[1] = WETH_ADDRESS;
|
|
path[2] = assetToSwapTo;
|
|
} else {
|
|
path = new address[](2);
|
|
path[0] = assetToSwapFrom;
|
|
path[1] = assetToSwapTo;
|
|
}
|
|
uint256[] memory amounts =
|
|
UNISWAP_ROUTER.swapExactTokensForTokens(
|
|
amountToSwap,
|
|
minAmountOut,
|
|
path,
|
|
address(this),
|
|
block.timestamp
|
|
);
|
|
|
|
emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[amounts.length - 1]);
|
|
|
|
return amounts[amounts.length - 1];
|
|
}
|
|
|
|
/**
|
|
* @dev Receive an exact amount `amountToReceive` of `assetToSwapTo` tokens for as few `assetToSwapFrom` tokens as
|
|
* possible.
|
|
* @param assetToSwapFrom Origin asset
|
|
* @param assetToSwapTo Destination asset
|
|
* @param maxAmountToSwap Max amount of `assetToSwapFrom` allowed to be swapped
|
|
* @param amountToReceive Exact amount of `assetToSwapTo` to receive
|
|
* @return the amount swapped
|
|
*/
|
|
function _swapTokensForExactTokens(
|
|
address assetToSwapFrom,
|
|
address assetToSwapTo,
|
|
uint256 maxAmountToSwap,
|
|
uint256 amountToReceive,
|
|
bool useEthPath
|
|
) internal returns (uint256) {
|
|
uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom);
|
|
uint256 toAssetDecimals = _getDecimals(assetToSwapTo);
|
|
|
|
uint256 fromAssetPrice = _getPrice(assetToSwapFrom);
|
|
uint256 toAssetPrice = _getPrice(assetToSwapTo);
|
|
|
|
uint256 expectedMaxAmountToSwap =
|
|
amountToReceive
|
|
.mul(toAssetPrice.mul(10**fromAssetDecimals))
|
|
.div(fromAssetPrice.mul(10**toAssetDecimals))
|
|
.percentMul(PercentageMath.PERCENTAGE_FACTOR.add(MAX_SLIPPAGE_PERCENT));
|
|
|
|
require(maxAmountToSwap < expectedMaxAmountToSwap, 'maxAmountToSwap exceed max slippage');
|
|
|
|
// Approves the transfer for the swap. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix.
|
|
IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), 0);
|
|
IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), maxAmountToSwap);
|
|
|
|
address[] memory path;
|
|
if (useEthPath) {
|
|
path = new address[](3);
|
|
path[0] = assetToSwapFrom;
|
|
path[1] = WETH_ADDRESS;
|
|
path[2] = assetToSwapTo;
|
|
} else {
|
|
path = new address[](2);
|
|
path[0] = assetToSwapFrom;
|
|
path[1] = assetToSwapTo;
|
|
}
|
|
|
|
uint256[] memory amounts =
|
|
UNISWAP_ROUTER.swapTokensForExactTokens(
|
|
amountToReceive,
|
|
maxAmountToSwap,
|
|
path,
|
|
address(this),
|
|
block.timestamp
|
|
);
|
|
|
|
emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[amounts.length - 1]);
|
|
|
|
return amounts[0];
|
|
}
|
|
|
|
/**
|
|
* @dev Get the price of the asset from the oracle denominated in eth
|
|
* @param asset address
|
|
* @return eth price for the asset
|
|
*/
|
|
function _getPrice(address asset) internal view returns (uint256) {
|
|
return ORACLE.getAssetPrice(asset);
|
|
}
|
|
|
|
/**
|
|
* @dev Get the decimals of an asset
|
|
* @return number of decimals of the asset
|
|
*/
|
|
function _getDecimals(address asset) internal view returns (uint256) {
|
|
return IERC20Detailed(asset).decimals();
|
|
}
|
|
|
|
/**
|
|
* @dev Get the aToken associated to the asset
|
|
* @return address of the aToken
|
|
*/
|
|
function _getReserveData(address asset) internal view returns (DataTypes.ReserveData memory) {
|
|
return LENDING_POOL.getReserveData(asset);
|
|
}
|
|
|
|
/**
|
|
* @dev Pull the ATokens from the user
|
|
* @param reserve address of the asset
|
|
* @param reserveAToken address of the aToken of the reserve
|
|
* @param user address
|
|
* @param amount of tokens to be transferred to the contract
|
|
* @param permitSignature struct containing the permit signature
|
|
*/
|
|
function _pullAToken(
|
|
address reserve,
|
|
address reserveAToken,
|
|
address user,
|
|
uint256 amount,
|
|
PermitSignature memory permitSignature
|
|
) internal {
|
|
if (_usePermit(permitSignature)) {
|
|
IERC20WithPermit(reserveAToken).permit(
|
|
user,
|
|
address(this),
|
|
permitSignature.amount,
|
|
permitSignature.deadline,
|
|
permitSignature.v,
|
|
permitSignature.r,
|
|
permitSignature.s
|
|
);
|
|
}
|
|
|
|
// transfer from user to adapter
|
|
IERC20(reserveAToken).safeTransferFrom(user, address(this), amount);
|
|
|
|
// withdraw reserve
|
|
LENDING_POOL.withdraw(reserve, amount, address(this));
|
|
}
|
|
|
|
/**
|
|
* @dev Tells if the permit method should be called by inspecting if there is a valid signature.
|
|
* If signature params are set to 0, then permit won't be called.
|
|
* @param signature struct containing the permit signature
|
|
* @return whether or not permit should be called
|
|
*/
|
|
function _usePermit(PermitSignature memory signature) internal pure returns (bool) {
|
|
return
|
|
!(uint256(signature.deadline) == uint256(signature.v) && uint256(signature.deadline) == 0);
|
|
}
|
|
|
|
/**
|
|
* @dev Calculates the value denominated in USD
|
|
* @param reserve Address of the reserve
|
|
* @param amount Amount of the reserve
|
|
* @param decimals Decimals of the reserve
|
|
* @return whether or not permit should be called
|
|
*/
|
|
function _calcUsdValue(
|
|
address reserve,
|
|
uint256 amount,
|
|
uint256 decimals
|
|
) internal view returns (uint256) {
|
|
uint256 ethUsdPrice = _getPrice(USD_ADDRESS);
|
|
uint256 reservePrice = _getPrice(reserve);
|
|
|
|
return amount.mul(reservePrice).div(10**decimals).mul(ethUsdPrice).div(10**18);
|
|
}
|
|
|
|
/**
|
|
* @dev Given an input asset amount, returns the maximum output amount of the other asset
|
|
* @param reserveIn Address of the asset to be swap from
|
|
* @param reserveOut Address of the asset to be swap to
|
|
* @param amountIn Amount of reserveIn
|
|
* @return Struct containing the following information:
|
|
* uint256 Amount out of the reserveOut
|
|
* uint256 The price of out amount denominated in the reserveIn currency (18 decimals)
|
|
* uint256 In amount of reserveIn value denominated in USD (8 decimals)
|
|
* uint256 Out amount of reserveOut value denominated in USD (8 decimals)
|
|
*/
|
|
function _getAmountsOutData(
|
|
address reserveIn,
|
|
address reserveOut,
|
|
uint256 amountIn
|
|
) internal view returns (AmountCalc memory) {
|
|
// Subtract flash loan fee
|
|
uint256 finalAmountIn = amountIn.sub(amountIn.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000));
|
|
|
|
if (reserveIn == reserveOut) {
|
|
uint256 reserveDecimals = _getDecimals(reserveIn);
|
|
address[] memory path = new address[](1);
|
|
path[0] = reserveIn;
|
|
|
|
return
|
|
AmountCalc(
|
|
finalAmountIn,
|
|
finalAmountIn.mul(10**18).div(amountIn),
|
|
_calcUsdValue(reserveIn, amountIn, reserveDecimals),
|
|
_calcUsdValue(reserveIn, finalAmountIn, reserveDecimals),
|
|
path
|
|
);
|
|
}
|
|
|
|
address[] memory simplePath = new address[](2);
|
|
simplePath[0] = reserveIn;
|
|
simplePath[1] = reserveOut;
|
|
|
|
uint256[] memory amountsWithoutWeth;
|
|
uint256[] memory amountsWithWeth;
|
|
|
|
address[] memory pathWithWeth = new address[](3);
|
|
if (reserveIn != WETH_ADDRESS && reserveOut != WETH_ADDRESS) {
|
|
pathWithWeth[0] = reserveIn;
|
|
pathWithWeth[1] = WETH_ADDRESS;
|
|
pathWithWeth[2] = reserveOut;
|
|
|
|
try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, pathWithWeth) returns (
|
|
uint256[] memory resultsWithWeth
|
|
) {
|
|
amountsWithWeth = resultsWithWeth;
|
|
} catch {
|
|
amountsWithWeth = new uint256[](3);
|
|
}
|
|
} else {
|
|
amountsWithWeth = new uint256[](3);
|
|
}
|
|
|
|
uint256 bestAmountOut;
|
|
try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, simplePath) returns (
|
|
uint256[] memory resultAmounts
|
|
) {
|
|
amountsWithoutWeth = resultAmounts;
|
|
|
|
bestAmountOut = (amountsWithWeth[2] > amountsWithoutWeth[1])
|
|
? amountsWithWeth[2]
|
|
: amountsWithoutWeth[1];
|
|
} catch {
|
|
amountsWithoutWeth = new uint256[](2);
|
|
bestAmountOut = amountsWithWeth[2];
|
|
}
|
|
|
|
uint256 reserveInDecimals = _getDecimals(reserveIn);
|
|
uint256 reserveOutDecimals = _getDecimals(reserveOut);
|
|
|
|
uint256 outPerInPrice =
|
|
finalAmountIn.mul(10**18).mul(10**reserveOutDecimals).div(
|
|
bestAmountOut.mul(10**reserveInDecimals)
|
|
);
|
|
|
|
return
|
|
AmountCalc(
|
|
bestAmountOut,
|
|
outPerInPrice,
|
|
_calcUsdValue(reserveIn, amountIn, reserveInDecimals),
|
|
_calcUsdValue(reserveOut, bestAmountOut, reserveOutDecimals),
|
|
(bestAmountOut == 0) ? new address[](2) : (bestAmountOut == amountsWithoutWeth[1])
|
|
? simplePath
|
|
: pathWithWeth
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev Returns the minimum input asset amount required to buy the given output asset amount
|
|
* @param reserveIn Address of the asset to be swap from
|
|
* @param reserveOut Address of the asset to be swap to
|
|
* @param amountOut Amount of reserveOut
|
|
* @return Struct containing the following information:
|
|
* uint256 Amount in of the reserveIn
|
|
* uint256 The price of in amount denominated in the reserveOut currency (18 decimals)
|
|
* uint256 In amount of reserveIn value denominated in USD (8 decimals)
|
|
* uint256 Out amount of reserveOut value denominated in USD (8 decimals)
|
|
*/
|
|
function _getAmountsInData(
|
|
address reserveIn,
|
|
address reserveOut,
|
|
uint256 amountOut
|
|
) internal view returns (AmountCalc memory) {
|
|
if (reserveIn == reserveOut) {
|
|
// Add flash loan fee
|
|
uint256 amountIn = amountOut.add(amountOut.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000));
|
|
uint256 reserveDecimals = _getDecimals(reserveIn);
|
|
address[] memory path = new address[](1);
|
|
path[0] = reserveIn;
|
|
|
|
return
|
|
AmountCalc(
|
|
amountIn,
|
|
amountOut.mul(10**18).div(amountIn),
|
|
_calcUsdValue(reserveIn, amountIn, reserveDecimals),
|
|
_calcUsdValue(reserveIn, amountOut, reserveDecimals),
|
|
path
|
|
);
|
|
}
|
|
|
|
(uint256[] memory amounts, address[] memory path) =
|
|
_getAmountsInAndPath(reserveIn, reserveOut, amountOut);
|
|
|
|
// Add flash loan fee
|
|
uint256 finalAmountIn = amounts[0].add(amounts[0].mul(FLASHLOAN_PREMIUM_TOTAL).div(10000));
|
|
|
|
uint256 reserveInDecimals = _getDecimals(reserveIn);
|
|
uint256 reserveOutDecimals = _getDecimals(reserveOut);
|
|
|
|
uint256 inPerOutPrice =
|
|
amountOut.mul(10**18).mul(10**reserveInDecimals).div(
|
|
finalAmountIn.mul(10**reserveOutDecimals)
|
|
);
|
|
|
|
return
|
|
AmountCalc(
|
|
finalAmountIn,
|
|
inPerOutPrice,
|
|
_calcUsdValue(reserveIn, finalAmountIn, reserveInDecimals),
|
|
_calcUsdValue(reserveOut, amountOut, reserveOutDecimals),
|
|
path
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev Calculates the input asset amount required to buy the given output asset amount
|
|
* @param reserveIn Address of the asset to be swap from
|
|
* @param reserveOut Address of the asset to be swap to
|
|
* @param amountOut Amount of reserveOut
|
|
* @return uint256[] amounts Array containing the amountIn and amountOut for a swap
|
|
*/
|
|
function _getAmountsInAndPath(
|
|
address reserveIn,
|
|
address reserveOut,
|
|
uint256 amountOut
|
|
) internal view returns (uint256[] memory, address[] memory) {
|
|
address[] memory simplePath = new address[](2);
|
|
simplePath[0] = reserveIn;
|
|
simplePath[1] = reserveOut;
|
|
|
|
uint256[] memory amountsWithoutWeth;
|
|
uint256[] memory amountsWithWeth;
|
|
address[] memory pathWithWeth = new address[](3);
|
|
|
|
if (reserveIn != WETH_ADDRESS && reserveOut != WETH_ADDRESS) {
|
|
pathWithWeth[0] = reserveIn;
|
|
pathWithWeth[1] = WETH_ADDRESS;
|
|
pathWithWeth[2] = reserveOut;
|
|
|
|
try UNISWAP_ROUTER.getAmountsIn(amountOut, pathWithWeth) returns (
|
|
uint256[] memory resultsWithWeth
|
|
) {
|
|
amountsWithWeth = resultsWithWeth;
|
|
} catch {
|
|
amountsWithWeth = new uint256[](3);
|
|
}
|
|
} else {
|
|
amountsWithWeth = new uint256[](3);
|
|
}
|
|
|
|
try UNISWAP_ROUTER.getAmountsIn(amountOut, simplePath) returns (
|
|
uint256[] memory resultAmounts
|
|
) {
|
|
amountsWithoutWeth = resultAmounts;
|
|
|
|
return
|
|
(amountsWithWeth[0] < amountsWithoutWeth[0] && amountsWithWeth[0] != 0)
|
|
? (amountsWithWeth, pathWithWeth)
|
|
: (amountsWithoutWeth, simplePath);
|
|
} catch {
|
|
return (amountsWithWeth, pathWithWeth);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Calculates the input asset amount required to buy the given output asset amount
|
|
* @param reserveIn Address of the asset to be swap from
|
|
* @param reserveOut Address of the asset to be swap to
|
|
* @param amountOut Amount of reserveOut
|
|
* @return uint256[] amounts Array containing the amountIn and amountOut for a swap
|
|
*/
|
|
function _getAmountsIn(
|
|
address reserveIn,
|
|
address reserveOut,
|
|
uint256 amountOut,
|
|
bool useEthPath
|
|
) internal view returns (uint256[] memory) {
|
|
address[] memory path;
|
|
|
|
if (useEthPath) {
|
|
path = new address[](3);
|
|
path[0] = reserveIn;
|
|
path[1] = WETH_ADDRESS;
|
|
path[2] = reserveOut;
|
|
} else {
|
|
path = new address[](2);
|
|
path[0] = reserveIn;
|
|
path[1] = reserveOut;
|
|
}
|
|
|
|
return UNISWAP_ROUTER.getAmountsIn(amountOut, path);
|
|
}
|
|
|
|
/**
|
|
* @dev Emergency rescue for token stucked on this contract, as failsafe mechanism
|
|
* - Funds should never remain in this contract more time than during transactions
|
|
* - Only callable by the owner
|
|
**/
|
|
function rescueTokens(IERC20 token) external onlyOwner {
|
|
token.transfer(owner(), token.balanceOf(address(this)));
|
|
}
|
|
}
|