-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from VenusProtocol/feat/converter-operator
[VEN-2299]: add a contract to operate token converters
- Loading branch information
Showing
18 changed files
with
2,755 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.13; | ||
|
||
import { IPancakeV3SwapCallback } from "@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3SwapCallback.sol"; | ||
import { IPancakeV3Pool } from "@pancakeswap/v3-core/contracts/interfaces/IPancakeV3Pool.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; | ||
|
||
import { approveOrRevert } from "../util/approveOrRevert.sol"; | ||
import { ISmartRouter } from "../third-party/pancakeswap-v8/ISmartRouter.sol"; | ||
import { Path } from "../third-party/pancakeswap-v8/Path.sol"; | ||
import { PoolAddress } from "../third-party/pancakeswap-v8/PoolAddress.sol"; | ||
import { MIN_SQRT_RATIO, MAX_SQRT_RATIO } from "../third-party/pancakeswap-v8/constants.sol"; | ||
|
||
/// @title ExactOutputFlashSwap | ||
/// @notice A base contract for exact output flash swap operations. | ||
/// | ||
/// Upon calling _flashSwap, swaps tokenX to tokenY using a flash swap, i.e. the contract: | ||
/// | ||
/// 1. Invokes the flash swap on the first pool from the path | ||
/// 2. Receives tokenY from the pool | ||
/// 3. Calls _onMoneyReceived, which should ensure that the contract has enough tokenX | ||
/// to repay the flash swap | ||
/// 4. Repays the flash swap with tokenX (doing the conversion if necessary) | ||
/// 5. Calls _onFlashSwapCompleted | ||
/// | ||
/// @dev This contract is abstract and should be inherited by a contract that implements | ||
/// _onMoneyReceived and _onFlashSwapCompleted. Note that in the callbacks transaction | ||
/// context (sender and value) is different from the original context. The inheriting | ||
/// contracts should save the original context in the application-specific data bytes | ||
/// passed to the callbacks. | ||
abstract contract ExactOutputFlashSwap is IPancakeV3SwapCallback { | ||
using SafeERC20 for IERC20; | ||
using Path for bytes; | ||
|
||
/// @notice Flash swap parameters | ||
struct FlashSwapParams { | ||
/// @notice Amount of tokenY to receive during the flash swap | ||
uint256 amountOut; | ||
/// @notice Exact-output (reversed) swap path, starting with tokenY and ending with tokenX | ||
bytes path; | ||
/// @notice Application-specific data | ||
bytes data; | ||
} | ||
|
||
/// @notice Callback data passed to the swap callback | ||
struct Envelope { | ||
/// @notice Exact-output (reversed) swap path, starting with tokenY and ending with tokenX | ||
bytes path; | ||
/// @notice Application-specific data | ||
bytes data; | ||
/// @notice Pool key of the pool that should have called the callback | ||
PoolAddress.PoolKey poolKey; | ||
} | ||
|
||
/// @notice The PancakeSwap SmartRouter contract | ||
ISmartRouter public immutable SWAP_ROUTER; | ||
|
||
/// @notice The PancakeSwap deployer contract | ||
address public immutable DEPLOYER; | ||
|
||
/// @notice Thrown if swap callback is called by a non-PancakeSwap contract | ||
/// @param expected Expected callback sender (pool address computed based on the pool key) | ||
/// @param actual Actual callback sender | ||
error InvalidCallbackSender(address expected, address actual); | ||
|
||
/// @notice Thrown if the swap callback is called with unexpected or zero amount of tokens | ||
error EmptySwap(); | ||
|
||
/// @param swapRouter_ PancakeSwap SmartRouter contract | ||
constructor(ISmartRouter swapRouter_) { | ||
ensureNonzeroAddress(address(swapRouter_)); | ||
|
||
SWAP_ROUTER = swapRouter_; | ||
DEPLOYER = swapRouter_.deployer(); | ||
} | ||
|
||
/// @notice Callback called by PancakeSwap pool during flash swap conversion | ||
/// @param amount0Delta Amount of pool's token0 to repay for the flash swap (negative if no need to repay this token) | ||
/// @param amount1Delta Amount of pool's token1 to repay for the flash swap (negative if no need to repay this token) | ||
/// @param data Callback data containing an Envelope structure | ||
function pancakeV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { | ||
Envelope memory envelope = abi.decode(data, (Envelope)); | ||
_verifyCallback(envelope.poolKey); | ||
if (amount0Delta <= 0 && amount1Delta <= 0) { | ||
revert EmptySwap(); | ||
} | ||
|
||
uint256 amountToPay; | ||
IERC20 tokenToPay; | ||
if (amount0Delta > 0) { | ||
tokenToPay = IERC20(envelope.poolKey.token0); | ||
amountToPay = uint256(amount0Delta); | ||
} else if (amount1Delta > 0) { | ||
tokenToPay = IERC20(envelope.poolKey.token1); | ||
amountToPay = uint256(amount1Delta); | ||
} | ||
|
||
(IERC20 tokenIn, uint256 maxAmountIn) = _onMoneyReceived(envelope.data); | ||
|
||
if (envelope.path.hasMultiplePools()) { | ||
bytes memory remainingPath = envelope.path.skipToken(); | ||
approveOrRevert(tokenIn, address(SWAP_ROUTER), maxAmountIn); | ||
SWAP_ROUTER.exactOutput( | ||
ISmartRouter.ExactOutputParams({ | ||
path: remainingPath, | ||
recipient: msg.sender, // repaying to the pool | ||
amountOut: amountToPay, | ||
amountInMaximum: maxAmountIn | ||
}) | ||
); | ||
approveOrRevert(tokenIn, address(SWAP_ROUTER), 0); | ||
} else { | ||
// If the path had just one pool, tokenToPay should be tokenX, so we can just repay the debt. | ||
tokenToPay.safeTransfer(msg.sender, amountToPay); | ||
} | ||
|
||
_onFlashSwapCompleted(envelope.data); | ||
} | ||
|
||
/// @dev Initiates a flash swap | ||
/// @param params Flash swap parameters | ||
function _flashSwap(FlashSwapParams memory params) internal { | ||
(address tokenY, address tokenB, uint24 fee) = params.path.decodeFirstPool(); | ||
PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey(tokenY, tokenB, fee); | ||
IPancakeV3Pool pool = IPancakeV3Pool(PoolAddress.computeAddress(DEPLOYER, poolKey)); | ||
|
||
bool swapZeroForOne = poolKey.token1 == tokenY; | ||
uint160 sqrtPriceLimitX96 = (swapZeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1); | ||
pool.swap( | ||
address(this), | ||
swapZeroForOne, | ||
-int256(params.amountOut), | ||
sqrtPriceLimitX96, | ||
abi.encode(Envelope(params.path, params.data, poolKey)) | ||
); | ||
} | ||
|
||
/// @dev Called when token Y is received during a flash swap. This function has to ensure | ||
/// that at the end of the execution the contract has enough token X to repay the flash | ||
/// swap. | ||
/// Note that msg.sender is the pool that called the callback, not the original caller | ||
/// of the transaction where _flashSwap was invoked. | ||
/// @param data Application-specific data | ||
/// @return tokenIn Token X | ||
/// @return maxAmountIn Maximum amount of token X to be used to repay the flash swap | ||
function _onMoneyReceived(bytes memory data) internal virtual returns (IERC20 tokenIn, uint256 maxAmountIn); | ||
|
||
/// @dev Called when the flash swap is completed and was paid for. By default, does nothing. | ||
/// Note that msg.sender is the pool that called the callback, not the original caller | ||
/// of the transaction where _flashSwap was invoked. | ||
/// @param data Application-specific data | ||
// solhint-disable-next-line no-empty-blocks | ||
function _onFlashSwapCompleted(bytes memory data) internal virtual {} | ||
|
||
/// @dev Ensures that the caller of a callback is a legitimate PancakeSwap pool | ||
/// @param poolKey The pool key of the pool to verify | ||
function _verifyCallback(PoolAddress.PoolKey memory poolKey) internal view { | ||
address pool = PoolAddress.computeAddress(DEPLOYER, poolKey); | ||
if (msg.sender != pool) { | ||
revert InvalidCallbackSender(pool, msg.sender); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.13; | ||
|
||
import { IPancakeV3SwapCallback } from "@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3SwapCallback.sol"; | ||
import { IPancakeV3Pool } from "@pancakeswap/v3-core/contracts/interfaces/IPancakeV3Pool.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; | ||
import { IAbstractTokenConverter } from "@venusprotocol/protocol-reserve/contracts/TokenConverter/IAbstractTokenConverter.sol"; | ||
|
||
import { ExactOutputFlashSwap } from "../flash-swap/ExactOutputFlashSwap.sol"; | ||
import { approveOrRevert } from "../util/approveOrRevert.sol"; | ||
import { transferAll } from "../util/transferAll.sol"; | ||
import { ISmartRouter } from "../third-party/pancakeswap-v8/ISmartRouter.sol"; | ||
import { BytesLib } from "../third-party/pancakeswap-v8/BytesLib.sol"; | ||
|
||
/// @title TokenConverterOperator | ||
/// @notice Converts tokens in a TokenConverter using an exact-output flash swap | ||
/// @dev Expects a reversed (exact output) path, i.e. the path starting with the token | ||
/// that it _sends_ to TokenConverter and ends with the token that it _receives_ from | ||
/// TokenConverter, e.g. if TokenConverter has BTC and wants USDT, the path should be | ||
/// USDT->(TokenB)->(TokenC)->...->BTC. This contract will then: | ||
/// 1. Compute the amount of USDT required for the conversion | ||
/// 2. Flash-swap TokenB to USDT (`tokenToSendToConverter`) | ||
/// 3. Use TokenConverter to convert USDT to BTC (`tokenToReceiveFromConverter`) | ||
/// 4. Swap some portion of BTC to an exact amount of TokenB (`tokenToPay`) | ||
/// 5. Repay for the swap in TokenB | ||
/// 6. Transfer the rest of BTC to the caller | ||
/// The exact output converter differs from an exact input version in that it sends the | ||
/// income in `tokenToReceiveFromConverter` to the beneficiary, while an exact input | ||
/// version would send the income in `tokenToSendToConverter`. The former is supposedly | ||
/// a bit more efficient since there's no slippage associated with the income conversion. | ||
contract TokenConverterOperator is ExactOutputFlashSwap { | ||
using SafeERC20 for IERC20; | ||
using BytesLib for bytes; | ||
|
||
/// @notice Conversion parameters | ||
struct ConversionParameters { | ||
/// @notice The receiver of the arbitrage income | ||
address beneficiary; | ||
/// @notice The token currently in the TokenConverter | ||
IERC20 tokenToReceiveFromConverter; | ||
/// @notice The amount (in `tokenToReceiveFromConverter` tokens) to receive as a result of conversion | ||
uint256 amount; | ||
/// @notice Minimal income to get from the arbitrage transaction (in `tokenToReceiveFromConverter`). | ||
/// This value can be negative to indicate that the sender is willing to pay for the transaction | ||
/// execution. In this case, abs(minIncome) will be withdrawn from the sender's wallet, the | ||
/// arbitrage will be executed, and the excess (if any) will be sent to the beneficiary. | ||
int256 minIncome; | ||
/// @notice The token the TokenConverter would get | ||
IERC20 tokenToSendToConverter; | ||
/// @notice Address of the token converter contract to arbitrage | ||
IAbstractTokenConverter converter; | ||
/// @notice Reversed (exact output) path to trade from `tokenToReceiveFromConverter` | ||
/// to `tokenToSendToConverter` | ||
bytes path; | ||
/// @notice Deadline for the transaction execution | ||
uint256 deadline; | ||
} | ||
|
||
/// @notice Conversion data to pass between calls | ||
struct ConversionData { | ||
/// @notice The receiver of the arbitrage income | ||
address beneficiary; | ||
/// @notice The token the TokenConverter would receive | ||
IERC20 tokenToSendToConverter; | ||
/// @notice The amount (in `amountToSendToConverter` tokens) to send to converter | ||
uint256 amountToSendToConverter; | ||
/// @notice The token currently in the TokenConverter | ||
IERC20 tokenToReceiveFromConverter; | ||
/// @notice The amount (in `tokenToReceiveFromConverter` tokens) to receive | ||
uint256 amountToReceiveFromConverter; | ||
/// @notice Minimal income to get from the arbitrage transaction (in `amountToReceiveFromConverter`). | ||
int256 minIncome; | ||
/// @notice Address of the token converter contract to arbitrage | ||
IAbstractTokenConverter converter; | ||
} | ||
|
||
/// @notice Thrown if the provided swap path start does not correspond to tokenToSendToConverter | ||
/// @param expected Expected swap path start (tokenToSendToConverter) | ||
/// @param actual Provided swap path start | ||
error InvalidSwapStart(address expected, address actual); | ||
|
||
/// @notice Thrown if the provided swap path end does not correspond to tokenToReceiveFromConverter | ||
/// @param expected Expected swap path end (tokenToReceiveFromConverter) | ||
/// @param actual Provided swap path end | ||
error InvalidSwapEnd(address expected, address actual); | ||
|
||
/// @notice Thrown if the amount of to receive from TokenConverter is less than expected | ||
/// @param expected Expected amount of tokens | ||
/// @param actual Actual amount of tokens | ||
error InsufficientLiquidity(uint256 expected, uint256 actual); | ||
|
||
/// @notice Thrown if the deadline has passed | ||
error DeadlinePassed(uint256 currentTimestamp, uint256 deadline); | ||
|
||
/// @notice Thrown on math underflow | ||
error Underflow(); | ||
|
||
/// @notice Thrown on math overflow | ||
error Overflow(); | ||
|
||
/// @param swapRouter_ PancakeSwap SmartRouter contract | ||
// solhint-disable-next-line no-empty-blocks | ||
constructor(ISmartRouter swapRouter_) ExactOutputFlashSwap(swapRouter_) {} | ||
|
||
/// @notice Converts tokens in a TokenConverter using a flash swap | ||
/// @param params Conversion parameters | ||
function convert(ConversionParameters calldata params) external { | ||
if (params.deadline < block.timestamp) { | ||
revert DeadlinePassed(block.timestamp, params.deadline); | ||
} | ||
|
||
_validatePath(params.path, address(params.tokenToSendToConverter), address(params.tokenToReceiveFromConverter)); | ||
|
||
(uint256 amountToReceive, uint256 amountToPay) = params.converter.getUpdatedAmountIn( | ||
params.amount, | ||
address(params.tokenToSendToConverter), | ||
address(params.tokenToReceiveFromConverter) | ||
); | ||
if (params.amount != amountToReceive) { | ||
revert InsufficientLiquidity(params.amount, amountToReceive); | ||
} | ||
|
||
if (params.minIncome < 0) { | ||
params.tokenToReceiveFromConverter.safeTransferFrom(msg.sender, address(this), _u(-params.minIncome)); | ||
} | ||
|
||
ConversionData memory data = ConversionData({ | ||
beneficiary: params.beneficiary, | ||
tokenToSendToConverter: params.tokenToSendToConverter, | ||
amountToSendToConverter: amountToPay, | ||
tokenToReceiveFromConverter: params.tokenToReceiveFromConverter, | ||
amountToReceiveFromConverter: amountToReceive, | ||
minIncome: params.minIncome, | ||
converter: params.converter | ||
}); | ||
|
||
_flashSwap(FlashSwapParams({ amountOut: amountToPay, path: params.path, data: abi.encode(data) })); | ||
} | ||
|
||
function _validatePath(bytes calldata path, address expectedPathStart, address expectedPathEnd) internal pure { | ||
address swapStart = path.toAddress(0); | ||
if (swapStart != expectedPathStart) { | ||
revert InvalidSwapStart(expectedPathStart, swapStart); | ||
} | ||
|
||
address swapEnd = path.toAddress(path.length - 20); | ||
if (swapEnd != expectedPathEnd) { | ||
revert InvalidSwapEnd(expectedPathEnd, swapEnd); | ||
} | ||
} | ||
|
||
function _onMoneyReceived(bytes memory data) internal override returns (IERC20 tokenIn, uint256 maxAmountIn) { | ||
ConversionData memory decoded = abi.decode(data, (ConversionData)); | ||
|
||
uint256 receivedAmount = _convertViaTokenConverter( | ||
decoded.converter, | ||
decoded.tokenToSendToConverter, | ||
decoded.tokenToReceiveFromConverter, | ||
decoded.amountToReceiveFromConverter | ||
); | ||
|
||
return (decoded.tokenToReceiveFromConverter, _u(_i(receivedAmount) - decoded.minIncome)); | ||
} | ||
|
||
function _onFlashSwapCompleted(bytes memory data) internal override { | ||
ConversionData memory decoded = abi.decode(data, (ConversionData)); | ||
transferAll(decoded.tokenToReceiveFromConverter, address(this), decoded.beneficiary); | ||
} | ||
|
||
/// @dev Get `tokenToReceive` from TokenConverter, paying with `tokenToPay` | ||
/// @param converter TokenConverter contract | ||
/// @param tokenToPay Token to be sent to TokenConverter | ||
/// @param tokenToReceive Token to be received from TokenConverter | ||
/// @param amountToReceive Amount to receive from TokenConverter in `tokenToReceive` tokens | ||
function _convertViaTokenConverter( | ||
IAbstractTokenConverter converter, | ||
IERC20 tokenToPay, | ||
IERC20 tokenToReceive, | ||
uint256 amountToReceive | ||
) internal returns (uint256) { | ||
uint256 balanceBefore = tokenToReceive.balanceOf(address(this)); | ||
uint256 maxAmountToPay = tokenToPay.balanceOf(address(this)); | ||
approveOrRevert(tokenToPay, address(converter), maxAmountToPay); | ||
converter.convertForExactTokens( | ||
maxAmountToPay, | ||
amountToReceive, | ||
address(tokenToPay), | ||
address(tokenToReceive), | ||
address(this) | ||
); | ||
approveOrRevert(tokenToPay, address(converter), 0); | ||
uint256 tokensReceived = tokenToReceive.balanceOf(address(this)) - balanceBefore; | ||
return tokensReceived; | ||
} | ||
|
||
function _u(int256 value) private pure returns (uint256) { | ||
if (value < 0) { | ||
revert Underflow(); | ||
} | ||
return uint256(value); | ||
} | ||
|
||
function _i(uint256 value) private pure returns (int256) { | ||
if (value > uint256(type(int256).max)) { | ||
revert Overflow(); | ||
} | ||
return int256(value); | ||
} | ||
} |
Oops, something went wrong.