Skip to content

Commit

Permalink
feat: add a contract to operate MoveDebtDelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
kkirka committed Dec 14, 2023
1 parent 5d4cfcc commit 5fb603f
Show file tree
Hide file tree
Showing 8 changed files with 519 additions and 0 deletions.
163 changes: 163 additions & 0 deletions contracts/flash-swap/ExactOutputFlashSwap.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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);
}

uint256 maxAmountIn = _onMoneyReceived(envelope.data);

if (envelope.path.hasMultiplePools()) {
bytes memory remainingPath = envelope.path.skipToken();
approveOrRevert(tokenToPay, address(SWAP_ROUTER), maxAmountIn);
SWAP_ROUTER.exactOutput(
ISmartRouter.ExactOutputParams({
path: remainingPath,
recipient: msg.sender, // repaying to the pool
amountOut: amountToPay,
amountInMaximum: maxAmountIn
})
);
approveOrRevert(tokenToPay, 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 maxAmountIn Maximum amount of token X to be used to repay the flash swap
function _onMoneyReceived(bytes memory data) internal virtual returns (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
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);
}
}
}
91 changes: 91 additions & 0 deletions contracts/operators/MoveDebtOperator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IVBep20 } from "@venusprotocol/venus-protocol/contracts/InterfacesV8.sol";
import { MoveDebtDelegate } from "@venusprotocol/venus-protocol/contracts/DelegateBorrowers/MoveDebtDelegate.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 { ExactOutputFlashSwap } from "../flash-swap/ExactOutputFlashSwap.sol";

contract MoveDebtOperator is ExactOutputFlashSwap {
using SafeERC20 for IERC20;

struct MoveDebtParams {
uint256 maxExtraAmount;
address originalSender;
address originalBorrower;
uint256 repayAmount;
IVBep20 vTokenToBorrow;
}

MoveDebtDelegate public immutable DELEGATE;

constructor(ISmartRouter swapRouter_, MoveDebtDelegate delegate_) ExactOutputFlashSwap(swapRouter_) {
ensureNonzeroAddress(address(delegate_));
DELEGATE = delegate_;
}

function moveDebt(
address originalBorrower,
uint256 repayAmount,
IVBep20 vTokenToBorrow,
uint256 maxExtraAmount,
bytes memory path
) external {
MoveDebtParams memory params = MoveDebtParams({
maxExtraAmount: maxExtraAmount,
originalSender: msg.sender,
originalBorrower: originalBorrower,
repayAmount: repayAmount,
vTokenToBorrow: vTokenToBorrow
});

bytes memory data = abi.encode(params);
_flashSwap(FlashSwapParams({ amountOut: repayAmount, path: path, data: data }));
}

function _onMoneyReceived(bytes memory data) internal override returns (uint256 maxAmountIn) {
MoveDebtParams memory params = abi.decode(data, (MoveDebtParams));
IERC20 repayToken = _repayToken();
IERC20 borrowToken = _borrowToken(params);

uint256 balanceBefore = borrowToken.balanceOf(address(this));

approveOrRevert(repayToken, address(DELEGATE), params.repayAmount);
DELEGATE.moveDebt(params.originalBorrower, params.repayAmount, params.vTokenToBorrow);
approveOrRevert(repayToken, address(DELEGATE), 0);

if (params.maxExtraAmount > 0) {
borrowToken.safeTransferFrom(params.originalSender, address(this), params.maxExtraAmount);
}

uint256 balanceAfter = borrowToken.balanceOf(address(this));
return balanceAfter - balanceBefore;
}

function _onFlashSwapCompleted(bytes memory data) internal override {
MoveDebtParams memory params = abi.decode(data, (MoveDebtParams));

_transferAll(_borrowToken(params), params.originalSender);
_transferAll(_repayToken(), params.originalSender);
}

function _transferAll(IERC20 token, address to) internal {
uint256 balance = token.balanceOf(address(this));
if (balance > 0) {
token.safeTransfer(to, balance);
}
}

function _repayToken() internal view returns (IERC20) {
return IERC20(DELEGATE.vTokenToRepay().underlying());
}

function _borrowToken(MoveDebtParams memory params) internal view returns (IERC20) {
return IERC20(params.vTokenToBorrow.underlying());
}
}
95 changes: 95 additions & 0 deletions contracts/third-party/pancakeswap-v8/BytesLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* @title Solidity Bytes Arrays Utils
* @author Gonçalo Sá <goncalo.sa@consensys.net>
*
* @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity.
* The library lets you concatenate, slice and type cast bytes arrays both in memory and storage.
*/
pragma solidity >=0.8.0 <0.9.0;

library BytesLib {
function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) {
require(_length + 31 >= _length, "slice_overflow");
require(_bytes.length >= _start + _length, "slice_outOfBounds");

bytes memory tempBytes;

assembly {
switch iszero(_length)
case 0 {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)

// The first word of the slice result is potentially a partial
// word read from the original array. To read it, we calculate
// the length of that partial word and start copying that many
// bytes into the array. The first word we copy will start with
// data we don't care about, but the last `lengthmod` bytes will
// land at the beginning of the contents of the new array. When
// we're done copying, we overwrite the full first word with
// the actual length of the slice.
let lengthmod := and(_length, 31)

// The multiplication in the next line is necessary
// because when slicing multiples of 32 bytes (lengthmod == 0)
// the following copy loop was copying the origin's length
// and then ending prematurely not copying everything it should.
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
let end := add(mc, _length)

for {
// The multiplication in the next line has the same exact purpose
// as the one above.
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}

mstore(tempBytes, _length)

//update free-memory pointer
//allocating the array padded to 32 bytes like the compiler does now
mstore(0x40, and(add(mc, 31), not(31)))
}
//if we want a zero-length slice let's just return a zero-length array
default {
tempBytes := mload(0x40)
//zero out the 32 bytes slice we are about to return
//we need to do it because Solidity does not garbage collect
mstore(tempBytes, 0)

mstore(0x40, add(tempBytes, 0x20))
}
}

return tempBytes;
}

function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
address tempAddress;

assembly {
tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
}

return tempAddress;
}

function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
require(_start + 3 >= _start, "toUint24_overflow");
require(_bytes.length >= _start + 3, "toUint24_outOfBounds");
uint24 tempUint;

assembly {
tempUint := mload(add(add(_bytes, 0x3), _start))
}

return tempUint;
}
}
26 changes: 26 additions & 0 deletions contracts/third-party/pancakeswap-v8/ISmartRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0;

interface ISmartRouter {
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}

struct ExactOutputParams {
bytes path;
address recipient;
uint256 amountOut;
uint256 amountInMaximum;
}

function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);

function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);

function deployer() external view returns (address);

function WETH9() external view returns (address);
}
Loading

0 comments on commit 5fb603f

Please sign in to comment.