-
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.
feat: add a contract to operate MoveDebtDelegate
- Loading branch information
Showing
8 changed files
with
519 additions
and
0 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,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); | ||
} | ||
} | ||
} |
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,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()); | ||
} | ||
} |
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,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; | ||
} | ||
} |
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,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); | ||
} |
Oops, something went wrong.