Skip to content

Commit

Permalink
Merge pull request #3 from VenusProtocol/feat/converter-operator
Browse files Browse the repository at this point in the history
[VEN-2299]: add a contract to operate token converters
  • Loading branch information
kkirka authored Jan 25, 2024
2 parents e932013 + 3418642 commit b3d3c3f
Show file tree
Hide file tree
Showing 18 changed files with 2,755 additions and 48 deletions.
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

0 comments on commit b3d3c3f

Please sign in to comment.