Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VEN-2299]: add a contract to operate token converters #3

Merged
merged 5 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions contracts/flash-swap/ExactOutputFlashSwap.sol
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);
}
}
}
211 changes: 211 additions & 0 deletions contracts/operators/TokenConverterOperator.sol
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);
}
}
Loading